diff --git a/server/pruef-buehne.mjs b/server/pruef-buehne.mjs
index 2340d59e..27950047 100644
--- a/server/pruef-buehne.mjs
+++ b/server/pruef-buehne.mjs
@@ -247,17 +247,18 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"]
el2 && el2 !== e && !el2.contains(e) && !e.contains(el2)
&& el2.children.length === 0 && el2.textContent.trim().length > 0;
- let schlecht = null, anzahl = 0, ohneStelle = 0;
+ let schlecht = null, anzahl = 0, ohneStelle = 0, uebersprungen = 0;
const alle = [...document.querySelectorAll("p, h1, h2, h3, span, a, li")]
.filter((e) => e.children.length === 0 && e.textContent.trim().length > 2);
for (const e of alle) {
const r = e.getBoundingClientRect();
- if (r.width < 8 || r.height < 8) continue;
- if (r.top < 0 || r.bottom > window.innerHeight) continue;
+ if (r.width < 8 || r.height < 8) { uebersprungen++; continue; }
+ if (r.top < 0 || r.bottom > window.innerHeight) { uebersprungen++; continue; }
const mitte = r.top + r.height / 2;
const st0 = getComputedStyle(e);
const kandidaten = [];
+ let gemessenBei = null, gemessenAuf = null;
/* ZUERST IN DER EIGENEN POLSTERUNG.
@@ -286,9 +287,81 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"]
for (const [kx, ky] of kandidaten) {
if (kx < 0 || ky < 0 || kx >= window.innerWidth || ky >= window.innerHeight) continue;
if (istFremderText(document.elementFromPoint(kx, ky), e)) continue;
+ /* UND KEIN FREMDER UNTERGRUND (22.09.2026).
+ ============================================================
+ Diese Pruefung meldete tagelang 3,28:1 fuer den Markentext
+ in der Kopfleiste. Drei Vermutungen nacheinander gemessen
+ und alle drei widerlegt: nicht die Glut der Leiste, nicht
+ der Schein hinter der Schrift, nicht die Schriftfarbe.
+
+ Die Antwort kam erst, als die Pruefung sagen konnte, WO
+ sie misst: bei 125,34 -- auf `button.wahl2__knopf`. Bei
+ 390 px liegt der Sicht-Umschalter direkt neben dem
+ Schriftzug, und der Abtastpunkt 4 px rechts daneben trifft
+ SEINEN Hintergrund. Der Text liegt dort nicht drauf.
+
+ Dieselbe Lehre wie schon einmal oben, nur andersherum:
+ Damals mass sie daneben statt innen, hier misst sie
+ daneben und trifft einen Nachbarn. Ein Element mit
+ eigenem, deckendem Hintergrund, das KEIN Vorfahre des
+ Textes ist, taugt nicht als sein Untergrund.
+
+ Die Regel ist schmal: Nur deckende Hintergruende zaehlen
+ als fremd. Ein durchsichtiger Nachbar laesst denselben
+ Grund durch wie der Text selbst -- dort darf weiter
+ gemessen werden. */
+ const dort = document.elementFromPoint(kx, ky);
+ if (dort && dort !== e && !e.contains(dort) && !dort.contains(e)) {
+ /* WESSEN FLAECHE IST DAS? Der Knopf selbst ist oft
+ durchsichtig -- seine Flaeche traegt der Behaelter
+ darueber. Deshalb die Elternkette hoch bis zum ersten
+ deckenden Hintergrund und fragen, ob DER auch ueber dem
+ Text liegt. Tut er das nicht, ist es eine fremde
+ Flaeche, und der Text liegt dort nicht drauf. */
+ const flaecheVon = (n) => {
+ for (let q = n; q && q !== document.documentElement; q = q.parentElement) {
+ const st2 = getComputedStyle(q);
+ const g2 = st2.backgroundColor || "";
+ const teile = g2.match(/[\d.]+/g);
+ const deckend = teile && (teile.length < 4 || Number(teile[3]) >= 0.5)
+ && !/rgba\(0, 0, 0, 0\)/.test(g2);
+ const bild = st2.backgroundImage && st2.backgroundImage !== "none"
+ && (st2.webkitBackgroundClip || st2.backgroundClip || "")
+ .split(",")[0].trim() !== "text";
+ if (deckend || bild) return q;
+ }
+ return null;
+ };
+ const meine = flaecheVon(e);
+ const fremde = flaecheVon(dort);
+ if (fremde && fremde !== meine) continue;
+ /* AUCH EINE HALBDURCHSICHTIGE EIGENE FLAECHE ZAEHLT.
+ Der Knopf, der den Fehlalarm ausloeste, liegt in
+ DERSELBEN Kopfleiste wie der Text -- die deckende
+ Flaeche ist also dieselbe. Er traegt aber darueber noch
+ eine eigene, halbdurchsichtige, und genau die hebt den
+ gemessenen Punkt an. Ein Text liegt darauf nicht drauf,
+ er liegt DANEBEN. */
+ {
+ let fremdeFlaeche = false;
+ for (let q = dort; q && q !== meine && !q.contains(e); q = q.parentElement) {
+ const st3 = getComputedStyle(q);
+ const t3 = (st3.backgroundColor || "").match(/[\d.]+/g);
+ const hatFarbe = t3 && (t3.length < 4 || Number(t3[3]) > 0.02);
+ const hatBild = st3.backgroundImage && st3.backgroundImage !== "none";
+ if (hatFarbe || hatBild) { fremdeFlaeche = true; break; }
+ }
+ if (fremdeFlaeche) continue;
+ }
+ }
const bx = Math.round(kx * skala), by = Math.round(ky * skala);
if (bx < 0 || bx >= c.width || by < 0 || by >= c.height) continue;
p = g.getImageData(bx, by, 1, 1).data;
+ gemessenBei = Math.round(kx) + "," + Math.round(ky);
+ const hier = document.elementFromPoint(kx, ky);
+ gemessenAuf = hier ? hier.tagName.toLowerCase()
+ + (typeof hier.className === "string" && hier.className
+ ? "." + hier.className.trim().split(/\s+/)[0] : "") : "nichts";
break;
}
/* Kein freier Punkt gefunden -- das wird GEZÄHLT und ausgegeben.
@@ -328,10 +401,26 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"]
const v = kontrast(farbe, [p[0], p[1], p[2]]);
anzahl++;
if (!schlecht || v / grenze < schlecht.v / schlecht.grenze) {
- schlecht = { v, grenze, text: e.textContent.trim().slice(0, 26), grund: [p[0], p[1], p[2]] };
+ /* WO ES STEHT, NICHT NUR WAS DASTEHT (22.09.2026).
+ Diese Pruefung meldete tagelang 3,28:1 bei "Spicy & Dogi"
+ -- und die Suche danach kostete eine halbe Stunde, weil
+ derselbe Text an mehreren Stellen vorkommt. Eine Pruefung,
+ die einen Fehler findet, ihn aber nicht auffindbar macht,
+ kostet mehr Zeit als sie spart. */
+ const wo = e.tagName.toLowerCase()
+ + (typeof e.className === "string" && e.className
+ ? "." + e.className.trim().split(/\s+/).join(".") : "");
+ schlecht = { v, grenze, text: e.textContent.trim().slice(0, 26),
+ grund: [p[0], p[1], p[2]], wo,
+ eltern: (() => { const k=[]; for (let q=e.parentElement; q && k.length<4; q=q.parentElement)
+ k.push(q.tagName.toLowerCase()+(typeof q.className==="string"&&q.className?"."+q.className.trim().split(/\s+/)[0]:"")); return k.join(" < "); })(),
+ lage: Math.round(r.left)+","+Math.round(r.top)+" "+Math.round(r.width)+"x"+Math.round(r.height),
+ gemessenBei, gemessenAuf,
+ groesse: Math.round(gr * 10) / 10, fett: st.fontWeight,
+ verlauf: clipText };
}
}
- return { ...schlecht, anzahl, ohneStelle, gesamt: alle.length };
+ return { ...schlecht, anzahl, ohneStelle, uebersprungen, gesamt: alle.length };
}, [bild.toString("base64"), DUNKELSTER_QUELLE]);
/* ---- GEGENPROBE ZUR VERLAUFSSCHRIFT ----------------------------
@@ -372,10 +461,35 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"]
`${s}: Gegenprobe -- dunkler Verlauf wird als dunkel erkannt `
+ `(${gegen.dunkel.dunkelster?.join(",")}), also faellt so etwas auf`);
- ok(schlimmster.v >= schlimmster.grenze,
+ /* DIE ZAHL GEHOERT IN DIE BEDINGUNG (22.09.2026).
+ Sie stand bisher nur im Meldetext. Am 22.09. wurde die Abtastung
+ nachsichtiger gemacht (fremde Flaechen zaehlen nicht mehr als
+ Untergrund) -- richtig, aber eine Lockerung kann zu weit gehen.
+ Wuerde sie irgendwann JEDEN Punkt verwerfen, waere diese Zeile
+ fuer immer gruen, ohne je wieder etwas anzusehen.
+ ERSTER ANLAUF WAR `anzahl >= 5` -- und die Pruefung wurde auf
+ uebersicht.html sofort rot, weil die Seite schlicht nur vier
+ Texte hat. Eine feste Schwelle ist eine Rechnung von gestern;
+ sie war hier keine fuenf Minuten alt.
+
+ Richtig ist die ABLEITUNG: Jeder gefundene Text muss in genau
+ einem der drei Toepfe landen -- gemessen, ohne freien Untergrund,
+ oder vorher aussortiert (zu klein / ausserhalb des Bildes). Geht
+ die Rechnung nicht auf, ist unterwegs etwas verschwunden, und
+ zwar still. Diese Bedingung braucht keine Zahl und veraltet nicht.
+ Wie VIELE davon gemessen werden duerfen, deckelt die Zeile
+ darunter ("hoechstens zwei Texte ohne freien Untergrund"). */
+ ok(schlimmster.anzahl + schlimmster.ohneStelle + schlimmster.uebersprungen
+ === schlimmster.gesamt
+ && schlimmster.v >= schlimmster.grenze,
`${s}: schlechtester Kontrast ${schlimmster.v.toFixed(2)}:1 (nötig ${schlimmster.grenze}:1) `
+ `bei "${schlimmster.text}" auf rgb(${schlimmster.grund.join(",")}) · `
- + `${schlimmster.anzahl} Stellen gemessen`);
+ + `${schlimmster.anzahl} gemessen + ${schlimmster.ohneStelle} ohne Grund `
+ + `+ ${schlimmster.uebersprungen} aussortiert = ${schlimmster.gesamt}`
+ + (schlimmster.wo ? ` -- ${schlimmster.wo}, ${schlimmster.groesse}px/${schlimmster.fett}`
+ + (schlimmster.verlauf ? ", Verlauf im Text" : "")
+ + ` [${schlimmster.lage}] in ${schlimmster.eltern}`
+ + ` -- gemessen bei ${schlimmster.gemessenBei} auf ${schlimmster.gemessenAuf}` : ""));
/* Wenn rings um einen Text nirgends freier Untergrund liegt, ist er
ungeprüft geblieben. Das darf vorkommen (dicht gesetzte Zeilen),
aber nicht unbemerkt und nicht bei vielen -- sonst meldet die
diff --git a/server/pruef-chatkachel.mjs b/server/pruef-chatkachel.mjs
index fe0a13f3..05e41fc5 100644
--- a/server/pruef-chatkachel.mjs
+++ b/server/pruef-chatkachel.mjs
@@ -21,12 +21,15 @@
AUFRUF: node server/pruef-chatkachel.mjs
===================================================================== */
-import { mkdtempSync, rmSync } from "node:fs";
+import { mkdtempSync, rmSync, readFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { dirname } from "node:path";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { eigenerPort } from "./helfer-port.mjs";
import { notbremse } from "./helfer-notbremse.mjs";
+const HIER = dirname(fileURLToPath(import.meta.url));
const PORT = await eigenerPort(import.meta, "pruef-chatkachel");
const ordner = mkdtempSync(join(tmpdir(), "ws-kachel-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
@@ -66,23 +69,98 @@ const kontrast = (a, b) => {
const [h, d] = [helligkeit(a), helligkeit(b)].sort((x, y) => y - x);
return (h + 0.05) / (d + 0.05);
};
-const SCHRIFT = "#e8f1fa"; /* die Textfarbe des Hauses */
+/* GEGEN WELCHE SCHRIFT? DIE, DIE WIRKLICH AUF DER BLASE STEHT.
+ ---------------------------------------------------------------------
+ Hier stand `const SCHRIFT = "#e8f1fa"` — die Textfarbe des Hauses,
+ abgeschrieben. Am 22.09.2026 bekam die Blase ihre EIGENE Schrift
+ (--blase-text / --blase-leise, beide heller), damit die zwölf Töne
+ überhaupt Farbe tragen können. Von da an maß diese Prüfung eine
+ Farbe, die auf einer Blase gar nicht mehr vorkommt.
-let schlechtester = 99, schlechtesterName = "";
-for (const k of CHAT_KACHELN) {
- const c = kontrast(SCHRIFT, k.farbe);
- if (c < schlechtester) { schlechtester = c; schlechtesterName = k.name; }
+ Das ist genau der Fehler, der an den Startkacheln Wochen gekostet
+ hat: eine Prüfung, die grün oder rot ist, ohne das anzusehen, was
+ auf dem Bildschirm steht. Eine abgeschriebene Farbe ist eine
+ Bestandsliste, und Bestandslisten altern.
+
+ Gelesen wird jetzt aus chat.css — derselben Stelle, aus der auch
+ `tools/chat-kacheln-rechnen.mjs` liest. Eine Quelle, zwei Leser.
+ Findet sie sich dort nicht, ist das der DRITTE AUSGANG: Die Prüfung
+ kann nichts messen und sagt es, statt eine Ersatzfarbe zu erfinden
+ und weiter grün zu leuchten. */
+const chatCss = readFileSync(
+ join(HIER, "..", "workspace", "assets", "css", "chat.css"), "utf8");
+function ausCss(name) {
+ const m = chatCss.match(new RegExp("--" + name + ":\\s*(#[0-9a-fA-F]{3,6})\\s*;"));
+ return m ? m[1] : null;
+}
+/* Die Zahlen rechts sind dieselben wie im Rechenwerkzeug — 7:1 (AAA)
+ für den Fließtext, weil der Chat die Seite ist, auf der am längsten
+ gelesen wird, und 4,5:1 für die vier Handgriffe der Fußzeile. */
+const BLASEN_SCHRIFT = [
+ ["blase-text", ".chat-nachricht__text", 7],
+ ["blase-leise", ".chat-nachricht__fuss", 4.5],
+];
+for (const [name, wo, noetig] of BLASEN_SCHRIFT) {
+ const farbe = ausCss(name);
+ if (!farbe) {
+ ok(false, `KONNTE NICHT NACHSEHEN: --${name} steht nicht in chat.css`
+ + " — ohne sie lässt sich der Kontrast der Kacheln nicht prüfen");
+ continue;
+ }
+ let schlecht = Infinity, wer = "";
+ for (const k of CHAT_KACHELN) {
+ const c = kontrast(farbe, k.farbe);
+ if (c < schlecht) { schlecht = c; wer = k.name; }
+ }
+ /* Die ANZAHL gehört in die Bedingung. Wäre CHAT_KACHELN je leer,
+ bliebe `schlecht` auf Infinity und die Zeile wäre grün, ohne einen
+ einzigen Ton angesehen zu haben. */
+ ok(CHAT_KACHELN.length >= 8 && schlecht >= noetig,
+ `${wo} trägt auf allen ${CHAT_KACHELN.length} Tönen `
+ + `(schlechtester ${wer}: ${schlecht.toFixed(2)}:1, nötig ${noetig})`);
}
-/* 7:1 ist die strengere WCAG-Stufe (AAA für Fließtext). Ein Chat ist
- die Seite, auf der am längsten gelesen wird — hier die niedrigere
- Schwelle zu nehmen wäre am falschen Ende gespart. */
-ok(schlechtester >= 7,
- `auch der hellste Ton trägt die Schrift noch (${schlechtesterName}: ${schlechtester.toFixed(1)}:1, nötig 7)`);
+/* GEGENPROBE: Eine Prüfung, die nur bestätigen kann, bestätigt nichts.
+ Ein absichtlich zu heller Ton MUSS durchfallen — sonst wäre das
+ Grün oben nur die Abwesenheit einer Messung. */
+{
+ const hell = ausCss("blase-text") || "#f3f7fc";
+ ok(kontrast(hell, "#8fa2bd") < 7,
+ "Gegenprobe — ein zu heller Ton fiele durch "
+ + `(${kontrast(hell, "#8fa2bd").toFixed(2)}:1 gegen nötige 7)`);
+}
+
+/* ALLE AUF DERSELBEN LEUCHTDICHTE — und das ist jetzt eine harte
+ Aussage, keine Nachsicht mehr. Hier stand Faktor 6; das war die
+ Toleranz für zwölf von Hand gewählte Grautöne. Seit sie gerechnet
+ werden, liegen sie auf einem Wert, und der Rest ist Rundung auf
+ ganze Bildpunkte. Ein Faktor über 1,1 hieße: jemand hat eine Farbe
+ von Hand eingetragen und damit die Rechnung umgangen. */
const spanne = Math.max(...CHAT_KACHELN.map((k) => helligkeit(k.farbe)))
/ Math.max(1e-6, Math.min(...CHAT_KACHELN.map((k) => helligkeit(k.farbe))));
-ok(spanne < 6,
- `die Töne liegen nah beieinander (Faktor ${spanne.toFixed(1)}) — keiner sticht heraus`);
+ok(spanne < 1.1,
+ `alle Töne liegen auf derselben Leuchtdichte (Faktor ${spanne.toFixed(3)}) `
+ + "— keiner ist heller oder dunkler als die anderen");
+
+/* UND SIE MÜSSEN BUNT SEIN. Gemessen an dem, was vor dem 22.09.2026
+ drinstand: Buntheit 0,002 bis 0,048, im Mittel 0,028 — zwölf
+ Grautöne. Ohne diese Zeile könnte die Liste unbemerkt dorthin
+ zurückfallen, und alle Kontrastprüfungen oben blieben grün. */
+const buntheit = (hexf) => {
+ const z = hexf.replace("#", "");
+ const [r, g, b] = [0, 2, 4].map((i) => parseInt(z.slice(i, i + 2), 16) / 255)
+ .map((c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
+ return Math.hypot(1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
+ 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s);
+};
+/* „grund" ist der bewusst neutrale Standard und zählt hier nicht mit. */
+const bunt = CHAT_KACHELN.filter((k) => k.wert !== "grund").map((k) => buntheit(k.farbe));
+ok(bunt.length >= 8 && Math.min(...bunt) >= 0.06,
+ `alle ${bunt.length} Farbtöne sind wirklich bunt `
+ + `(blassester ${Math.min(...bunt).toFixed(3)}, vor dem Umbau lag der bunteste bei 0,048)`);
/* KEINE DOPPELTE FARBE. Zwei gleiche Töne wären zwei Namen für
dieselbe Kachel — man wählt den zweiten und sieht keinen Unterschied. */
@@ -156,9 +234,42 @@ const verlauf = await (await fetch(BASIS + `/workspace/api/chat/raeume/${raum}/n
const vonLuna = (verlauf.nachrichten || []).find((n) => n.von === "Luna");
ok(vonLuna?.von_kachel === "moos",
`Filipe bekommt Lunas Kachel mitgeliefert (${vonLuna?.von_kachel})`);
+/* UND WER NICHTS EINGESTELLT HAT, TRÄGT TROTZDEM EINE FARBE.
+ ---------------------------------------------------------------------
+ Hier stand bis zum 22.09.2026 das Gegenteil: `ok(!vonFilipe
+ ?.von_kachel, "und seine eigene bleibt Standard …")`. Das war
+ richtig, solange „nichts eingestellt" Grau hieß.
+
+ Gemessen an der Live-Datenbank hatten vier von fünfzehn Personen
+ eine Farbe gewählt — der Chat war also für alle anderen einfarbig,
+ egal wie gut die zwölf Töne gerechnet sind. Seitdem bekommt jede
+ Person von sich aus einen der elf bunten Töne, stabil aus ihrer ID.
+
+ Geprüft wird jetzt beides, und das Zweite ist das Wichtigere:
+ dass die Farbe SICH NICHT ÄNDERT. Eine Person, die heute grün und
+ morgen rot ist, ist schlechter als eine ohne Farbe — man merkt sich
+ die falsche. */
const vonFilipe = (verlauf.nachrichten || []).find((n) => n.von === "Filipe");
-ok(!vonFilipe?.von_kachel,
- "und seine eigene bleibt Standard, solange er nichts einstellt");
+const bunteToene = CHAT_KACHELN.filter((k) => k.wert !== "grund").map((k) => k.wert);
+ok(bunteToene.length >= 8 && bunteToene.includes(vonFilipe?.von_kachel),
+ `und wer nichts eingestellt hat, trägt trotzdem eine Farbe `
+ + `(${vonFilipe?.von_kachel})`);
+
+/* Zweimal fragen muss dasselbe ergeben. */
+const nochmal = await (await fetch(BASIS + `/workspace/api/chat/raeume/${raum}/nachrichten`,
+ { headers: { Cookie: kFilipe } })).json();
+const vonFilipe2 = (nochmal.nachrichten || []).find((n) => n.von === "Filipe");
+ok(!!vonFilipe?.von_kachel && vonFilipe2?.von_kachel === vonFilipe?.von_kachel,
+ `und es ist beim zweiten Abruf dieselbe (${vonFilipe2?.von_kachel})`);
+
+/* Der Wähler darf sie NICHT als gewählt anhaken — sonst sähe es aus,
+ als hätte man selbst etwas eingestellt, und der neutrale Ton wäre
+ unerreichbar geworden. */
+const meins = await (await fetch(BASIS + "/workspace/api/chat/kacheln",
+ { headers: { Cookie: kFilipe } })).json();
+ok(meins.meine == null && bunteToene.includes(meins.automatisch),
+ `im Wähler steht trotzdem nichts angehakt (meine=${meins.meine}), `
+ + `und die automatische wird genannt (${meins.automatisch})`);
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await pw.chromium.launch();
@@ -313,7 +424,21 @@ try {
return c.backgroundColor + " | " + c.backgroundImage;
});
const vorher = await kachelBild();
- await s.click('.chat-kachelwahl__ton[data-wert="wein"]');
+ /* WELCHEN TON ANKLICKEN? EINEN AUS DER LISTE, NICHT EINEN GEMERKTEN.
+ Hier stand `data-wert="wein"` fest im Text. Am 22.09.2026 wurden
+ die zwoelf Toene neu gerechnet, `wein` fiel weg -- und die Pruefung
+ wartete 30 Sekunden auf einen Knopf, den es nicht mehr gibt, und
+ brach ab. Der Fehler lag nicht im Chat, sondern in dieser Zeile.
+
+ Genommen wird jetzt der erste Ton, der NICHT die Voreinstellung
+ ist. Damit ist garantiert, dass sich etwas aendert -- und die
+ Zeile ueberlebt jede kuenftige Umbenennung. */
+ const probeTon = CHAT_KACHELN.find((k) => k.wert !== "grund");
+ if (!probeTon) {
+ ok(false, "KONNTE NICHT NACHSEHEN: es gibt keinen Ton ausser dem Standard");
+ throw new Error("keine Toene zum Pruefen");
+ }
+ await s.click(`.chat-kachelwahl__ton[data-wert="${probeTon.wert}"]`);
await s.waitForTimeout(600);
const nachher = await kachelBild();
ok(vorher !== nachher,
@@ -322,8 +447,9 @@ try {
/* UND ES IST GESPEICHERT, nicht nur gemalt. */
const nachSetzen = await (await fetch(BASIS + "/workspace/api/chat/kacheln",
{ headers: { Cookie: kFilipe } })).json();
- ok(nachSetzen.meine === "wein",
- `und die Wahl ist gespeichert, nicht nur gemalt (${nachSetzen.meine})`);
+ ok(nachSetzen.meine === probeTon.wert,
+ `und die Wahl ist gespeichert, nicht nur gemalt `
+ + `(${nachSetzen.meine}, erwartet ${probeTon.wert})`);
ok(konsole.length === 0,
konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 80) : "keine Fehler im Browser");
diff --git a/server/pruef-kachelfarben.mjs b/server/pruef-kachelfarben.mjs
index 4b67e4f5..fa94ce65 100644
--- a/server/pruef-kachelfarben.mjs
+++ b/server/pruef-kachelfarben.mjs
@@ -170,6 +170,72 @@ console.log("\n=== Kein Paar sieht gleich aus ===");
`${eng.d.toFixed(4)} zwischen Ton ${eng.a?.n} und ${eng.b?.n}`);
}
+/* ---- 1b. Die Willkommenskachel traegt WIRKLICH alle Farben ---------- */
+/* Filipe: „die kachel soll alle farben haben die wir benutzt haben."
+ ---------------------------------------------------------------------
+ `tools/kachel-regenbogen.mjs` schreibt den Streifenblock aus den
+ benutzten Toenen. Einmal gelaufen ist er aber ab da eine ABSCHRIFT --
+ und Abschriften altern. Kommt morgen eine Kachel dazu, hat „alle
+ Farben" eben doch nicht alle, und niemand sieht es: 30 Streifen sehen
+ aus wie 31.
+
+ Deshalb wird hier nicht geprueft, ob der Block existiert, sondern ob
+ er noch zu den heute benutzten Toenen passt -- in BEIDE Richtungen.
+ Fehlt eine Farbe, ist die Kachel unvollstaendig; steht eine zu viel
+ darin, zeigt sie eine Farbe, die es auf der Wand nicht mehr gibt.
+
+ Die Pruefung sagt auch, was zu tun ist. Eine Pruefung, die einen
+ Fehler findet, ihn aber nicht behebbar macht, kostet mehr Zeit als
+ sie spart:
+ node tools/kachel-regenbogen.mjs --schreiben
+ -------------------------------------------------------------------- */
+console.log("\n=== Die Willkommenskachel hat alle Farben ===");
+{
+ const ANF = "/* ---- ANFANG GERECHNET: kachel-regenbogen.mjs ---- */";
+ const ENDE = "/* ---- ENDE GERECHNET ---- */";
+ const a = css.indexOf(ANF);
+ const e = css.indexOf(ENDE);
+ if (a < 0 || e < 0) {
+ pruef("KONNTE NICHT NACHSEHEN: der gerechnete Block steht nicht in"
+ + " start.css", false,
+ "erwartet zwischen den Marken ANFANG/ENDE GERECHNET");
+ } else {
+ const block = css.slice(a, e);
+ /* Gezaehlt werden die Farben im STREIFENVERLAUF, nicht im ganzen
+ Block -- Lesesaum und Vignette bringen eigene Werte mit, die
+ nichts mit den Kacheltoenen zu tun haben. */
+ const imBlock = new Set(
+ [...block.matchAll(/color-mix\(in srgb, (#[0-9a-fA-F]{6}) /g)]
+ .map((m) => m[1].toLowerCase()));
+ const sollen = new Set([...benutzt.keys()]
+ .filter((n) => farben.has(n))
+ .map((n) => farben.get(n).toLowerCase()));
+
+ const fehlen = [...sollen].filter((f) => !imBlock.has(f));
+ const zuviel = [...imBlock].filter((f) => !sollen.has(f));
+
+ pruef(`die Kachel zeigt ${imBlock.size} Farben, benutzt werden ${sollen.size}`,
+ imBlock.size >= 20 && imBlock.size === sollen.size,
+ `${imBlock.size} statt ${sollen.size}`);
+ pruef("keine benutzte Farbe fehlt in der Kachel",
+ fehlen.length === 0,
+ `es fehlen ${fehlen.length}: ${fehlen.join(", ")}`
+ + " — node tools/kachel-regenbogen.mjs --schreiben");
+ pruef("und keine Farbe steht darin, die niemand mehr benutzt",
+ zuviel.length === 0,
+ `${zuviel.length} zu viel: ${zuviel.join(", ")}`
+ + " — node tools/kachel-regenbogen.mjs --schreiben");
+
+ /* GEGENPROBE. Eine Mengenpruefung, die nur bestaetigen kann,
+ bestaetigt nichts: Waere der Streifenverlauf leer, waeren alle
+ drei Zeilen oben trivial gruen. */
+ const gefaelscht = new Set([...imBlock].filter((f) => f !== [...imBlock][0]));
+ pruef("Gegenprobe — fehlt eine Farbe, faellt es auf",
+ [...sollen].filter((f) => !gefaelscht.has(f)).length > 0,
+ "die Mengenpruefung merkt eine fehlende Farbe nicht");
+ }
+}
+
/* ---- 2. Eindeutig je Ansicht --------------------------------------- */
console.log("\n=== Keine Ansicht zeigt zwei gleiche ===");
{
diff --git a/server/workspace-chat.js b/server/workspace-chat.js
index 15c7ff39..68325ca1 100644
--- a/server/workspace-chat.js
+++ b/server/workspace-chat.js
@@ -188,8 +188,8 @@ function bildWegFuer(personId) {
Kachel gehoert einem selbst, GESEHEN wird sie von allen anderen --
deshalb ist sie kein Platz fuer etwas, das man nicht abschalten kann.
- Die zwoelf Toene hier sind alle gleich dunkel und gleich gesaettigt.
- Sie unterscheiden sich im Farbton und in sonst nichts. Damit ist der
+ Die zwoelf Toene hier liegen alle auf DERSELBEN LEUCHTDICHTE. Sie
+ unterscheiden sich im Farbton und in sonst nichts. Damit ist der
Kontrast zur Schrift bei jedem gleich -- er muss nicht bei jedem
einzeln geprueft werden, und niemand kann sich unlesbar machen.
Augenschonend ist hier keine Geschmacksfrage: Ein Chat ist die
@@ -197,21 +197,55 @@ function bildWegFuer(personId) {
KEINE der zwoelf ist hell. Das ist Absicht und nicht Sparsamkeit --
die Schrift ist hell, und eine helle Kachel waere fuer die Person,
- die sie waehlt, hübsch und fuer alle anderen unlesbar.
+ die sie waehlt, huebsch und fuer alle anderen unlesbar.
+
+ ---------------------------------------------------------------------
+ DIESE LISTE IST GERECHNET, NICHT GEWAEHLT (22.09.2026)
+
+ Filipe: „ich will dass die farben viel klarer und stärker sind ...
+ die kachel von den texten von den leuten im chat soll auch klarer
+ und stärker sein bitte."
+
+ Bis heute lagen alle zwoelf bei einer Buntheit zwischen 0,002 und
+ 0,048 -- auf dem Bildschirm zwoelf Grautoene mit einem Hauch Farbe.
+ Das lag nicht an fehlendem Mut, sondern an der Fusszeile der Blase:
+ Sie stand auf der leisen Hausschrift, und gegen die darf die Flaeche
+ kaum Farbe haben.
+
+ Geaendert wurde deshalb ZUERST die Schrift der Blase (--blase-text /
+ --blase-leise in chat.css, beide heller als die der Seite) und DANN
+ die Flaeche. Andersherum waere es der Fehler vom selben Vormittag
+ gewesen, als 27 von 31 Startkacheln unlesbar wurden.
+
+ NICHT VON HAND AENDERN. Die Werte kommen aus
+ `tools/chat-kacheln-rechnen.mjs`; es liest die beiden Schriftfarben
+ aus dem CSS und rechnet daraus die hellste Flaeche aus, die beide
+ noch tragen (7:1 fuer den Nachrichtentext, 4,8:1 fuer die
+ Fusszeile). Wer hier eine Farbe eintippt, umgeht genau die Rechnung,
+ die die Lesbarkeit garantiert. Richtig ist:
+
+ node tools/chat-kacheln-rechnen.mjs --schreiben
+ node server/pruef-chatkachel.mjs
+
+ DIE SCHLUESSEL LINKS STEHEN IN DER DATENBANK. Wer einen umbenennt,
+ setzt die Wahl der betroffenen Person still auf „Standard" zurueck.
+ Vor diesem Umbau nachgesehen: gewaehlt waren `veilchen`, `ziegel`
+ und `moos` -- alle drei stehen weiter unten und haben ihren
+ Farbbereich behalten.
===================================================================== */
export const CHAT_KACHELN = [
- { wert: "grund", name: "Standard", farbe: "#131a25" },
- { wert: "tinte", name: "Tinte", farbe: "#141c2e" },
- { wert: "veilchen", name: "Veilchen", farbe: "#1d1730" },
- { wert: "pflaume", name: "Pflaume", farbe: "#251729" },
- { wert: "wein", name: "Wein", farbe: "#2a1620" },
- { wert: "ziegel", name: "Ziegel", farbe: "#2a1a17" },
- { wert: "bernstein", name: "Bernstein", farbe: "#271f14" },
- { wert: "moos", name: "Moos", farbe: "#16251b" },
- { wert: "tanne", name: "Tanne", farbe: "#132622" },
- { wert: "lagune", name: "Lagune", farbe: "#122429" },
- { wert: "stahl", name: "Stahl", farbe: "#1b2128" },
- { wert: "schiefer", name: "Schiefer", farbe: "#20201f" },
+ { wert: "grund", name: "Neutral", farbe: "#4f545c" },
+ { wert: "tinte", name: "Tinte", farbe: "#0150af" },
+ { wert: "veilchen", name: "Veilchen", farbe: "#6a01e5" },
+ { wert: "pflaume", name: "Pflaume", farbe: "#9301a2" },
+ { wert: "kirsche", name: "Kirsche", farbe: "#a5015e" },
+ { wert: "ziegel", name: "Ziegel", farbe: "#ad0103" },
+ { wert: "bernstein", name: "Bernstein", farbe: "#7e4601" },
+ { wert: "oliv", name: "Oliv", farbe: "#635201" },
+ { wert: "moos", name: "Moos", farbe: "#405c01" },
+ { wert: "tanne", name: "Tanne", farbe: "#00603d" },
+ { wert: "lagune", name: "Lagune", farbe: "#035e5c" },
+ { wert: "ozean", name: "Ozean", farbe: "#045b74" },
];
/** Nur ein Ton aus der Liste kommt durch -- sonst nichts.
@@ -226,7 +260,63 @@ export function kachelPruefen(wert) {
return CHAT_KACHELN.some((k) => k.wert === String(wert)) ? String(wert) : null;
}
+/* WER NICHTS EINSTELLT, BEKOMMT TROTZDEM EINE FARBE (22.09.2026)
+ ---------------------------------------------------------------------
+ Filipe: „die kachel von den texten von den leuten im chat soll auch
+ klarer und stärker sein ... so dass die leute richtig bekommen es zu
+ benutzen."
+
+ Vorher hiess „nichts eingestellt" GRAU -- und eingestellt hatten es
+ vier von fuenfzehn. Auf dem Bildschirm war der Chat damit einfarbig,
+ egal wie gut die zwoelf Toene gerechnet sind. Eine Einstellung, die
+ man erst finden muss, damit ueberhaupt etwas passiert, benutzt kaum
+ jemand; das Ergebnis sehen aber alle.
+
+ Jetzt bekommt jede Person von sich aus einen der elf bunten Toene.
+ Dieselbe Person hat immer denselben -- man erkennt sie an der Farbe,
+ noch bevor man den Namen liest. Wer einen anderen will, waehlt ihn
+ weiterhin; die eigene Wahl gewinnt immer.
+
+ WARUM AUS DER ID UND NICHT AUS DEM NAMEN: Ein Name aendert sich
+ (Heirat, Schreibweise, Kuerzel), eine ID nie. Eine Farbe, die beim
+ Umbenennen springt, waere schlimmer als gar keine.
+
+ Der neutrale Ton bleibt in der Liste -- er ist jetzt eine WAHL
+ („ich will keine Farbe"), keine Voreinstellung. */
+/* WARUM NICHT EINFACH `id % 11`.
+ ---------------------------------------------------------------------
+ Die elf Toene stehen in der Reihenfolge ihres Farbwinkels -- Nachbarn
+ in der Liste sind auch Nachbarn im Farbkreis. Mit `id % 11` bekommen
+ zwei Leute, deren Nummern aufeinanderfolgen, deshalb zwei aehnliche
+ Farben. Gemessen am Bildschirm: Person 13 und 14 sassen im selben
+ Gespraech, eine magenta, eine rot -- auf dem Bildschirmfoto kaum zu
+ trennen, und genau das, was die Farbe verhindern soll.
+
+ Elf ist eine Primzahl. Damit ist JEDE Schrittweite von 1 bis 10
+ teilerfremd, laeuft also durch alle elf Toene, bevor sich einer
+ wiederholt -- keiner geht verloren. Die 4 ist gewaehlt, weil sie den
+ groessten Sprung ergibt: vier Schritte sind rund 131 Grad im
+ Farbkreis, fast das Maximum von 180. Aufeinanderfolgende Nummern
+ landen damit so weit auseinander wie moeglich. */
+const SPRUNG = 4;
+
+export function autoKachelFuer(personId) {
+ const bunte = CHAT_KACHELN.filter((k) => k.wert !== "grund");
+ if (!bunte.length) return null;
+ const n = Number(personId);
+ if (!Number.isFinite(n) || n < 0) return null;
+ return bunte[(n * SPRUNG) % bunte.length].wert;
+}
+
+/** Der Ton, den diese Person TRAEGT -- eigene Wahl, sonst die
+ * automatische. Fuer die Frage „was hat sie eingestellt?" gibt es
+ * `gewaehlteKachelFuer`; die beiden zu verwechseln hiesse, dass im
+ * Waehler ploetzlich ein Ton angehakt waere, den niemand gewaehlt hat. */
function kachelFuer(personId) {
+ return gewaehlteKachelFuer(personId) || autoKachelFuer(personId);
+}
+
+function gewaehlteKachelFuer(personId) {
try {
return kachelPruefen(db().prepare(
"SELECT chat_kachel FROM personen WHERE id = ?").get(personId)?.chat_kachel);
@@ -543,7 +633,12 @@ chatRouter.get("/workspace/api/chat/kacheln", (req, res) => {
try {
res.json({
kacheln: CHAT_KACHELN,
- meine: kachelFuer(req.person.id),
+ /* Die eigene WAHL, nicht der getragene Ton -- sonst stuende im
+ Waehler ein Haken bei einer Farbe, die niemand gewaehlt hat. */
+ meine: gewaehlteKachelFuer(req.person.id),
+ /* Damit der Waehler beim neutralen Eintrag zeigen kann, was
+ stattdessen getragen wird, wenn man nichts waehlt. */
+ automatisch: autoKachelFuer(req.person.id),
});
} catch (f) {
console.error("[chat] Kacheln:", f?.message);
@@ -1091,7 +1186,9 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
auch die von gestern. Die Farbe an jede Zeile zu schreiben
waere eine zweite Wahrheit -- und nach der ersten Aenderung
haette dieselbe Person zwei Farben im selben Gespraech. */
- von_kachel: kachelPruefen(n.von_kachel),
+ /* Eigene Wahl, sonst die automatische -- dieselbe Regel wie
+ ueberall, damit eine Person in jeder Liste gleich aussieht. */
+ von_kachel: kachelPruefen(n.von_kachel) || autoKachelFuer(n.person_id),
text: n.weg_am ? null : n.text,
zurueckgenommen: !!n.weg_am,
erstellt: n.erstellt,
diff --git a/server/workspace.js b/server/workspace.js
index a98f7278..baf76f27 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -1223,8 +1223,20 @@ const TREFF_BEREICHE = [
der Treff-Chat daneben 29 Nachrichten -- geredet wird im Chat, das
Brett war eine Kachel ohne Inhalt. Sie zeigt jetzt auf die Seite,
die erklaert, was es hier ueberhaupt gibt. */
+ /* ALLE FARBEN DES HAUSES IN EINER KACHEL (22.09.2026).
+
+ Filipe: „die farbe dieser kachel soll alle farben haben die es auf
+ der ganze seite gibt ... nicht gemischt ... soll komplett farbig
+ sein und ultra geil aussehen."
+
+ `ton: 34` bleibt stehen und ist kein Widerspruch: Er faerbt Rand,
+ Zeichen und Pfeil und ist der Rueckfall, falls das Stilblatt
+ einmal nicht laedt. Die Streifen kommen aus einer eigenen Regel,
+ die `tools/kachel-regenbogen.mjs` rechnet -- aus den benutzten
+ Toenen, nicht aus einer Liste hier. */
{ ...TREFF_GRUPPE, name: "Willkommen", unter: "Was es hier gibt, kurz erklaert",
- zeichen: "community", ton: 34, gross: true, ziel: "willkommen.html", szene: "lounge" },
+ zeichen: "community", ton: 34, gross: true, regenbogen: true,
+ ziel: "willkommen.html", szene: "lounge" },
/* DER CHAT (19.09.2026). Filipe: "eine geile mischung von punkt 2
und drei ... nach mitternacht bis morgens 6 uhr keiner schreiben
kann damit auch die privatsphaere beruecksichtig wird ueber nacht
@@ -1261,14 +1273,23 @@ const TREFF_BEREICHE = [
hat nur diese eine. */
{ ...TREFF_GRUPPE, name: "Treff-Chat", unter: "Miteinander reden – nachts schläft er",
zeichen: "chat", ton: 41, ziel: "chat.html?raum=treff", szene: "lounge" },
+ /* HIGHLIGHTS AN DIE SPITZE (22.09.2026).
+
+ Filipe: „da wo anschlagbrett ist soll highlights sein und dan den
+ rest nochmal weiter fuehren."
+
+ Kein Tausch, sondern ein Aufruecken: Highlights nimmt den Platz,
+ alle anderen wandern eine Stelle weiter. Inhaltlich stimmt das
+ auch -- was die Leute selbst gemacht haben, steht vor dem, was
+ ihnen angesagt wird. */
+ { ...TREFF_GRUPPE, name: "Highlights", unter: "Eure Clips und Bilder",
+ zeichen: "live", ton: 29, ziel: "bereich.html?b=highlight", szene: "arena" },
{ ...TREFF_GRUPPE, name: "Anschlagbrett", unter: "Was gerade gilt",
zeichen: "reports", ton: 33, ziel: "bereich.html?b=anschlag", szene: "halle" },
{ ...TREFF_GRUPPE, name: "Was ansteht", unter: "Die n\u00e4chsten Streams und Events",
zeichen: "kalender", ton: 31, ziel: "bereich.html?b=ansteht", szene: "skyline" },
{ ...TREFF_GRUPPE, name: "Wunschliste", unter: "Was ihr euch w\u00fcnscht \u2013 und wer mitwill",
zeichen: "content", ton: 30, ziel: "bereich.html?b=wunsch", szene: "portal" },
- { ...TREFF_GRUPPE, name: "Highlights", unter: "Eure Clips und Bilder",
- zeichen: "live", ton: 29, ziel: "bereich.html?b=highlight", szene: "arena" },
/* HIESS BIS ZUM 21.09.2026 "Unsere Seiten" (Ziel und Datei bleiben
gleich, nur der Name auf der Kachel aendert sich).
diff --git a/tools/chat-kacheln-rechnen.mjs b/tools/chat-kacheln-rechnen.mjs
new file mode 100644
index 00000000..9b58ba87
--- /dev/null
+++ b/tools/chat-kacheln-rechnen.mjs
@@ -0,0 +1,390 @@
+/* =====================================================================
+ DIE ZWÖLF CHAT-KACHELN NEU RECHNEN (22.09.2026)
+ ---------------------------------------------------------------------
+ Filipe: „ich will dass die farben viel klarer und stärker sind und
+ die schrift vom chat soll dan auch angepasst werden ... die kachel
+ von den texten von den leuten im chat soll auch klarer und stärker
+ sein bitte."
+
+ WAS VORHER DA WAR UND WARUM ES SO BLASS WAR
+ Die zwölf Töne lagen bei einer Helligkeit von rund 1 % (#131a25 und
+ Verwandte). Auf dem Bildschirm sind das zwölf Grautöne mit einem
+ Hauch Farbe — genau das, was auf Filipes Bildschirmfoto zu sehen ist.
+
+ Der Grund dafür war richtig gedacht: Die Fußzeile der Blase (Uhrzeit,
+ „antworten", „kopieren") steht auf var(--text-leise) — einem leisen
+ Grau. Gegen eine kräftige Fläche wäre die unlesbar geworden. Die
+ dunkle Kachel war also keine Geschmacksentscheidung, sondern die
+ Folge einer zu leisen Schrift.
+
+ DESHALB WIRD BEIDES ZUSAMMEN GERECHNET, nicht nacheinander. Die
+ Blase bekommt ihre EIGENE Schriftfarbe (--blase-text/--blase-leise,
+ heller als die der Seite), und dieses Werkzeug rechnet aus, wie
+ kräftig die Fläche dann werden DARF.
+
+ Eine Farbe erst stärker zu machen und hinterher zu messen, ob die
+ Schrift noch geht, ist genau die Reihenfolge, mit der am selben Tag
+ schon einmal 27 von 31 Startkacheln unlesbar geworden sind.
+
+ DAS VERFAHREN
+ 1. Die Schriftfarben werden aus dem CSS GELESEN, nicht hier
+ abgeschrieben. Eine zweite Liste wäre die, die beim nächsten
+ Farbwechsel vergessen wird.
+ 2. Aus dem nötigen Kontrast und der dunkelsten dieser Schriften
+ folgt GENAU EINE Leuchtdichte — die hellste Fläche, die beide
+ Schriften noch tragen. Sie wird ausgerechnet, nicht gesucht.
+ 3. Auf dieser einen Leuchtdichte bekommt jeder der elf Farbtöne die
+ höchste Buntheit, die der Bildschirm hergibt.
+
+ WARUM ALLE AUF DERSELBEN LEUCHTDICHTE LIEGEN: Die Kachel gehört
+ einem selbst, GESEHEN wird sie von allen anderen. Gleiche
+ Leuchtdichte heißt, dass der Kontrast bei jedem der zwölf derselbe
+ ist — niemand kann sich durch seine Wahl schlechter lesbar machen
+ als die anderen.
+
+ Nicht dieselbe empfundene Helligkeit, sondern dieselbe Leuchtdichte:
+ Das ist die Größe, an der Lesbarkeit hängt. Der erste Anlauf hat die
+ andere genommen und deshalb behauptet, es gebe gar keine Lösung.
+
+ AUFRUF
+ node tools/chat-kacheln-rechnen.mjs nur zeigen
+ node tools/chat-kacheln-rechnen.mjs --schreiben eintragen
+ ===================================================================== */
+
+import { readFileSync, writeFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { dirname, join } from "node:path";
+
+const HIER = dirname(fileURLToPath(import.meta.url));
+const W = (...t) => join(HIER, "..", ...t);
+
+/* ---- Farbrechnen ---------------------------------------------------- */
+const zuLinear = (v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
+const ausLinear = (v) => (v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055);
+
+function helligkeit([r, g, b]) {
+ return 0.2126 * zuLinear(r / 255) + 0.7152 * zuLinear(g / 255) + 0.0722 * zuLinear(b / 255);
+}
+function kontrast(a, b) {
+ const ha = helligkeit(a), hb = helligkeit(b);
+ const [h, d] = ha > hb ? [ha, hb] : [hb, ha];
+ return (h + 0.05) / (d + 0.05);
+}
+function ausOklch(L, C, hGrad) {
+ const h = (hGrad * Math.PI) / 180;
+ const a = C * Math.cos(h), bb = C * Math.sin(h);
+ const l_ = (L + 0.3963377774 * a + 0.2158037573 * bb) ** 3;
+ const m_ = (L - 0.1055613458 * a - 0.0638541728 * bb) ** 3;
+ const s_ = (L - 0.0894841775 * a - 1.2914855480 * bb) ** 3;
+ const r = 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_;
+ const g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_;
+ const b2 = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.7076147010 * s_;
+ return [r, g, b2];
+}
+/** Lässt der Bildschirm diese Farbe überhaupt zu? */
+function imRaum(L, C, h) {
+ const [r, g, b] = ausOklch(L, C, h);
+ return r >= -1e-4 && g >= -1e-4 && b >= -1e-4
+ && r <= 1.0001 && g <= 1.0001 && b <= 1.0001;
+}
+function zuRgb(L, C, h) {
+ return ausOklch(L, C, h)
+ .map((v) => Math.max(0, Math.min(255, Math.round(ausLinear(v) * 255))));
+}
+const hex = (rgb) => "#" + rgb.map((x) => x.toString(16).padStart(2, "0")).join("");
+function ausHex(t) {
+ const m = t.replace("#", "");
+ const v = m.length === 3
+ ? [...m].map((c) => c + c)
+ : [m.slice(0, 2), m.slice(2, 4), m.slice(4, 6)];
+ return v.map((x) => parseInt(x, 16));
+}
+
+/* ---- Die Schriftfarben AUS DEM CSS holen ---------------------------- */
+/* Drei Dateien, weil die Blase in beiden Häusern vorkommt und das
+ Crew-Haus eigene Farben setzt. Genommen wird je Rolle die DUNKELSTE
+ der gefundenen — die bestimmt den schlechtesten Fall, und nur der
+ zählt. */
+function farbenLesen() {
+ const dateien = ["chat.css", "crew-haus.css", "gate.css"];
+ const gefunden = new Map();
+ for (const d of dateien) {
+ const t = readFileSync(W("workspace", "assets", "css", d), "utf8");
+ for (const m of t.matchAll(/--([a-z0-9-]+):\s*(#[0-9a-fA-F]{3,6})\s*;/g)) {
+ if (!gefunden.has(m[1])) gefunden.set(m[1], []);
+ gefunden.get(m[1]).push(ausHex(m[2]));
+ }
+ }
+ return (name) => {
+ const l = gefunden.get(name);
+ if (!l || !l.length) return null;
+ return l.slice().sort((a, b) => helligkeit(a) - helligkeit(b))[0];
+ };
+}
+const dunkelste = farbenLesen();
+
+/* WELCHE SCHRIFT STEHT AUF EINER BLASE, UND WAS BRAUCHT SIE.
+ ---------------------------------------------------------------------
+ JEDE SCHRIFT HAT IHREN EIGENEN ANSPRUCH — eine gemeinsame Zahl war
+ hier falsch.
+
+ Erster Anlauf verlangte für beide 4,8:1. Das Ergebnis erreichte für
+ den Nachrichtentext 6,86:1 — und riss damit die 7:1, auf die
+ pruef-chatkachel seit dem 20.09. besteht. Die Begründung dort steht
+ und gilt: Ein Chat ist die Seite, auf der am längsten gelesen wird,
+ und AAA ist hier nicht zu viel verlangt.
+
+ Die Fußzeile ist kein Fließtext, sondern vier Handgriffe (Uhrzeit,
+ antworten, kopieren, anheften). Für sie gelten die 4,5:1 der Norm,
+ plus Spielraum auf 4,8 — ein Wert genau auf der Grenze wird beim
+ nächsten Rundungsschritt rot.
+
+ Bindend ist am Ende die STRENGERE der beiden, bezogen auf ihre
+ jeweilige Farbe. Welche das ist, rechnet das Werkzeug aus; es steht
+ hier bewusst nicht als Zahl. */
+const SCHRIFTEN = [
+ ["blase-text", ".chat-nachricht__text", 7.0],
+ ["blase-leise", ".chat-nachricht__fuss", 4.8],
+];
+const pruefFarben = [];
+for (const [name, wo, noetig] of SCHRIFTEN) {
+ const f = dunkelste(name);
+ if (!f) {
+ console.error("\nKONNTE NICHT NACHSEHEN: --" + name + " steht in keiner"
+ + " der drei CSS-Dateien.\n Ohne sie lässt sich nicht ausrechnen, wie"
+ + " kräftig die Fläche sein darf.\n Erst --" + name + " in chat.css"
+ + " anlegen, dann dieses Werkzeug erneut aufrufen.");
+ process.exit(2);
+ }
+ pruefFarben.push({ name, wo, noetig, farbe: f });
+}
+
+console.log("Schriftfarben aus dem CSS gelesen:");
+for (const s of pruefFarben) {
+ console.log(" --" + s.name.padEnd(12) + " " + hex(s.farbe) + " braucht "
+ + s.noetig.toFixed(1) + ":1 " + s.wo);
+}
+
+/* ---- Die eine Leuchtdichte, auf der alle zwölf liegen ---------------
+ ---------------------------------------------------------------------
+ ZWEITER ANLAUF, UND DIESMAL DIE RICHTIGE GRÖSSE.
+
+ Der erste suchte eine gemeinsame OKLab-Helligkeit — also gleiche
+ EMPFUNDENE Helligkeit — und meldete danach, es gebe gar keine Lösung.
+ Das lag nicht an der Schrift, sondern an der Größe, die ich gleich
+ halten wollte: Lesbarkeit hängt nicht an der empfundenen Helligkeit,
+ sondern an der LEUCHTDICHTE. Ein gesättigtes Grün und ein gesättigtes
+ Blau bei derselben OKLab-Helligkeit unterscheiden sich in der
+ Leuchtdichte um ein Vielfaches — und damit im Kontrast zur Schrift.
+ Gelb fiel deshalb bei jeder Helligkeit durch.
+
+ Das Versprechen der zwölf Töne lautet wörtlich „damit bleibt die
+ Schrift überall gleich gut lesbar". Gleiche Leuchtdichte heißt:
+ jeder der zwölf hat EXAKT denselben Kontrast. Das ist das
+ Versprechen, gemessen statt umschrieben.
+
+ UND SIE WIRD NICHT GESUCHT, SONDERN AUSGERECHNET. Aus dem nötigen
+ Kontrast und der dunkelsten Schrift auf der Blase folgt genau ein
+ Wert. Suchen müsste man nur, wenn er sich nicht herleiten ließe.
+ -------------------------------------------------------------------- */
+const ANZAHL_BUNT = 11; /* + 1 neutraler Standard = 12 */
+/* Start bei 258 Grad (Blau-Violett), wo die Hausfarbe ohnehin liegt —
+ der Sprung vom Standard zur ersten bunten Wahl ist dann keiner. */
+const HUES = Array.from({ length: ANZAHL_BUNT },
+ (_, i) => ((i * 360) / ANZAHL_BUNT + 258) % 360);
+
+/* WIE BUNT MUSS ES MINDESTENS WERDEN, DAMIT DER UMBAU ETWAS BRINGT.
+ Gemessen an den zwölf, die bis heute drinstanden: ihre Buntheit liegt
+ zwischen 0,002 und 0,048, im Mittel 0,028 — deshalb sahen sie auf
+ Filipes Bildschirmfoto aus wie zwölf Grautöne. 0,06 liegt über dem
+ buntesten davon. Erreicht ein Ton das nicht, ist die Schrift auf der
+ Blase zu dunkel — dann sagt das Werkzeug das, statt wieder blasse
+ Farben auszuliefern. */
+const MINDEST_BUNTHEIT = 0.06;
+const HOECHST_BUNTHEIT = 0.33;
+
+/* Die hellste Fläche, die JEDE Schrift auf der Blase noch trägt. */
+let zielLeuchte = Infinity;
+for (const sf of pruefFarben) {
+ zielLeuchte = Math.min(zielLeuchte, (helligkeit(sf.farbe) + 0.05) / sf.noetig - 0.05);
+}
+if (!(zielLeuchte > 0)) {
+ console.error("\nKONNTE NICHT NACHSEHEN: Selbst eine schwarze Fläche"
+ + " erreicht den noetigen Kontrast nicht.\n Dann ist die Schrift auf der"
+ + " Blase zu dunkel — erst --blase-leise aufhellen.");
+ process.exit(2);
+}
+
+/** Der buntestmögliche Ton dieses Farbtons auf genau dieser Leuchtdichte. */
+function buntest(hGrad) {
+ let besteC = 0, besteRgb = null;
+ for (let L = 0.20; L <= 0.80; L += 0.002) {
+ for (let c = HOECHST_BUNTHEIT; c >= 0.004; c -= 0.002) {
+ if (!imRaum(L, c, hGrad)) continue;
+ const rgb = zuRgb(L, c, hGrad);
+ /* Gerundete Bildpunkte treffen den Zielwert nie genau — ein
+ Prozent Spielraum. Der Kontrast wird danach am GERUNDETEN
+ Ergebnis nachgeprüft, nicht am gerechneten: ausgeliefert wird
+ das gerundete. */
+ /* EINE OBERGRENZE, KEIN MITTELWERT. Erster Anlauf erlaubte ein
+ Prozent nach BEIDEN Seiten -- und ein Prozent nach oben
+ reisst genau die Schwelle, aus der dieser Wert hergeleitet
+ ist. Nach unten sind drei Prozent erlaubt, damit gerundete
+ Bildpunkte ueberhaupt einen Treffer haben. */
+ const y = helligkeit(rgb);
+ if (y > zielLeuchte || y < zielLeuchte * 0.97) continue;
+ if (!pruefFarben.every((sf) => kontrast(rgb, sf.farbe) >= sf.noetig)) continue;
+ if (c > besteC) { besteC = c; besteRgb = rgb; }
+ break;
+ }
+ }
+ return besteRgb ? { c: besteC, rgb: besteRgb } : null;
+}
+
+const bunte = HUES.map((h) => ({ h, ...(buntest(h) || {}) }));
+const fehlen = bunte.filter((b) => !b.rgb || b.c < MINDEST_BUNTHEIT);
+if (fehlen.length) {
+ console.error("\nKONNTE NICHT NACHSEHEN: " + fehlen.length + " von "
+ + ANZAHL_BUNT + " Farbtönen bleiben unter Buntheit " + MINDEST_BUNTHEIT
+ + ".\n Betroffen: " + fehlen.map((f) => Math.round(f.h) + " Grad").join(", ")
+ + "\n Heißt: die Schrift auf der Blase ist zu dunkel für kräftige"
+ + " Farben.\n Erst --blase-leise in chat.css aufhellen, dann erneut"
+ + " rechnen.");
+ process.exit(2);
+}
+
+/* Der neutrale Standard liegt auf derselben Leuchtdichte — sonst wäre
+ ausgerechnet die Voreinstellung die Ausnahme. */
+/* UND ER WIRD GENAUSO NACHGEPRÜFT WIE DIE ANDEREN ELF.
+ Erster Anlauf prüfte hier nur die Leuchtdichte und verließ sich auf
+ das Prozent Spielraum. Das Ergebnis war #50555e mit 6,97:1 — um drei
+ Hundertstel unter der eigenen Schwelle, und ausgerechnet bei der
+ Voreinstellung, die die meisten sehen. Die Schlusszeile des Werkzeugs
+ hat es gemeldet; gefunden hat es also die Prüfung, nicht der Blick.
+ Ein Spielraum, der in EINE Richtung wirkt, ist kein Spielraum. */
+let grundRgb = null;
+for (let L = 0.20; L <= 0.80; L += 0.001) {
+ const rgb = zuRgb(L, 0.016, 262);
+ const y = helligkeit(rgb);
+ if (y > zielLeuchte || y < zielLeuchte * 0.97) continue;
+ if (!pruefFarben.every((sf) => kontrast(rgb, sf.farbe) >= sf.noetig)) continue;
+ grundRgb = rgb; break;
+}
+if (!grundRgb) {
+ console.error("\nKONNTE NICHT NACHSEHEN: für den neutralen Standard ließ"
+ + " sich die Leuchtdichte nicht treffen.");
+ process.exit(2);
+}
+
+/* ---- Die zwölf ------------------------------------------------------ */
+/* DIE SCHLÜSSEL LINKS SIND KEINE BESCHRIFTUNG — SIE STEHEN IN DER
+ DATENBANK. Wer sie umbenennt, setzt die Wahl der betroffenen Leute
+ still auf „Standard" zurück: `kachelPruefen` kennt den alten Wert
+ dann nicht mehr und gibt null.
+
+ Vor diesem Umbau nachgesehen (nur lesend auf der Live-Datenbank):
+ gewählt sind `veilchen` (2×), `ziegel` (1×) und `moos` (1×), sonst
+ nichts. Alle drei stehen weiter unten und behalten ihren Farbbereich
+ — Veilchen bleibt violett, Ziegel bleibt rot, Moos bleibt grün.
+ Niemand verliert seine Einstellung.
+
+ Die Reihenfolge ist NICHT frei: Sie gehört zu HUES, Eintrag für
+ Eintrag. Wer hier umsortiert, vertauscht die Farben. */
+const TOENE = [
+ ["tinte", "Tinte"], ["veilchen", "Veilchen"], ["pflaume", "Pflaume"],
+ ["kirsche", "Kirsche"], ["ziegel", "Ziegel"], ["bernstein", "Bernstein"],
+ ["oliv", "Oliv"], ["moos", "Moos"], ["tanne", "Tanne"],
+ ["lagune", "Lagune"], ["ozean", "Ozean"],
+];
+if (TOENE.length !== HUES.length) {
+ console.error("\nKONNTE NICHT NACHSEHEN: " + TOENE.length + " Namen für "
+ + HUES.length + " Farbtöne.");
+ process.exit(2);
+}
+/* „Neutral" und nicht mehr „Standard": Seit dem 22.09.2026 ist dieser
+ Ton keine Voreinstellung mehr, sondern eine Wahl -- „ich will keine
+ Farbe". Wer nichts einstellt, bekommt automatisch einen der elf
+ bunten. Ein Eintrag, der „Standard" heisst und nicht der Standard
+ ist, waere die unfreundlichste Art, das zu verschweigen. */
+const liste = [{ wert: "grund", name: "Neutral", farbe: hex(grundRgb) }];
+bunte.forEach((b, i) => {
+ liste.push({ wert: TOENE[i][0], name: TOENE[i][1], farbe: hex(b.rgb) });
+});
+
+console.log("\nGemeinsame Leuchtdichte: " + zielLeuchte.toFixed(4)
+ + " (die zwölf bisherigen lagen bei rund 0,010 — daher das Grau)");
+console.log("Buntheit: " + Math.min(...bunte.map((b) => b.c)).toFixed(3)
+ + " bis " + Math.max(...bunte.map((b) => b.c)).toFixed(3)
+ + " (bisher 0,002 bis 0,048)");
+let schlechtester = Infinity;
+for (const k of liste) {
+ const rgb = ausHex(k.farbe);
+ const ks = pruefFarben.map((s) => kontrast(rgb, s.farbe));
+ schlechtester = Math.min(schlechtester, ...ks);
+ console.log(" " + k.name.padEnd(10) + " " + k.farbe + " "
+ + ks.map((v) => v.toFixed(2) + ":1").join(" "));
+}
+console.log("\nJede Schrift gegen alle zwoelf, schlechtester Fall:");
+for (const sf of pruefFarben) {
+ const m = Math.min(...liste.map((k) => kontrast(ausHex(k.farbe), sf.farbe)));
+ console.log(" --" + sf.name.padEnd(12) + " " + m.toFixed(2) + ":1 (noetig "
+ + sf.noetig.toFixed(1) + ") " + (m >= sf.noetig ? "haelt" : "ZU WENIG"));
+}
+
+/* ---- Eintragen ------------------------------------------------------ */
+if (!process.argv.includes("--schreiben")) {
+ console.log("\nNur gezeigt. Mit --schreiben wird es eingetragen.");
+ process.exit(0);
+}
+const datei = W("server", "workspace-chat.js");
+const quelle = readFileSync(datei, "utf8");
+const anf = quelle.indexOf("export const CHAT_KACHELN = [");
+const ende = quelle.indexOf("\n];", anf);
+if (anf < 0 || ende < 0) {
+ console.error("KONNTE NICHT NACHSEHEN: CHAT_KACHELN steht nicht mehr da,"
+ + " wo dieses Werkzeug sie sucht.");
+ process.exit(2);
+}
+const breite = Math.max(...liste.map((k) => k.wert.length)) + 3;
+const neu = "export const CHAT_KACHELN = [\n"
+ + liste.map((k) => " { wert: " + ('"' + k.wert + '",').padEnd(breite)
+ + " name: " + ('"' + k.name + '",').padEnd(13)
+ + " farbe: \"" + k.farbe + "\" },").join("\n");
+writeFileSync(datei, quelle.slice(0, anf) + neu + quelle.slice(ende), "utf8");
+console.log("\nEingetragen in server/workspace-chat.js");
+
+/* ---- UND DER RUECKFALL IM STILBLATT ---------------------------------
+ Das Stilblatt braucht den Standardton EBENFALLS: Wer nie etwas
+ eingestellt hat, bekommt vom Server keinen Wert, und die Blase faellt
+ auf das zurueck, was in chat.css steht.
+
+ Genau daran ist der erste Anlauf am 22.09.2026 gescheitert. Die
+ zwoelf Toene waren gerechnet und kraeftig -- und auf dem Bildschirm
+ sahen ALLE Blasen aus wie vorher, weil dort noch #131a25 stand, die
+ alte Farbe. Gemeldet hat das kein Werkzeug; gesehen hat es erst das
+ Bildschirmfoto. Eine Rechnung, die nur die Haelfte der Stellen
+ erreicht, ist keine Rechnung, sondern eine zweite Meinung.
+
+ Deshalb schreibt dasselbe Werkzeug beide Stellen. Von Hand gepflegt
+ liefen sie wieder auseinander -- die Frage waere nur, wann. */
+const stil = W("workspace", "assets", "css", "chat.css");
+const ANFANG = "/* ---- ANFANG GERECHNET: chat-kacheln-rechnen.mjs ---- */";
+const SCHLUSS = "/* ---- ENDE GERECHNET ---- */";
+const stilAlt = readFileSync(stil, "utf8");
+const a2 = stilAlt.indexOf(ANFANG);
+const e2 = stilAlt.indexOf(SCHLUSS);
+if (a2 < 0 || e2 < 0) {
+ console.error("\nKONNTE NICHT NACHSEHEN: Die Marken fuer den Rueckfall"
+ + " stehen nicht in chat.css."+QQ.replace(QQ,"")+"\n Erwartet:\n " + ANFANG + "\n "
+ + SCHLUSS + "\n Ohne sie faellt jede Blase ohne eigene Wahl auf einen"
+ + " alten Wert zurueck.");
+ process.exit(2);
+}
+const stilNeu = ANFANG + "\n"
+ + "/* ERZEUGT VON tools/chat-kacheln-rechnen.mjs -- NICHT VON HAND AENDERN.\n"
+ + " Der Ton, den eine Blase traegt, solange niemand etwas eingestellt\n"
+ + " hat. Derselbe Wert wie der Eintrag Standard in CHAT_KACHELN. */\n"
+ + ".chat-nachricht { --kachel-standard: " + liste[0].farbe + "; }\n"
+ + SCHLUSS;
+writeFileSync(stil, stilAlt.slice(0, a2) + stilNeu + stilAlt.slice(e2 + SCHLUSS.length), "utf8");
+console.log("Rueckfall in chat.css gesetzt: " + liste[0].farbe);
diff --git a/tools/kachel-regenbogen.mjs b/tools/kachel-regenbogen.mjs
new file mode 100644
index 00000000..0ce3369b
--- /dev/null
+++ b/tools/kachel-regenbogen.mjs
@@ -0,0 +1,220 @@
+/* =====================================================================
+ DIE WILLKOMMENSKACHEL BEKOMMT ALLE FARBEN DES HAUSES (22.09.2026)
+ ---------------------------------------------------------------------
+ Filipe: „die farbe dieser kachel soll alle farben haben die es auf
+ der ganze seite gibt, also nicht gemischt sondern ich will alle
+ farben übelst geil in der kachel haben die kachel soll alle farben
+ haben die wir benutzt haben. soll komplett farbig sein und ultra geil
+ aussehen lass dir was einfallen und überrasch mich bitte."
+
+ „NICHT GEMISCHT" IST DIE EIGENTLICHE ANSAGE.
+ Ein Verlauf über alle Töne wäre ein Brei: Zwischen zwei Nachbarfarben
+ entstehen Zwischentöne, die es im Haus gar nicht gibt, und die
+ einzelne Farbe ist nicht mehr zu erkennen. Deshalb harte Kanten —
+ jede Farbe bekommt ihren eigenen Streifen und bleibt sie selbst.
+ Man kann die Kachel ablaufen und jede Kachel der Wand darin
+ wiederfinden.
+
+ WARUM DAS EIN WERKZEUG IST UND KEINE HANDARBEIT
+ Die Liste der Farben steht nicht hier, sie wird ABGELEITET — aus
+ denselben Kachellisten, die die Startseite baut (`bereicheFuer` /
+ `zusatzBereicheFuer` über alle Rollen und beide Häuser), und die
+ Farbwerte dazu aus `start.css`. Eine abgeschriebene Liste wäre die,
+ die beim nächsten neuen Bereich vergessen wird — und dann hätte die
+ Kachel „alle Farben" eben doch nicht alle.
+
+ Kommt morgen eine Kachel dazu, genügt ein Lauf:
+ node tools/kachel-regenbogen.mjs nur zeigen
+ node tools/kachel-regenbogen.mjs --schreiben eintragen
+
+ Und `pruef-willkommen` zählt nach, ob der Block noch zu den
+ tatsächlich benutzten Tönen passt — sonst wäre „einmal gelaufen"
+ dasselbe wie „von Hand gepflegt", nur unsichtbar.
+
+ AUGENSCHONEND BLEIBT PFLICHT
+ Die Töne werden nicht roh nebeneinandergelegt. Jeder wird mit dem
+ dunklen Hausgrund gemischt, genau wie auf jeder anderen Kachel auch
+ (dort: `color-mix(--ton 62%, --flaeche)`). Die Kachel ist dadurch
+ voller Farbe und trotzdem so dunkel wie ihre Nachbarn — und der
+ Lesesaum unten, der auf allen Kacheln liegt, hält den Text lesbar.
+ Keine Bewegung, kein Flackern.
+ ===================================================================== */
+
+import { readFileSync, writeFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { dirname, join } from "node:path";
+
+const HIER = dirname(fileURLToPath(import.meta.url));
+const CSS = join(HIER, "..", "workspace", "assets", "css", "start.css");
+const ANFANG = "/* ---- ANFANG GERECHNET: kachel-regenbogen.mjs ---- */";
+const ENDE = "/* ---- ENDE GERECHNET ---- */";
+
+/* ---- Welche Töne werden wirklich benutzt? --------------------------- */
+/* Dasselbe Verfahren wie in kachel-farben-entzerren.mjs. Es steht hier
+ noch einmal und nicht in einer gemeinsamen Datei, weil beide
+ Werkzeuge sonst voneinander abhängen — und das eine soll laufen
+ können, auch wenn am anderen gerade gebaut wird. Was zählt, ist dass
+ BEIDE ableiten statt abzuschreiben. */
+async function benutzteToene() {
+ const w = await import("../server/workspace.js");
+ const rollen = ["admin", "hand", "linke", "modi", "gast",
+ "manager", "scout", "creator", "spicy"];
+ const toene = new Map(); /* ton -> Set(Kachelnamen) */
+ for (const r of rollen) {
+ for (const haus of ["crew", "agentur"]) {
+ const p = { id: 1, rolle: r, name: "X", haus };
+ let k = [];
+ try {
+ k = [...(w.bereicheFuer(p, haus) || []), ...(w.zusatzBereicheFuer(p, haus) || [])];
+ } catch { continue; }
+ for (const kachel of k) {
+ if (!kachel || kachel.ton == null) continue;
+ if (!toene.has(kachel.ton)) toene.set(kachel.ton, new Set());
+ toene.get(kachel.ton).add(kachel.name);
+ }
+ }
+ }
+ return toene;
+}
+
+/* ---- Die Farbwerte dazu --------------------------------------------- */
+function farbenAusCss(css) {
+ const m = new Map();
+ for (const t of css.matchAll(/\[data-ton="(\d+)"\]\s*\{\s*--ton:\s*(#[0-9a-fA-F]{6})/g)) {
+ m.set(Number(t[1]), t[2].toLowerCase());
+ }
+ return m;
+}
+
+/* ---- Farbton für die Sortierung ------------------------------------- */
+/* Sortiert wird nach Farbton, nicht nach Tonnummer. Die Nummern sind
+ die Reihenfolge, in der Bereiche entstanden sind — nebeneinander
+ ergäbe das ein Durcheinander. Nach Farbton sortiert läuft die Kachel
+ einmal durch den Regenbogen, und genau das soll man sehen. */
+function farbwinkel(hex) {
+ const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255)
+ .map((c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
+ const A = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
+ const B = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
+ return (Math.atan2(B, A) * 180) / Math.PI;
+}
+
+/* ===================================================================== */
+const css = readFileSync(CSS, "utf8");
+const farben = farbenAusCss(css);
+const benutzt = await benutzteToene();
+
+const fehlend = [...benutzt.keys()].filter((t) => !farben.has(t));
+if (fehlend.length) {
+ console.error("\nKONNTE NICHT NACHSEHEN: Für " + fehlend.length + " benutzte"
+ + " Töne steht keine Farbe in start.css.\n Betroffen: " + fehlend.join(", ")
+ + "\n Ohne sie wäre die Kachel unvollständig, und niemand sähe es.");
+ process.exit(2);
+}
+if (benutzt.size < 8) {
+ console.error("\nKONNTE NICHT NACHSEHEN: nur " + benutzt.size + " benutzte"
+ + " Töne gefunden.\n Das sind zu wenige, um glaubhaft zu sein — vermutlich"
+ + " hat sich der Aufbau\n der Kachellisten geändert und die Ableitung"
+ + " greift ins Leere.");
+ process.exit(2);
+}
+
+const liste = [...benutzt.keys()]
+ .map((t) => ({ ton: t, farbe: farben.get(t), wer: [...benutzt.get(t)].join(", ") }))
+ .sort((a, b) => farbwinkel(a.farbe) - farbwinkel(b.farbe));
+
+console.log(liste.length + " benutzte Töne, nach Farbwinkel sortiert:\n");
+for (const e of liste) {
+ console.log(" Ton " + String(e.ton).padStart(2) + " " + e.farbe
+ + " " + String(Math.round(farbwinkel(e.farbe))).padStart(4) + " Grad " + e.wer);
+}
+
+/* ---- Den Streifenverlauf bauen -------------------------------------- */
+/* Jede Farbe bekommt genau denselben Anteil. Die Breite ist damit eine
+ Rechnung aus der Anzahl, keine eingetragene Zahl — kommt eine Kachel
+ dazu, rücken alle zusammen, und es bleibt lückenlos. */
+const anteil = 100 / liste.length;
+const streifen = liste.map((e, i) => {
+ const von = (i * anteil).toFixed(3);
+ const bis = ((i + 1) * anteil).toFixed(3);
+ /* GEGEN EINEN DECKENDEN GRUND, NICHT GEGEN --flaeche.
+ Erster Lauf nahm `var(--flaeche)` -- dieselbe Mischung wie auf
+ jeder anderen Kachel. Am Bildschirm sahen die Baender dadurch
+ milchig aus: --flaeche ist halbdurchsichtig (Alpha 0,89), und
+ durch jedes Band schien das Buehnenbild. Auf einer einfarbigen
+ Kachel faellt das nicht auf, bei 31 schmalen Streifen nebeneinander
+ schon -- sie verlieren genau das, wofuer sie da sind.
+
+ Diese eine Kachel deckt deshalb. Das ist auch inhaltlich richtig:
+ Sie soll die Farben des Hauses ZEIGEN, nicht durch sie hindurch
+ etwas anderes. */
+ const f = `color-mix(in srgb, ${e.farbe} 72%, #070b14)`;
+ return ` ${f} ${von}%,\n ${f} ${bis}%`;
+}).join(",\n");
+
+const block = `${ANFANG}
+/* ERZEUGT VON tools/kachel-regenbogen.mjs — NICHT VON HAND ÄNDERN.
+ ${liste.length} Streifen, einer je benutztem Kachelton, sortiert nach
+ Farbwinkel. Die Liste ist aus den Kachellisten abgeleitet; wer eine
+ Kachel hinzufügt, lässt das Werkzeug erneut laufen. Die Prüfung
+ pruef-willkommen zählt nach, ob beides noch zusammenpasst. */
+.kachel[data-regenbogen="ja"] {
+ background-image:
+ /* 1. DER LESESAUM — er liegt ZUERST, also obenauf, und ist der
+ Grund, warum diese Kachel bunt sein darf, ohne dass die
+ Schrift leidet.
+
+ STAERKER ALS AUF DEN ANDEREN KACHELN, und zwar gemessen:
+ Mit den Werten der normalen Kachel (.90/.70/.24) kam der
+ Text hier auf 4,45:1 -- fuenf Hundertstel unter der Grenze.
+ Kein Wunder: Dort liegt EIN dunkler Ton unter der Schrift,
+ hier liegen deckende Farbbaender. Ein heller Untergrund
+ braucht einen dunkleren Saum, sonst ist die Kachel schoen
+ und der Text weg. Gefunden hat das pruef-kachelfarben, nicht
+ der Blick -- 4,45 gegen 4,50 sieht man nicht. */
+ linear-gradient(to top,
+ rgba(4, 6, 14, .95) 0%, rgba(4, 6, 14, .82) 26%,
+ rgba(4, 6, 14, .32) 48%, transparent 66%),
+ /* 2. DIE VIGNETTE — aussen dunkler als innen. Ohne sie wäre die
+ Kachel eine Fläche, mit ihr ein Ausschnitt. */
+ radial-gradient(120% 120% at 50% 45%, transparent 44%, rgba(3, 5, 12, .42) 100%),
+ /* 3. EIN SCHRÄGER GLANZ quer über die Streifen. Er bindet die
+ ${liste.length} Bänder zu einer Fläche zusammen — ohne ihn sieht es
+ aus wie ein Farbmuster, mit ihm wie ein Gegenstand. */
+ linear-gradient(104deg,
+ rgba(255, 255, 255, .10) 0%,
+ rgba(255, 255, 255, .02) 34%,
+ rgba(0, 0, 0, .10) 68%,
+ rgba(0, 0, 0, .26) 100%),
+ /* 4. DIE FARBEN DES HAUSES, harte Kanten, kein Verlauf dazwischen.
+ Jede Farbe bleibt sie selbst — genau das war die Ansage. */
+ linear-gradient(100deg,
+${streifen});
+ background-size: 100% 100%, 100% 100%, 100% 100%, 100% 100%;
+ /* Die Kante links trägt alle Farben mit, in derselben Reihenfolge —
+ sonst wäre ausgerechnet der farbigste Streifen der Kachel einfarbig. */
+ border-color: color-mix(in srgb, var(--ton) 38%, transparent);
+}
+${ENDE}`;
+
+if (!process.argv.includes("--schreiben")) {
+ console.log("\n--- so sähe der Block aus ---\n");
+ console.log(block);
+ console.log("\nNur gezeigt. Mit --schreiben wird es eingetragen.");
+ process.exit(0);
+}
+
+let neu;
+if (css.includes(ANFANG) && css.includes(ENDE)) {
+ const a = css.indexOf(ANFANG);
+ const e = css.indexOf(ENDE) + ENDE.length;
+ neu = css.slice(0, a) + block + css.slice(e);
+} else {
+ neu = css.replace(/\s*$/, "\n\n") + block + "\n";
+}
+writeFileSync(CSS, neu, "utf8");
+console.log("\nEingetragen in workspace/assets/css/start.css ("
+ + liste.length + " Streifen).");
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 8d6f79c8..aac28178 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`. -->
-
-
+
+
-
+