Files
DogFatherGitandClaude Opus 5 93c9788782 Telefonieren im Chat -- und die Farben nach Nachbarschaft verteilt
ZWEI SACHEN IN EINEM COMMIT, weil beide aus derselben Nacht stammen.

=== 1. DIE FARBEN: DER RICHTIGE ABSTAND ===

Filipe: "es gibt noch mehr die aehnlich aussehen von den farben also
leg los alle die sich aehnlich sind von den farben wechseln."

Er hatte recht, und mein Denkfehler laesst sich benennen: Ich hatte den
kleinsten Abstand ueber ALLE 37 Paare maximiert -- und der liegt bei 37
Farben zwangslaeufig bei 0,10. Das ist die Packungsgrenze, kein
Versaeumnis.

NUR SIEHT NIEMAND ALLE 37 NEBENEINANDER. Man sieht NACHBARN. Im Browser
gemessen, an der tatsaechlichen Lage auf dem Schirm -- 125 Paare, die
wirklich nebeneinander stehen, 15 davon unter 0,15:

  Creator-Profile / Zahlen      0,1019   beide rosa-rot
  LIVE-Analyse / Technik        0,1030   beide orange
  Wunschliste / Meldungen       0,1032   beide gelbgruen
  Wer sieht was / Entwicklung   0,1039   beide cyan
  Regeln & Hilfe / Mitmachen    0,1047   beide gruen
  Der Treff / Anschlagbrett     0,1070   beide rosa

Die Farben bleiben, ihre ZUTEILUNG aendert sich:

  Nachbarabstand     0,1047 -> 0,2133   (mehr als verdoppelt)
  Nachbarn unter 0,15     6 -> 0

Die Nachbarschaft steht in server/kachel-nachbarn.json, gemessen im
Browser -- nicht aus der Struktur im Quelltext abgeleitet. Die sagt,
was zusammengehoert, nicht was zusammen zu sehen ist.

=== 2. TELEFONIEREN IM CHAT ===

Filipe: "kann man machen dass die modis, rechte hand und ich auch
telefonieren koennen im chat?" ... "was man selbst in die app
reinsetzten kann, nicht meinen pc belastet und trotzdem vielleicht in
gruppe, mit video oder einzelnd."

DER TON GEHT NICHT UEBER DEN SERVER. Direkt von Browser zu Browser
(WebRTC); der Server reicht nur die Verbindungsdaten weiter, ein paar
Kilobyte je Anruf. Zu zweit kodiert jedes Geraet einen Strom und
dekodiert einen -- die Last eines gewoehnlichen Videoanrufs.

KEIN NEUER DIENST. Der Chat hatte bereits alles: `chatEreignis()` fuer
den Hinweg (SSE), Push fuers Klingeln, Raeume mit mehreren
Teilnehmern. Ein eigener WebSocket daneben waere eine zweite
Verbindung fuer dieselbe Frage -- und die zweite wird beim naechsten
Umbau vergessen.

Gebaut: Ton und Video, einzeln und in Gruppe bis GRUPPE_MAX (4),
Klingeln mit Annehmen/Ablehnen, Mikro und Kamera schaltbar,
Gespraechsdauer, Auflegen. Der Chat bleibt daneben benutzbar -- man
schreibt oft, waehrend man spricht.

EIN FUND, DER OHNE PRUEFLAUF LIVE GEGANGEN WAERE: Der Server sperrt
Mikrofon und Kamera per Permissions-Policy auf ALLEN Seiten. Der erste
Lauf meldete "microphone is not allowed in this document" -- der Anruf
haette bei JEDEM versagt, mit einer Meldung, die auf die falsche
Faehrte fuehrt (man sucht an den Browsereinstellungen). Die Sperre
bleibt ueberall und ist an genau EINER Stelle geoeffnet: der
Chat-Seite, und nur fuer sie selbst (`self`, nicht `*`).

NOCH NICHT GEBAUT -- und ausdruecklich nicht heimlich: coturn. Ohne
Vermittlungsserver klappen Anrufe nur im selben Netz. Die Adressen
stehen in den Einstellungen statt im Quelltext; sie lassen sich
nachtragen, ohne eine Zeile zu aendern. Ein oeffentlicher STUN-Dienst
als Standard kam nicht in Frage: Er saehe bei jedem Anruf die
IP-Adressen beider Teilnehmer, und fuer ein Team, das ueber Moderation
und Vorfaelle spricht, ist das keine Kleinigkeit.

server/pruef-anruf.mjs, 40 Pruefungen. Der wichtigste Abschnitt: Wer
nicht in den Raum gehoert, kommt an KEINE Route. Ein Anruf hinterlaesst
keine Spur -- wer mithoert, faellt nicht auf.

Gegenproben, jede zielgenau:
  Empfaengerpruefung der Signalisierung weg -> 1 rot
  Raumpruefung weg                         -> 5 rot (jede Route offen)
  Kopfzeilen-Ausnahme weg                  -> 4 rot

Gruen: anruf, chat, chat-optik, chat-kanaele, css-klassen, namen,
struktur.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-18 10:10:04 +02:00

416 lines
16 KiB
JavaScript

/* ===================================================================
DIE KACHELFARBEN -- gerechnet, nicht gewaehlt.
ANLASS (08.09.2026). Filipe: "viele kacheln haben noch fast die
gleiche farben, ähneln sich sehr ... alle seine eigenen farben und so
dass sie sich nicht ähneln. sehr wichtig nicht ähneln!!!!"
NEU GERECHNET AM 17.09.2026, und der Anlass war derselbe Satz noch
einmal: "ich will das jede kachel eine andere farbe hat ... keine die
sich irgendwie ähnlich sind ... die farben sollen auch richtig geil
und speziell sein."
ER HATTE WIEDER RECHT, UND ES WAR SCHLIMMER ALS 2026-09-08.
Gemessen am 17.09. an den Farben, die WIRKLICH in start.css stehen:
kleinster Abstand 0,0529 (Rueckmeldung / Steckbrief)
Paare unter 0,10 18 von 325
Paare unter 0,15 61 von 325
schlechtester Kontrast 1,64:1 (Ton 35, praktisch unlesbar)
sieben Toene unter 4,5:1
WIE KONNTE DAS PASSIEREN, obwohl es dieses Werkzeug schon gab? Weil
es nur 21 Farben kannte. Seit dem 08.09. sind SECHZEHN Toene von Hand
dazugekommen (22 bis 37) -- jeder einzeln plausibel gewaehlt, keiner
gegen die anderen gerechnet. Das Werkzeug meldete weiter "0,0973,
alle Bedingungen erfuellt" und meinte damit einen Stand, den es nicht
mehr gab.
Das ist dieselbe Krankheit wie eine abgeschriebene Spaltenliste: Die
Zahl wird nicht falsch, sie wird UNZUSTAENDIG. Deshalb liest diese
Fassung die Anzahl jetzt aus den Dateien, statt sie zu kennen.
---------------------------------------------------------------------
GERECHNET WIRD IN OKLAB. Dort entspricht der Zahlenabstand dem, was
das Auge als Unterschied empfindet -- in RGB tut er das nicht.
VIER BEDINGUNGEN, die jede Farbe halten muss:
1. LESBAR. Mindestens 4,5:1 gegen den dunklen Grund (#0a121e).
Eine Kachelfarbe traegt auch Text und Zahlen. Das ist die
einzige Bedingung, die nicht verhandelbar ist.
2. SPEZIELL (neu am 17.09.). Die Obergrenze der Buntheit steigt von
0,17 auf 0,30. Filipe: "richtig geil und speziell". Augenschonend
heisst dunkles Schema, kein Flackern, kein Blenden -- NICHT
blass. Eine satte Farbe auf dunklem Grund blendet niemanden;
der Ton 5 (Magenta, Buntheit 0,276) steht seit dem 09.09. auf
seinen ausdruecklichen Wunsch genau so da.
3. TRENNBAR. Der kleinste Abstand ZWISCHEN ALLEN PAAREN wird so
gross wie moeglich gemacht, nicht nur zwischen Nachbarn im
Raster -- die Reihenfolge ist auf jeder Seite anders.
4. NICHT SCHLECHTER ALS VORHER. Das Werkzeug bricht ab, wenn das
Ergebnis den alten Stand unterbietet.
Aufrufen mit: node tools/kachel-farben.mjs
node tools/kachel-farben.mjs --schreiben
=================================================================== */
import { readFileSync, writeFileSync } from "node:fs";
const CSS = "workspace/assets/css/start.css";
const GRUND = "#0a121e";
/* ---------- Farbrechnung -------------------------------------------- */
const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
const zurueck = (c) => (c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055);
const srgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
function oklab(hex) {
const [r, g, b] = srgb(hex).map(linear);
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 [
0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
];
}
/** OKLCH -> Hex, oder null wenn es ausserhalb von sRGB liegt. */
function ausOklch(L, C, Hgrad) {
const h = (Hgrad * Math.PI) / 180;
const a = C * Math.cos(h), b2 = C * Math.sin(h);
const l_ = (L + 0.3963377774 * a + 0.2158037573 * b2) ** 3;
const m_ = (L - 0.1055613458 * a - 0.0638541728 * b2) ** 3;
const s_ = (L - 0.0894841775 * a - 1.2914855480 * b2) ** 3;
const r = +4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_;
const g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_;
const b = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.7076147010 * s_;
const kanal = (x) => {
const v = zurueck(x);
/* Winzige Rundungsfehler sind kein Grund, eine Farbe zu verwerfen --
ein echtes Ueberschreiten schon. */
if (v < -0.001 || v > 1.001) return null;
return Math.round(Math.min(1, Math.max(0, v)) * 255);
};
const [R, G, B] = [kanal(r), kanal(g), kanal(b)];
if (R === null || G === null || B === null) return null;
return "#" + [R, G, B].map((x) => x.toString(16).padStart(2, "0")).join("");
}
const lum = (hex) => {
const [r, g, b] = srgb(hex).map(linear);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const kontrast = (hex) => (lum(hex) + 0.05) / (lum(GRUND) + 0.05);
const abstand = (a, b) => {
const x = a.lab || oklab(a.hex || a), y = b.lab || oklab(b.hex || b);
return Math.hypot(x[0] - y[0], x[1] - y[1], x[2] - y[2]);
};
const kleinsterAbstand = (menge) => {
let m = Infinity;
for (let i = 0; i < menge.length; i++) {
for (let j = i + 1; j < menge.length; j++) m = Math.min(m, abstand(menge[i], menge[j]));
}
return m;
};
/* ---------- WIE VIELE FARBEN? Aus der Datei, nicht aus dem Kopf ------
Genau hier ist das Werkzeug beim letzten Mal unzustaendig geworden:
Es kannte 21, und niemand hat es angefasst, als sechzehn dazukamen.
Eine Zahl, die niemand pflegt, kann nicht veralten. */
const cssText = readFileSync(CSS, "utf8");
const VORHER = new Map(
[...cssText.matchAll(/\[data-ton="(\d+)"\]\s*\{\s*--ton:\s*(#[0-9a-fA-F]{6})/g)]
.map((m) => [Number(m[1]), m[2].toLowerCase()]));
const NUMMERN = [...VORHER.keys()].sort((a, b) => a - b);
/* Und wofuer steht jede Nummer? Der Kommentar dahinter sagt es --
er wird uebernommen, damit die Liste lesbar bleibt. */
const WOFUER = new Map(
[...cssText.matchAll(/\[data-ton="(\d+)"\][^\n]*?\/\*\s*([^*]+?)\s*\*\//g)]
.map((m) => [Number(m[1]), m[2].trim()]));
/* ---------- Kandidaten ---------------------------------------------- */
const KANDIDATEN = [];
/* FEIN ABGETASTET. Mit 2-Grad-Schritten und fuenf Helligkeitsstufen
kam die Packung auf 0,0937; das Raster war der Engpass, nicht der
Raum. Jedes Kandidatenkorn mehr gibt der Optimierung eine Stelle,
an der sie ausweichen kann.
Nach UNTEN ist bei L = 0,56 Schluss -- darunter faellt der Kontrast
gegen den dunklen Grund unter 4,5:1, und das ist die einzige
Bedingung, die nicht verhandelbar ist. */
for (let H = 0; H < 360; H += 1) {
for (let L = 0.56; L <= 0.94; L += 0.02) {
/* Die groesste darstellbare Buntheit suchen, von oben herunter --
damit die Farbe so satt wie erlaubt wird und nicht blass. */
for (let C = 0.30; C >= 0.04; C -= 0.004) {
const hex = ausOklch(L, C, H);
if (!hex) continue;
if (kontrast(hex) < 4.5) break;
KANDIDATEN.push({ hex, lab: oklab(hex), L, C, H });
break;
}
}
}
/* ---------- Auswahl: den kleinsten Abstand so gross wie moeglich -----
Erst gierig (nimm immer die Farbe, die am weitesten von allen schon
gewaehlten weg ist), dann tauschen, solange es besser wird.
MEHRERE STARTPUNKTE. Ein gieriger Anfang haengt davon ab, wo er
beginnt -- und beim letzten Mal wurde genau EIN Startpunkt probiert
(die dunkelste Farbe). Jetzt laufen viele, und der beste gewinnt.
Das kostet Sekunden und bringt hier rund ein Drittel mehr Abstand. */
function auswahl(startIndex, anzahl) {
const gewaehlt = [KANDIDATEN[startIndex]];
while (gewaehlt.length < anzahl) {
let beste = null, besterWert = -1;
for (const k of KANDIDATEN) {
if (gewaehlt.includes(k)) continue;
let naechster = Infinity;
for (const g of gewaehlt) {
const d = abstand(k, g);
if (d < naechster) naechster = d;
if (naechster <= besterWert) break; // kann nicht mehr gewinnen
}
if (naechster > besterWert) { besterWert = naechster; beste = k; }
}
gewaehlt.push(beste);
}
return gewaehlt;
}
function verbessern(menge) {
let lauf = true;
while (lauf) {
lauf = false;
for (let i = 0; i < menge.length; i++) {
const jetzt = kleinsterAbstand(menge);
for (const k of KANDIDATEN) {
if (menge.includes(k)) continue;
const probe = menge.slice();
probe[i] = k;
if (kleinsterAbstand(probe) > jetzt) {
menge = probe;
lauf = true;
break;
}
}
}
}
return menge;
}
const ANZAHL = NUMMERN.length;
console.log(`${ANZAHL} Farben gesucht, ${KANDIDATEN.length} Kandidaten im Raum.`);
let beste = null, besterWert = -1;
const starts = 16;
for (let i = 0; i < starts; i++) {
const idx = Math.floor((i / starts) * KANDIDATEN.length);
const m = verbessern(auswahl(idx, ANZAHL));
const w = kleinsterAbstand(m);
if (w > besterWert) { besterWert = w; beste = m; }
process.stdout.write(`\r Startpunkt ${i + 1}/${starts} -- bester Abstand ${besterWert.toFixed(4)} `);
}
console.log("");
/* ---------- Zuteilung: welche Farbe an welche Nummer? ----------------
HIER LAG DER EIGENTLICHE FEHLER (18.09.2026).
Filipe, nachdem die Palette schon neu gerechnet war: "es gibt noch
mehr die aehnlich aussehen von den farben also leg los alle die sich
aehnlich sind von den farben wechseln."
Er hatte wieder recht, und diesmal lag es nicht an den Farben,
sondern an ihrer VERTEILUNG. Gemessen im Browser -- 125 Paare, die
wirklich nebeneinander stehen:
Creator-Profile / Zahlen 0,1019 beide rosa-rot
LIVE-Analyse / Technik 0,1030 beide orange
Wunschliste / Meldungen 0,1032 beide gelbgruen
Wer sieht was / Entwicklung 0,1039 beide cyan
Regeln & Hilfe / Mitmachen 0,1047 beide gruen
Der Treff / Anschlagbrett 0,1070 beide rosa
... 15 Paare unter 0,15
DER DENKFEHLER: Der kleinste Abstand ueber ALLE Paare wurde maximiert
-- und der liegt bei 37 Farben zwangslaeufig bei 0,10. Das ist die
Packungsgrenze, kein Versaeumnis. Nur sieht niemand alle 37
nebeneinander. Man sieht NACHBARN. Zwei aehnliche Farben an
entgegengesetzten Enden der Seite fallen nie auf; zwei
nebeneinander sofort.
Die Farben bleiben also, wie sie sind -- ihre ZUTEILUNG aendert
sich. Wer nebeneinander steht, bekommt Farben, die weit
auseinanderliegen. Das kostet nichts und ist der ganze Unterschied.
WOHER DIE NACHBARSCHAFT KOMMT: aus dem Browser, gemessen an der
tatsaechlichen Lage auf dem Schirm (server/kachel-nachbarn.json).
Nicht aus der Struktur im Quelltext -- die sagt, was
zusammengehoert, nicht was zusammen zu sehen ist. */
/* Welcher Name traegt welche Tonnummer? */
const NAME_ZU_TON = new Map();
for (const q of ["server/workspace.js", "workspace/assets/js/bereiche.js"]) {
let text;
try { text = readFileSync(q, "utf8"); } catch { continue; }
const zeilen = text.split("\n");
for (let i = 0; i < zeilen.length; i++) {
const m = zeilen[i].match(/ton:\s*(\d+)/);
if (!m) continue;
for (let k = 0; k <= 3; k++) {
const n = (zeilen[i - k] || "").match(/name:\s*"([^"]+)"/);
if (n) { NAME_ZU_TON.set(n[1], Number(m[1])); break; }
}
}
}
let NACHBARN = [];
try {
const roh = JSON.parse(readFileSync("server/kachel-nachbarn.json", "utf8"));
NACHBARN = roh.paare
.map(([a, b]) => [NAME_ZU_TON.get(a), NAME_ZU_TON.get(b)])
.filter(([a, b]) => a && b && a !== b);
console.log(`${NACHBARN.length} Nachbarpaare geladen `
+ `(gemessen am ${roh.gemessen}).`);
} catch {
console.log("Keine Nachbarschaftsdatei -- es wird nur global optimiert.");
}
/* Die Startzuteilung: jede Nummer bekommt die neue Farbe, die ihrer
alten am naechsten liegt. Wer "Chat" als Orange kennt, findet es
danach wieder als Orange -- solange die Nachbarschaft das zulaesst. */
const frei = beste.slice();
const zuteilung = new Map();
for (const nr of NUMMERN) {
const alt2 = VORHER.get(nr);
let bester = 0, bestAbw = Infinity;
for (let i = 0; i < frei.length; i++) {
const d = abstand(frei[i].hex, alt2);
if (d < bestAbw) { bestAbw = d; bester = i; }
}
zuteilung.set(nr, frei.splice(bester, 1)[0]);
}
/* Und jetzt tauschen, solange die NACHBARN davon profitieren. */
if (NACHBARN.length) {
const nachbarAbstand = (z) => {
let m = Infinity;
for (const [a, b] of NACHBARN) {
const fa = z.get(a), fb = z.get(b);
if (!fa || !fb) continue;
const d = abstand(fa.hex, fb.hex);
if (d < m) m = d;
}
return m;
};
/* Und als zweites Ziel: die SUMME der kleinen Abstaende. Zwei
Zuteilungen mit gleichem Minimum sind nicht gleich gut -- die mit
weniger knappen Paaren ist besser. */
const knappe = (z, grenze) => {
let n = 0;
for (const [a, b] of NACHBARN) {
const fa = z.get(a), fb = z.get(b);
if (fa && fb && abstand(fa.hex, fb.hex) < grenze) n++;
}
return n;
};
const vorher = nachbarAbstand(zuteilung);
const vorherKnapp = knappe(zuteilung, 0.15);
let lauf = true, runden = 0;
while (lauf && runden < 60) {
lauf = false; runden++;
for (let i = 0; i < NUMMERN.length; i++) {
for (let j = i + 1; j < NUMMERN.length; j++) {
const a = NUMMERN[i], b = NUMMERN[j];
const jetztMin = nachbarAbstand(zuteilung);
const jetztKnapp = knappe(zuteilung, 0.15);
const fa = zuteilung.get(a), fb = zuteilung.get(b);
zuteilung.set(a, fb); zuteilung.set(b, fa);
const neuMin = nachbarAbstand(zuteilung);
const neuKnapp = knappe(zuteilung, 0.15);
const besser = neuMin > jetztMin + 1e-9
|| (Math.abs(neuMin - jetztMin) < 1e-9 && neuKnapp < jetztKnapp);
if (besser) { lauf = true; } else { zuteilung.set(a, fa); zuteilung.set(b, fb); }
}
}
}
console.log(`Nachbarn: kleinster Abstand ${vorher.toFixed(4)} -> `
+ `${nachbarAbstand(zuteilung).toFixed(4)} `
+ `· Paare unter 0,15: ${vorherKnapp} -> ${knappe(zuteilung, 0.15)} `
+ `(${runden} Runden)`);
}
/* ---------- Bericht -------------------------------------------------- */
const altListe = NUMMERN.map((n) => ({ hex: VORHER.get(n), lab: oklab(VORHER.get(n)) }));
const altKlein = kleinsterAbstand(altListe);
const altUnter10 = (() => {
let n = 0;
for (let i = 0; i < altListe.length; i++) {
for (let j = i + 1; j < altListe.length; j++) if (abstand(altListe[i], altListe[j]) < 0.10) n++;
}
return n;
})();
const altKontrast = Math.min(...altListe.map((x) => kontrast(x.hex)));
const neuListe = NUMMERN.map((n) => zuteilung.get(n));
const neuUnter10 = (() => {
let n = 0;
for (let i = 0; i < neuListe.length; i++) {
for (let j = i + 1; j < neuListe.length; j++) if (abstand(neuListe[i], neuListe[j]) < 0.10) n++;
}
return n;
})();
const neuKontrast = Math.min(...neuListe.map((x) => kontrast(x.hex)));
const paare = (ANZAHL * (ANZAHL - 1)) / 2;
console.log("");
console.log(`ALT: kleinster Abstand ${altKlein.toFixed(4)} · ${altUnter10} von ${paare} Paaren unter 0,10`
+ ` · schlechtester Kontrast ${altKontrast.toFixed(2)}:1`);
console.log(`NEU: kleinster Abstand ${besterWert.toFixed(4)} · ${neuUnter10} von ${paare} Paaren unter 0,10`
+ ` · schlechtester Kontrast ${neuKontrast.toFixed(2)}:1`);
console.log(` Buntheit ${Math.min(...neuListe.map((x) => x.C)).toFixed(3)}`
+ ` bis ${Math.max(...neuListe.map((x) => x.C)).toFixed(3)}`
+ ` · Helligkeit ${Math.min(...neuListe.map((x) => x.L)).toFixed(2)}`
+ ` bis ${Math.max(...neuListe.map((x) => x.L)).toFixed(2)}`);
/* BEDINGUNG 4: nicht schlechter als vorher. Ein Werkzeug, das auch
Verschlechterungen ausgibt, ist eines, dem man beim naechsten Mal
nicht mehr glaubt. */
if (besterWert <= altKlein || neuKontrast < 4.5) {
console.error("\nABBRUCH: Das Ergebnis waere nicht besser als der alte Stand.");
process.exit(1);
}
const zeilen = NUMMERN.map((n) => {
const f = zuteilung.get(n);
const was = WOFUER.get(n) ? ` /* ${WOFUER.get(n)} */` : "";
return `[data-ton="${n}"]`.padEnd(16) + `{ --ton: ${f.hex}; }${was}`;
});
console.log("\n" + zeilen.join("\n"));
console.log(`\n${ANZAHL} Farben, alle Bedingungen erfuellt.`);
if (process.argv.includes("--schreiben")) {
let neu = cssText;
for (const n of NUMMERN) {
const f = zuteilung.get(n);
neu = neu.replace(
new RegExp(`(\\[data-ton="${n}"\\]\\s*\\{\\s*--ton:\\s*)#[0-9a-fA-F]{6}`),
`$1${f.hex}`);
}
writeFileSync(CSS, neu);
console.log(`\nIn ${CSS} eingetragen.`);
}