diff --git a/tools/kachel-farben.mjs b/tools/kachel-farben.mjs index 7fbe7f7c..46cd2675 100644 --- a/tools/kachel-farben.mjs +++ b/tools/kachel-farben.mjs @@ -1,173 +1,251 @@ /* =================================================================== - Die 17 Kachelfarben der Startseite -- und wer welche bekommt. + Die 21 Kachelfarben -- gerechnet, nicht gewaehlt. - Filipe wollte, dass JEDE Kachel ihre eigene Farbe hat. Ein erster - Versuch mit frei gewaehlten Farben ergab Paare mit einem Abstand von - 1,4 -- also praktisch dieselbe Farbe. Das sieht nach Zufall aus, nicht - nach Absicht. + ANLASS (08.09.2026). Filipe: "viele kacheln haben noch fast die + gleiche farben, ähneln sich sehr und ich will dass du komplett + eskalierst ... alle seine eigenen farben und so dass sie sich nicht + ähneln. sehr wichtig nicht ähneln!!!!" - Deshalb wird hier gerechnet statt gewaehlt: + Er hat recht, und es liess sich messen. Die alte Palette lief + gleichmaessig um den Farbkreis, mit ABSICHTLICH konstanter Helligkeit + und Farbstaerke -- "dadurch wirken alle gleich stark und keine + draengt sich vor". Der Gedanke war gut und hat genau den Fehler + erzeugt, den Filipe sieht: - 1. 17 Farbtoene, gleichmaessig um den Farbkreis verteilt, in OKLCH - gerechnet (dort sind gleiche Abstaende auch fuer das Auge gleich). - Helligkeit und Farbstaerke bleiben konstant -- dadurch wirken alle - gleich stark und keine draengt sich vor. Wo eine Farbstaerke den - darstellbaren Bereich verliesse (Gelb und Gruen tun das frueher), - wird sie so weit gesenkt, bis sie hineinpasst. + Kleinster Abstand zweier Toene: 0,033 (Dateien gegen Creator) + Paare unter 0,05 (kaum trennbar): 21 von 210 + Spanne der Helligkeit: 0,002 - 2. Die Zuordnung ist eine Suche, kein Geschmack. Verglichen wird vom - Auge nur, was NEBENEINANDER liegt -- 17 Farben muessen sich also - nicht alle voneinander unterscheiden, sondern nur von ihren - Nachbarn im Raster. Gesucht wird die Anordnung, bei der der - kleinste Abstand zweier Nachbarn so gross wie moeglich ist. + Bei 21 Farben auf EINEM Ring mit gleicher Helligkeit bleibt als + einziger Unterschied der Farbton. Nachbarn liegen dann zwangslaeufig + dicht beieinander -- 360 Grad geteilt durch 21 sind 17 Grad, und + 17 Grad Farbton sieht man schlicht nicht. - 3. Unter allen Anordnungen, die eine harte Untergrenze schaffen, wird - die genommen, deren Farben am besten zur Sache passen: LIVE rot, - weil "on air" rot ist. Technik gelb wie ein Warnschild. Reports - gruen wie ein Ergebnis. Personen golden wie ein Schluessel. + WAS SICH AENDERT: Helligkeit und Farbstaerke variieren jetzt mit. + Zwei Farben mit aehnlichem Farbton stehen dadurch trotzdem weit + auseinander, weil die eine hell und satt und die andere dunkel und + ruhig ist. Der Abstand bekommt eine zweite und dritte Dimension. + + GERECHNET WIRD IN OKLAB. Dort entspricht der Zahlenabstand dem, was + das Auge als Unterschied empfindet -- in RGB tut er das nicht: #ff0000 + und #00ff00 sind rechnerisch gleich weit auseinander wie #000080 und + #0000ff, sehen aber voellig anders getrennt aus. + + DREI BEDINGUNGEN, die jede Farbe halten muss: + + 1. LESBAR. Mindestens 4,5:1 gegen den dunklen Grund (#0a121e). + Eine Kachelfarbe traegt auch Text und Zahlen. + 2. AUGENSCHONEND (Hausregel). Farbstaerke gedeckelt -- satt ja, + Neon nein. Deshalb liegt die Obergrenze der Buntheit bei 0,17 + und nicht am Rand des Darstellbaren. + 3. TRENNBAR. Der kleinste Abstand ZWISCHEN ALLEN PAAREN wird so + gross wie moeglich gemacht, nicht nur der zwischen Nachbarn im + Raster. Filipe sieht die Kacheln auch untereinander, nicht nur + nebeneinander -- und auf der Uebersichtsseite stehen sie in + anderer Reihenfolge als auf der Startseite. Aufrufen mit: node tools/kachel-farben.mjs Die Ausgabe wird nach start.css uebertragen (Abschnitt "Kacheln"). =================================================================== */ -const L = 0.655, C_MAX = 0.148; +/* ---------- Farbenlehre ------------------------------------------- */ -function oklchZuRgb(L, C, hGrad) { - const h = hGrad * Math.PI / 180; - const a = C * Math.cos(h), b = C * Math.sin(h); - const l_ = L + 0.3963377774 * a + 0.2158037573 * b; - const m_ = L - 0.1055613458 * a - 0.0638541728 * b; - const s_ = L - 0.0894841775 * a - 1.2914855480 * b; +const zuLinear = (c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)); +const zuSrgb = (c) => (c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055); + +function oklabAusRgb(r, g, b) { + 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 -> sRGB. Gibt null zurueck, wenn die Farbe ausserhalb des + * darstellbaren Bereichs liegt -- dann wird sie nicht heimlich + * beschnitten, sondern verworfen. Ein beschnittener Wert waere eine + * ANDERE Farbe als die gerechnete, und alle Abstaende darauf falsch. */ +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; + const m_ = L - 0.1055613458 * a - 0.0638541728 * bb; + const s_ = L - 0.0894841775 * a - 1.2914855480 * bb; const l = l_ ** 3, m = m_ ** 3, s = s_ ** 3; - const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s; + const r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s; const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s; - const bl = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s; - const gam = (v) => (v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055); - return [gam(r), gam(g), gam(bl)]; + const b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s; + const kanaele = [r, g, b].map(zuSrgb); + if (kanaele.some((c) => c < -0.002 || c > 1.002)) return null; + const bytes = kanaele.map((c) => Math.round(Math.min(1, Math.max(0, c)) * 255)); + return { + hex: "#" + bytes.map((n) => n.toString(16).padStart(2, "0")).join(""), + oklab: oklabAusRgb(...bytes.map((n) => zuLinear(n / 255))), + leucht: 0.2126 * zuLinear(bytes[0] / 255) + 0.7152 * zuLinear(bytes[1] / 255) + + 0.0722 * zuLinear(bytes[2] / 255), + }; } -const imRaum = (rgb) => rgb.every((v) => v >= -0.001 && v <= 1.001); -const hex = (rgb) => "#" + rgb.map((v) => - Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join(""); -/* 01.09.2026: 18 statt 17. Der eigene Steckbrief hat eine eigene - Kachel bekommen (vorher teilte er sich die Seite mit der - Creator-Akte). Damit aendert sich der Abstand zwischen allen Toenen - -- deshalb wird hier neu gerechnet und nicht eine Farbe dazuerfunden. +const abstand = (x, y) => Math.hypot( + x.oklab[0] - y.oklab[0], x.oklab[1] - y.oklab[1], x.oklab[2] - y.oklab[2]); - 06.09.2026: 21. Drei Kacheln sind an einem Tag dazugekommen -- Chat, - Zahlen und Agentur -- und jede hatte sich zunaechst einen Ton mit - einer bestehenden geteilt. Das faellt am Rechner kaum auf und ist - trotzdem falsch: Filipe wollte ausdruecklich, dass JEDE Kachel ihre - eigene Farbe hat, und pruef-start-ansicht hat es gemessen (18 Farben - auf 21 Kacheln). Eine Farbe dazuzuerfinden waere der bequeme Weg und - der falsche -- sie laege dann irgendwo zwischen zweien und saehe nach - Zufall aus. Also wird der ganze Kreis neu geteilt. */ -const ANZAHL = 21; -const winkel = (i) => (i * 360 / ANZAHL + 250) % 360; -function farbe(i) { - let C = C_MAX; - while (C > 0.02 && !imRaum(oklchZuRgb(L, C, winkel(i)))) C -= 0.004; - return hex(oklchZuRgb(L, C, winkel(i))); -} +/* Der Grund, auf dem die Kacheln liegen. */ +const GRUND_LEUCHT = 0.2126 * zuLinear(0x0a / 255) + + 0.7152 * zuLinear(0x12 / 255) + + 0.0722 * zuLinear(0x1e / 255); +const kontrast = (f) => (f.leucht + 0.05) / (GRUND_LEUCHT + 0.05); + +/* ---------- Die Kacheln, in fester Reihenfolge --------------------- */ const KACHELN = [ - "Dashboard", "Aufgaben", "Kalender", "Calls", "Chat", "Dateien", - "Zahlen", "Mein Steckbrief", "Creator-Profile", "Start-Check", "LIVE", "Content", - "Community", "Technik", "Schutz", "Agentur", "Reports", - "Personen", "Scout", "Bibliothek", "Automationen", + "Dashboard", "Zahlen", "LIVE", "Chat", "Schutz", "Community", "Reports", + "Kalender", "Bibliothek", "Steckbrief", "Personen", "Content", "Aufgaben", + "Technik", "Calls", "Start-Check", "Agentur", "Automationen", "Dateien", + "Creator-Profile", "Scout", ]; -const N = (n) => KACHELN.indexOf(n); -/* Nachbarschaft aus dem tatsaechlichen Aufbau der Seite: drei Spalten, - Dashboard ueber zwei. Nebeneinander UND untereinander. */ -const NACHBARN = [ - /* Taeglich: Dashboard ueber zwei Spalten, dann Aufgaben daneben; - darunter Kalender, Calls, Chat; darunter Dateien. */ - ["Dashboard", "Aufgaben"], ["Dashboard", "Kalender"], ["Dashboard", "Calls"], - ["Aufgaben", "Chat"], ["Kalender", "Calls"], ["Calls", "Chat"], - ["Kalender", "Dateien"], - /* Rund um den Creator: elf Kacheln in drei Spalten. */ - ["Zahlen", "Mein Steckbrief"], ["Mein Steckbrief", "Creator-Profile"], - ["Zahlen", "Start-Check"], ["Mein Steckbrief", "LIVE"], ["Creator-Profile", "Content"], - ["Start-Check", "LIVE"], ["LIVE", "Content"], - ["Start-Check", "Community"], ["LIVE", "Technik"], ["Content", "Schutz"], - ["Community", "Technik"], ["Technik", "Schutz"], - ["Community", "Agentur"], ["Technik", "Reports"], ["Agentur", "Reports"], - /* Team & System. */ - ["Personen", "Scout"], ["Scout", "Bibliothek"], ["Personen", "Automationen"], -].map(([a, b]) => [N(a), N(b)]); - -const abstand = (a, b) => { - const d = Math.abs(winkel(a) - winkel(b)) % 360; - return Math.min(d, 360 - d); +/* Wo eine Kachel inhaltlich hingehoert. Das ist die einzige Stelle mit + Geschmack, und sie ist bewusst schwach gewichtet: Der Farbton ist ein + WUNSCH, die Trennbarkeit eine BEDINGUNG. Wo beides nicht zusammengeht, + gewinnt die Trennbarkeit -- eine Kachel, die man nicht auseinanderhaelt, + nuetzt auch in der "richtigen" Farbe nichts. */ +const WUNSCHTON = { + "LIVE": 25, // rot -- "on air" + "Technik": 95, // gelb wie ein Warnschild + "Reports": 150, // gruen wie ein Ergebnis + "Personen": 70, // golden wie ein Schluessel + "Kalender": 320, // pink, hebt den Tagesplan heraus + "Dashboard": 250, // blau, der Ausgangspunkt }; -const WUNSCH = { - LIVE: 17, Content: 38, Technik: 81, Reports: 144, Personen: 59, - Schutz: 250, Community: 335, Dashboard: 250, Kalender: 208, - Bibliothek: 271, "Start-Check": 165, - /* Die drei Neuen (06.09.2026): Chat gruen wie ein gelesener Haken, - Zahlen blau wie eine Messung, Agentur orange -- die Farbe, die - "von aussen" sagt, ohne zu warnen. */ - Chat: 150, Zahlen: 265, Agentur: 40, -}; -function wunschNaehe(z) { - let punkte = 0; - for (const [name, wunsch] of Object.entries(WUNSCH)) { - const d = Math.abs(winkel(z[N(name)]) - wunsch) % 360; - punkte += 180 - Math.min(d, 360 - d); - } - return punkte; -} +/* ---------- Kandidaten ---------------------------------------------- */ -/* Erst die Unterscheidbarkeit sichern, dann unter allem, was das - schafft, die passendste Anordnung waehlen. */ -const MINDEST = 95; -function bewerte(z) { - let schlechtestes = 999; - for (const [a, b] of NACHBARN) schlechtestes = Math.min(schlechtestes, abstand(z[a], z[b])); - if (schlechtestes < MINDEST) return schlechtestes; // noch nicht gut genug - return 1e6 + schlechtestes * 1000 + wunschNaehe(z); -} - -/* Fester Zufall: Dieselbe Eingabe muss dieselbe Ausgabe ergeben, sonst - waeren die Farben nach jedem Lauf andere. */ -let saat = 987654321; -const zufall = () => (saat = (saat * 1103515245 + 12345) % 2147483648) / 2147483648; - -let beste = null, besteNote = -1; -for (let lauf = 0; lauf < 900; lauf++) { - const z = [...Array(ANZAHL).keys()]; - /* ANZAHL - 1, nicht 16. Hier stand eine feste 16 -- bei achtzehn - Kacheln wurden die letzten beiden also nie mitgemischt, und der - Suchraum war stillschweigend kleiner als gedacht. Faellt nicht auf, - weil trotzdem eine Loesung herauskommt; nur eben nicht unbedingt - die beste. */ - for (let i = ANZAHL - 1; i > 0; i--) { const j = Math.floor(zufall() * (i + 1)); [z[i], z[j]] = [z[j], z[i]]; } - let note = bewerte(z), besser = true; - while (besser) { - besser = false; - for (let a = 0; a < ANZAHL; a++) for (let b = a + 1; b < ANZAHL; b++) { - [z[a], z[b]] = [z[b], z[a]]; - const neu = bewerte(z); - if (neu > note) { note = neu; besser = true; } else { [z[a], z[b]] = [z[b], z[a]]; } +const KANDIDATEN = []; +for (let H = 0; H < 360; H += 3) { + for (const L of [0.60, 0.66, 0.72, 0.78, 0.84]) { + /* Die groesste Farbstaerke suchen, die bei diesem Ton und dieser + Helligkeit noch darstellbar ist -- gedeckelt bei 0,17 + (augenschonend, siehe Kopf). Von oben herunter, damit die Farbe + so satt wie erlaubt wird und nicht blass. */ + for (let C = 0.17; C >= 0.05; C -= 0.005) { + const f = ausOklch(L, C, H); + if (!f) continue; + if (kontrast(f) < 4.5) break; // zu dunkel fuer Text + KANDIDATEN.push({ ...f, L, C, H }); + break; } } - if (note > besteNote) { besteNote = note; beste = [...z]; } } -let schlechtestes = 999, paar = ""; -for (const [a, b] of NACHBARN) { - const d = abstand(beste[a], beste[b]); - if (d < schlechtestes) { schlechtestes = d; paar = `${KACHELN[a]} / ${KACHELN[b]}`; } +/* ---------- Auswahl: den kleinsten Abstand so gross wie moeglich ---- + + Erst gierig (nimm immer die Farbe, die am weitesten von allen schon + gewaehlten weg ist), dann verbessern: Jede gewaehlte Farbe wird + probeweise gegen jede andere getauscht; wird der kleinste Abstand + dadurch groesser, bleibt der Tausch. Das laeuft, bis keine + Verbesserung mehr kommt. + + Warum nicht einfach 21 gleichmaessige Punkte: Weil der darstellbare + Bereich keine Kugel ist. Gelb vertraegt viel Helligkeit und wenig + Buntheit, Blau umgekehrt. Ein gleichmaessiges Gitter liegt deshalb + teils ausserhalb, teils verschenkt es Platz. */ + +const anzahl = KACHELN.length; +let gewaehlt = [KANDIDATEN.reduce((a, b) => (a.L * 100 + a.H < b.L * 100 + b.H ? a : b))]; +while (gewaehlt.length < anzahl) { + let beste = null, besterWert = -1; + for (const k of KANDIDATEN) { + if (gewaehlt.includes(k)) continue; + const naechster = Math.min(...gewaehlt.map((g) => abstand(k, g))); + if (naechster > besterWert) { besterWert = naechster; beste = k; } + } + gewaehlt.push(beste); } -console.log(`Kleinster Abstand zweier Nachbarn: ${schlechtestes.toFixed(1)}° (${paar})\n`); -console.log("--- fuer start.css ---"); -KACHELN.forEach((k, i) => { - console.log(`.kachel[data-ton="${beste[i] + 1}"] { --ton: ${farbe(beste[i])}; } /* ${k}, ${Math.round(winkel(beste[i]))}° */`); +const kleinster = (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; +}; + +let verbessert = true, runden = 0; +while (verbessert && runden < 60) { + verbessert = false; runden++; + for (let i = 0; i < gewaehlt.length; i++) { + const jetzt = kleinster(gewaehlt); + for (const k of KANDIDATEN) { + if (gewaehlt.includes(k)) continue; + const probe = gewaehlt.slice(); probe[i] = k; + if (kleinster(probe) > jetzt + 1e-9) { gewaehlt = probe; verbessert = true; break; } + } + } +} + +/* ---------- Zuordnung: Wunschton, wo er ohne Verlust geht ---------- */ + +const frei = gewaehlt.slice(); +const zuteilung = new Map(); +/* Erst die Kacheln mit Wunsch -- jede bekommt die freie Farbe, deren + Ton ihrem Wunsch am naechsten liegt. */ +for (const [name, wunsch] of Object.entries(WUNSCHTON)) { + if (!KACHELN.includes(name)) continue; + let beste = 0, bestAbw = Infinity; + frei.forEach((f, i) => { + const abw = Math.min(Math.abs(f.H - wunsch), 360 - Math.abs(f.H - wunsch)); + if (abw < bestAbw) { bestAbw = abw; beste = i; } + }); + zuteilung.set(name, frei.splice(beste, 1)[0]); +} +/* Der Rest der Reihe nach nach Farbton, damit benachbarte Kacheln im + Raster nicht zufaellig springen -- der Abstand ist ohnehin gesichert. */ +frei.sort((a, b) => a.H - b.H); +for (const name of KACHELN) if (!zuteilung.has(name)) zuteilung.set(name, frei.shift()); + +/* ---------- Nachweis ------------------------------------------------ */ + +const ergebnis = KACHELN.map((n) => ({ name: n, ...zuteilung.get(n) })); +const min = kleinster(ergebnis); +const Ls = ergebnis.map((f) => f.oklab[0]); +let unter5 = 0, paare = 0; +for (let i = 0; i < ergebnis.length; i++) + for (let j = i + 1; j < ergebnis.length; j++) { + paare++; if (abstand(ergebnis[i], ergebnis[j]) < 0.05) unter5++; + } + +console.log("ALT (gemessen): kleinster Abstand 0,0328 · 21 von 210 Paaren unter 0,05" + + " · Helligkeitsspanne 0,002\n"); +console.log(`NEU: kleinster Abstand ${min.toFixed(4)} · ${unter5} von ${paare}` + + ` Paaren unter 0,05 · Helligkeitsspanne ${(Math.max(...Ls) - Math.min(...Ls)).toFixed(3)}\n`); + +const schlechtester = Math.min(...ergebnis.map(kontrast)); +console.log(`Schlechtester Kontrast gegen den Grund: ${schlechtester.toFixed(2)}:1` + + ` (Grenze 4,5) -> ${schlechtester >= 4.5 ? "haelt" : "HAELT NICHT"}\n`); + +console.log("/* Die Nummern bleiben an ihren Kacheln -- eine Farbe gehoert zu"); +console.log(" einem Bereich, nicht zu einem Platz im Raster. */"); +ergebnis.forEach((f, i) => { + const n = `[data-ton="${i + 1}"]`.padEnd(16); + console.log(`${n}{ --ton: ${f.hex}; } /* ${f.name} */`); }); -console.log("\n--- fuer start.js (ton je Kachel) ---"); -console.log(JSON.stringify(Object.fromEntries(KACHELN.map((k, i) => [k, beste[i] + 1])))); -console.log("\n--- alle Farben in der Reihenfolge der Plaetze ---"); -console.log([...Array(ANZAHL).keys()].map((i) => farbe(i)).join(",")); + +/* DIE ZAHLEN GEHOEREN IN DIE BEDINGUNG, nicht nur in den Bericht: + Ein Lauf, der eine schlechtere Palette ausgibt als die alte, waere + sonst nicht von einem guten zu unterscheiden. */ +if (min <= 0.0328) { + console.error(`\nFEHLER: Der kleinste Abstand (${min.toFixed(4)}) ist nicht besser als` + + ` der alte (0,0328). Nichts uebernehmen.`); + process.exit(1); +} +if (schlechtester < 4.5) { + console.error(`\nFEHLER: Eine Farbe liegt unter 4,5:1 gegen den Grund.`); + process.exit(1); +} +if (ergebnis.length !== 21) { + console.error(`\nFEHLER: ${ergebnis.length} Farben statt 21.`); + process.exit(1); +} +console.log(`\n${ergebnis.length} Farben, alle Bedingungen erfuellt.`); diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index add667e4..ad16d413 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -817,27 +817,50 @@ body.start > main { position: relative; z-index: 1; } data-ton: Dieselbe Nummer färbt die Kachel und die Hinweiszeile, die dorthin führt. Eine Farbe gehört hier zu einem Bereich, nicht zu einem Bauteil. */ -[data-ton="1"] { --ton: #3e95e7; } /* Dashboard */ -[data-ton="2"] { --ton: #688beb; } /* Zahlen */ -[data-ton="3"] { --ton: #8881e7; } /* LIVE */ -[data-ton="4"] { --ton: #a178dc; } /* Chat */ -[data-ton="5"] { --ton: #b670ca; } /* Schutz */ -[data-ton="6"] { --ton: #c76ab3; } /* Community */ -[data-ton="7"] { --ton: #d36699; } /* Reports */ -[data-ton="8"] { --ton: #da657c; } /* Kalender */ -[data-ton="9"] { --ton: #dc685d; } /* Bibliothek */ -[data-ton="10"] { --ton: #d96e3c; } /* Mein Steckbrief */ -[data-ton="11"] { --ton: #d0780e; } /* Personen */ -[data-ton="12"] { --ton: #bd8501; } /* Content */ -[data-ton="13"] { --ton: #a98f0d; } /* Aufgaben */ -[data-ton="14"] { --ton: #929909; } /* Technik */ -[data-ton="15"] { --ton: #72a234; } /* Calls */ -[data-ton="16"] { --ton: #47a858; } /* Start-Check */ -[data-ton="17"] { --ton: #11aa7a; } /* Agentur */ -[data-ton="18"] { --ton: #0da795; } /* Automationen */ -[data-ton="19"] { --ton: #16a4a9; } /* Dateien */ -[data-ton="20"] { --ton: #01a2bd; } /* Creator-Profile */ -[data-ton="21"] { --ton: #119dd4; } /* Scout */ +/* NEU GERECHNET AM 08.09.2026 (tools/kachel-farben.mjs). + + 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!!!!" -- und er hatte recht, es liess sich + messen. Die alte Palette lief um EINEN Farbring mit absichtlich + konstanter Helligkeit; damit blieb als einziger Unterschied der + Farbton, und 360 Grad auf 21 Kacheln sind 17 Grad. So wenig sieht man + nicht. + + alt kleinster Abstand 0,033 · 21 von 210 Paaren unter 0,05 + · Helligkeitsspanne 0,002 + neu kleinster Abstand 0,097 · 0 von 210 Paaren unter 0,05 + · Helligkeitsspanne 0,242 + + Jetzt variieren Helligkeit UND Farbstaerke mit. Zwei Farben mit + aehnlichem Ton stehen dadurch trotzdem weit auseinander, weil die eine + hell und satt und die andere dunkel und ruhig ist. + + Gemessen in OKLab (dort entspricht der Zahlenabstand dem empfundenen + Unterschied), und jede Farbe haelt 4,5:1 gegen den dunklen Grund -- + sie traegt auch Text. Wer sie neu rechnet: Das Werkzeug bricht ab, + wenn das Ergebnis schlechter waere als der alte Stand. */ +[data-ton="1"] { --ton: #0284dd; } /* Dashboard */ +[data-ton="2"] { --ton: #e15e8e; } /* Zahlen */ +[data-ton="3"] { --ton: #ff9781; } /* LIVE */ +[data-ton="4"] { --ton: #c26201; } /* Chat */ +[data-ton="5"] { --ton: #febf12; } /* Schutz */ +[data-ton="6"] { --ton: #768b09; } /* Community */ +[data-ton="7"] { --ton: #089868; } /* Reports */ +[data-ton="8"] { --ton: #c06ad0; } /* Kalender */ +[data-ton="9"] { --ton: #afde57; } /* Bibliothek */ +[data-ton="10"] { --ton: #7bba3a; } /* Steckbrief */ +[data-ton="11"] { --ton: #ed8712; } /* Personen */ +[data-ton="12"] { --ton: #01edb5; } /* Content */ +[data-ton="13"] { --ton: #18bfa3; } /* Aufgaben */ +[data-ton="14"] { --ton: #c0a311; } /* Technik */ +[data-ton="15"] { --ton: #5fddff; } /* Calls */ +[data-ton="16"] { --ton: #0da2c6; } /* Start-Check */ +[data-ton="17"] { --ton: #bac8fc; } /* Agentur */ +[data-ton="18"] { --ton: #9299fd; } /* Automationen */ +[data-ton="19"] { --ton: #796ce0; } /* Dateien */ +[data-ton="20"] { --ton: #fc88db; } /* Creator-Profile */ +[data-ton="21"] { --ton: #fdb0d2; } /* Scout */ /* Rückfallwert für alles, was einen Ton braucht, aber keine Nummer hat. Steht bewusst in :where() -- damit zählt die Zeile beim Wettstreit der