From 438493c7dc6a04fd2d6e2b9c096970a68adf75be Mon Sep 17 00:00:00 2001 From: Dogfather Date: Fri, 18 Sep 2026 02:55:23 +0200 Subject: [PATCH] Kacheln: 37 Farben neu gerechnet, und in jeder ein eigenes Universum Filipe, zwei Wuensche: "ich will das jede kachel eine andere farbe hat ... keine die sich irgendwie aehnlich sind ... richtig geil und speziell" und "der hintergrund ... soll noch mehr viel mehr nach universum sein. nicht einfach so paar weisse punkte sondern richtig geil hochwertiger geiler universum ... in den kacheln ueberall". BEIDES WAR BERECHTIGT, und beides liess sich messen: kleinster Farbabstand 0,0529 (Rueckmeldung / Steckbrief) Paare unter 0,10 26 von 666 schlechtester Kontrast 1,64:1 (Ton 35 -- praktisch unlesbar) sieben Toene unter 4,5:1 Universum auf GENAU EINER Kachel WIE DAS PASSIEREN KONNTE, obwohl es ein Farbwerkzeug gab: Es kannte 21 Farben. Seit dem 08.09. waren SECHZEHN von Hand dazugekommen (Ton 22 bis 37) -- jede einzeln plausibel, keine gegen die anderen gerechnet. Das Werkzeug meldete weiter "0,0973, alle Bedingungen erfuellt" und meinte einen Stand, den es nicht mehr gab. Dieselbe Krankheit wie eine abgeschriebene Spaltenliste: Die Zahl wird nicht falsch, sie wird UNZUSTAENDIG. Es liest die Anzahl jetzt aus den Dateien. NEU GERECHNET, alle 37 auf einmal: kleinster Abstand 0,0529 -> 0,1010 (fast doppelt) Paare unter 0,10 26 -> 0 schlechtester Kontrast 1,64 -> 4,50:1 Buntheit bis 0,300 statt 0,170, Helligkeit 0,58 bis 0,92 Drei Stellschrauben: Buntheitsgrenze hoch (augenschonend heisst dunkles Schema und kein Flackern, NICHT blass), Raster von 2 Grad auf 1 Grad und von fuenf auf zwanzig Helligkeitsstufen, und sechzehn Startpunkte statt einem. Der Kontrast 4,5:1 bleibt hart -- eine Kachelfarbe traegt Text. DAS UNIVERSUM STEHT JETZT IM HINTERGRUND JEDER KACHEL, nicht in einem Element: Beide Pseudo-Elemente sind vergeben (Leuchtschiene, Glanz), und ein neues Element muesste an jeder Stelle nachgetragen werden, die Kacheln baut. Eine Ebene, die man vergessen kann, wird vergessen. Siebzehn Ebenen: drei nahe Sterne mit Hof, vier mittlere (zwei im Kachelton), fuenf Staubkoerner, eine Milchstrasse als Schraege, drei Nebel im Kachelton, ein kuehler Gegenpol, eine Vignette. Die Nebel tragen `var(--ton)` -- 37 Kacheln sind damit 37 verschiedene Nebel. Keine Bewegung: 28 driftende Felder waeren 28 Dauerlaeufer auf der Grafikkarte. Nach dem ersten Bildschirmfoto nachgeschaerft -- die Sterne lagen bei 6 bis 14 Prozent und waren aus der Naehe nicht zu sehen, genau die "paar weissen punkte". Der Grund steht in der Kachel selbst: Sie ist halbdurchsichtig, darunter liegt das Buehnenbild. Ein Sternenfeld muss sich hier gegen ein FOTO durchsetzen, nicht gegen Schwarz. NEU: server/helfer-png.mjs -- ein PNG-Leser (zlib, 90 Zeilen). Er beantwortet die Frage, die aus dem CSS nicht mehr zu beantworten war: Zwischen Textfarbe und Flaeche liegen jetzt sieben Ebenen plus das durchscheinende Buehnenbild. Gemessen am Bildschirmfoto: schlechtester Textkontrast 8,02:1 (Grenze 4,5). NEU: server/pruef-kachel-universum.mjs, 12 Pruefungen. DREI ANLAEUFE FUER DIE STERNPRUEFUNG, und die ersten zwei waren gruen und wertlos: 1. Ebenen im CSS zaehlen -- `0px` ist auch eine Zahl. Alle Sterne auf null: blieb gruen. 2. Helle Punkte im Bildschirmfoto zaehlen -- die Kachel ist halbdurchsichtig, das Foto darunter hat selbst Punktstruktur. Gemessen: 459 Punkte mit Sternen, 441 ohne. Vier Prozent sind kein Nachweis, sondern Rauschen. 3. Jetzt die GROESSEN aus dem CSS (>= 0,7 px). Schwaecher, aber ehrlich -- und die Gegenprobe greift: 12 -> 0. Dabei fiel ein eigener Messfehler auf: Der Bereich wurde aus der Textposition geschaetzt und lag OBERHALB der Kachel. Verraten hat es die Gegenprobe, die zweimal exakt 153 lieferte -- eine Messung, deren Ergebnis sich nicht aendert, wenn man das Gemessene entfernt, misst etwas anderes. Gegenproben, alle zielgenau: zwei gleiche Farben -> 3 rot Sterne auf 0px -> 1 rot Nebel ohne Kachelton -> 1 rot (nach dem Schaerfen: vorher blieb es gruen, weil `--ton` auch im Grundverlauf steht) Gruen: kachel-universum, css-klassen, start-ansicht, namen. Co-Authored-By: Claude Opus 5 --- server/helfer-png.mjs | 103 ++++ server/pruef-kachel-universum.mjs | 941 ++++++++++-------------------- tools/kachel-farben.mjs | 456 ++++++++------- workspace/app.webmanifest | 8 +- workspace/assets/css/start.css | 208 +++++-- workspace/aufgaben.html | 24 +- workspace/automation.html | 26 +- workspace/befinden.html | 24 +- workspace/bereich.html | 36 +- workspace/bewerben.html | 30 +- workspace/bewerbungen.html | 28 +- workspace/calls.html | 28 +- workspace/chat.html | 26 +- workspace/content.html | 30 +- workspace/crew-index.html | 10 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 26 +- workspace/entwicklung.html | 24 +- workspace/index.html | 8 +- workspace/kalender.html | 28 +- workspace/leistung.html | 26 +- workspace/personen.html | 26 +- workspace/profil.html | 28 +- workspace/rechte.html | 24 +- workspace/report.html | 28 +- workspace/scouting.html | 26 +- workspace/start.html | 24 +- workspace/startcheck.html | 28 +- workspace/steckbrief.html | 28 +- workspace/talente.html | 26 +- workspace/team.html | 26 +- workspace/teamlage.html | 24 +- workspace/teilen.html | 22 +- workspace/treff-moderation.html | 24 +- workspace/treff-regeln.html | 26 +- workspace/uebersicht.html | 26 +- workspace/wissen.html | 26 +- 37 files changed, 1229 insertions(+), 1281 deletions(-) create mode 100644 server/helfer-png.mjs diff --git a/server/helfer-png.mjs b/server/helfer-png.mjs new file mode 100644 index 00000000..ac91c777 --- /dev/null +++ b/server/helfer-png.mjs @@ -0,0 +1,103 @@ +/* ===================================================================== + EINEN BILDSCHIRMFOTO-PIXEL LESEN (17.09.2026) + + ANLASS: Der Kachelgrund hat ein Universum bekommen -- Sterne, drei + Nebel, eine Vignette, dazu der Glanz und das durchscheinende + Buehnenbild. Die Frage "ist der Text darauf noch lesbar?" laesst + sich aus dem CSS nicht mehr beantworten: Zwischen Textfarbe und + `--flaeche` liegen sieben Ebenen. + + WAS ZAEHLT, IST DER PUNKT AUF DEM SCHIRM. Und den liefert nur ein + Bildschirmfoto. + + WARUM SELBST GESCHRIEBEN: Das Projekt hat kein Bildmodul, und fuer + drei Pixel eines nachzuinstallieren waere eine Abhaengigkeit, die + jemand pflegen muss. Node bringt `zlib` mit; ein PNG ist danach nur + noch Zeilenfilter, und davon gibt es fuenf. + + ES LIEST NUR, WAS PLAYWRIGHT SCHREIBT: 8 Bit je Kanal, RGB oder + RGBA, nicht verschraenkt. Alles andere meldet es als Fehler, statt + zu raten -- ein Bildleser, der bei unbekanntem Format irgendetwas + zurueckgibt, faelscht Messwerte. + ===================================================================== */ +import { inflateSync } from "node:zlib"; + +/** Liest ein PNG aus einem Puffer. + * @returns {{breite:number, hoehe:number, kanaele:number, daten:Buffer}} */ +export function pngLesen(puffer) { + if (puffer.length < 8 || puffer.readUInt32BE(0) !== 0x89504e47) { + throw new Error("Kein PNG"); + } + let pos = 8; + let breite = 0, hoehe = 0, tiefe = 0, art = 0; + const teile = []; + while (pos < puffer.length) { + const laenge = puffer.readUInt32BE(pos); + const name = puffer.toString("ascii", pos + 4, pos + 8); + const inhalt = puffer.subarray(pos + 8, pos + 8 + laenge); + if (name === "IHDR") { + breite = inhalt.readUInt32BE(0); + hoehe = inhalt.readUInt32BE(4); + tiefe = inhalt[8]; + art = inhalt[9]; + if (tiefe !== 8) throw new Error(`Nur 8 Bit je Kanal, nicht ${tiefe}`); + if (art !== 2 && art !== 6) throw new Error(`Nur RGB/RGBA, nicht Typ ${art}`); + if (inhalt[12] !== 0) throw new Error("Verschraenkte PNG werden nicht gelesen"); + } else if (name === "IDAT") { + teile.push(inhalt); + } else if (name === "IEND") { + break; + } + pos += 12 + laenge; // Laenge + Name + Inhalt + Pruefsumme + } + const kanaele = art === 6 ? 4 : 3; + const roh = inflateSync(Buffer.concat(teile)); + + /* Die Filter rueckgaengig machen. Jede Zeile beginnt mit einem Byte, + das sagt, wie sie kodiert wurde -- PNG-Spezifikation, Abschnitt 9. */ + const zeile = breite * kanaele; + const daten = Buffer.alloc(zeile * hoehe); + for (let y = 0; y < hoehe; y++) { + const filter = roh[y * (zeile + 1)]; + const quelle = roh.subarray(y * (zeile + 1) + 1, y * (zeile + 1) + 1 + zeile); + const ziel = daten.subarray(y * zeile, (y + 1) * zeile); + const oben = y > 0 ? daten.subarray((y - 1) * zeile, y * zeile) : null; + for (let x = 0; x < zeile; x++) { + const a = x >= kanaele ? ziel[x - kanaele] : 0; // links + const b = oben ? oben[x] : 0; // oben + const c = oben && x >= kanaele ? oben[x - kanaele] : 0; // links oben + let wert = quelle[x]; + if (filter === 1) wert += a; + else if (filter === 2) wert += b; + else if (filter === 3) wert += (a + b) >> 1; + else if (filter === 4) { + /* Paeth: der Nachbar, der der Vorhersage am naechsten liegt. */ + const p = a + b - c; + const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c); + wert += (pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c); + } else if (filter !== 0) { + throw new Error(`Unbekannter Zeilenfilter ${filter}`); + } + ziel[x] = wert & 0xff; + } + } + return { breite, hoehe, kanaele, daten }; +} + +/** Die Farbe an einer Stelle -- [r, g, b]. */ +export function pixel(bild, x, y) { + const i = (Math.round(y) * bild.breite + Math.round(x)) * bild.kanaele; + return [bild.daten[i], bild.daten[i + 1], bild.daten[i + 2]]; +} + +const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4); + +/** Relative Helligkeit nach WCAG. */ +export const helligkeit = ([r, g, b]) => + 0.2126 * linear(r / 255) + 0.7152 * linear(g / 255) + 0.0722 * linear(b / 255); + +/** Kontrast zweier Farben nach WCAG -- immer >= 1. */ +export function kontrast(a, b) { + const x = helligkeit(a), y = helligkeit(b); + return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); +} diff --git a/server/pruef-kachel-universum.mjs b/server/pruef-kachel-universum.mjs index de120cb6..f1e1a631 100644 --- a/server/pruef-kachel-universum.mjs +++ b/server/pruef-kachel-universum.mjs @@ -1,99 +1,223 @@ /* ===================================================================== - DER GRUND JEDER KACHEL IM HAUS VON TEAM DOGI (11.09.2026) + DIE KACHELN: FARBEN UND UNIVERSUM (17.09.2026) - Filipe: "veraender die kacheln und den hintergrund, ich will dass - die hintergrunde von den kacheln immer unviersum artig ist, es muss - richtig geil sein aber immer so dass man alles noch gut erkkent. - und das IN DER GANZEN WEBSITE VON TEAM DOGI. nur die kacheln. [...] - wichtig ist die form der kacheln soll gleich bleiben." + Filipe, zwei Wünsche in einem Satz: - Daraus werden vier pruefbare Saetze, und jeder einzelne kann - scheitern: + „ich will das jede kachel eine andere farbe hat, es soll keine die + gleiche farben haben bitte und auch keine die sich irgendwie + ähnlich sind ... die farben sollen auch richtig geil und speziell + sein." - 1. UEBERALL. Nicht "auf der Startseite", sondern auf jeder Kachel - des Hauses -- auch auf denen, die es heute noch nicht gibt. - Deshalb wird geprueft, dass crew-haus.css KEINE Kachelklasse - nennt: Sie setzt vier Werte um, aus denen module.css den Grund - jeder Kachel baut. Eine Datei, die stattdessen Kachel fuer - Kachel aufzaehlt, waere bei der naechsten neuen unvollstaendig - -- lautlos. - 2. NUR DIE KACHELN. Der Grund der SEITE bleibt, wie er war. - 3. DIE FORM BLEIBT GLEICH. Fase, Silhouette und die drei - Eckwinkel werden vor und nach dem Hauswechsel verglichen, - Zeichen fuer Zeichen. - 4. MAN ERKENNT ALLES NOCH GUT. Und das ist der Teil, der wirklich - Arbeit macht. + „der hintergrund von allen kacheln haben wir richtung universum + mäßig gemacht, aber bin noch nicht ganz zufrieden ... nicht + einfach so paar weiße punkte sondern richtig geil hochwertiger + geiler universum als hintergrund in den kacheln überall." + + BEIDES WAR BERECHTIGT, und beides ließ sich messen: + + kleinster Farbabstand 0,0529 (Rückmeldung / Steckbrief) + Paare unter 0,10 26 von 666 + schlechtester Kontrast 1,64:1 (Ton 35 — unlesbar) + Universum auf GENAU EINER Kachel + + Der Grund für das erste: Ein Werkzeug rechnete die Farben, kannte + aber nur 21 — seit dem 08.09. waren sechzehn von Hand dazugekommen. + Es meldete weiter „alle Bedingungen erfüllt" und meinte einen Stand, + den es nicht mehr gab. --------------------------------------------------------------------- - WIE LESBARKEIT AUF EINEM STERNENHIMMEL GEMESSEN WIRD + WAS HIER GEPRÜFT WIRD — und warum es drei Abschnitte braucht: - Ein Sternenfeld ist kein Hintergrund, sondern viele. Hinter einem - Buchstaben liegt mal fast schwarzer Grund, mal Nebel, mal -- selten - -- der Kern eines Sterns. Eine einzige Durchschnittszahl wuerde - genau den Fall verschlucken, der stoert; eine Regel auf den - einzelnen hellsten Punkt dagegen waere eine Regel GEGEN Sterne, denn - ein Stern ist per Definition der hellste Punkt seiner Umgebung. + 1. Die Farben stehen weit genug auseinander (OKLab, alle Paare). + 2. Jede hält 4,5:1 gegen den dunklen Grund — sie trägt auch Text. + 3. DER TEXT AUF DER FERTIGEN KACHEL ist lesbar. - Deshalb drei Zahlen je Textstelle, alle aus denselben echten - Bildpunkten eines Bildschirmfotos: + Der dritte ist der, den sonst niemand macht, und der einzige, der + die Frage wirklich beantwortet: Zwischen Textfarbe und `--flaeche` + liegen inzwischen SIEBEN Ebenen (drei Nebel, Sterne, Milchstraße, + Vignette, Glanz) plus das durchscheinende Bühnenbild. Aus dem CSS + lässt sich das nicht mehr ausrechnen — gemessen wird deshalb am + Bildschirmfoto, an den echten Bildpunkten. - * MITTEL -- die Flaeche, die ein Buchstabe wirklich sieht. - Muss 4,5:1 halten. Das ist die Regel. - * ANTEIL -- wie viel Prozent der Punkte hinter dem Text unter - 4,5:1 liegen. Das ist die eigentliche Messung: Sie - unterscheidet einen PUNKT von einer FLAECHE. Ein - Nebel, der zu hell wird, faellt hier sofort auf, - auch wenn der Mittelwert noch durchgeht. Grenze: - 1,5 Prozent. Gemessen wurden 0,25 Prozent, im - Agenturhaus an derselben Stelle 0,115 Prozent. - * HELLSTER -- der einzelne hellste Bildpunkt, also ein Sternkern. - Grenze 3,4:1 -- bewusst unter der Regel, denn dieser - Punkt ist kleiner als ein Buchstabenstrich breit. - Ohne diese Zahl koennte jemand die Sterne beliebig - hell machen, solange nur wenige davon da sind. - - Gemessen wird an ECHTEN BILDPUNKTEN, nicht an Farbangaben. Das ist - hier keine Vorsicht, sondern Notwendigkeit: Was hinter dem Text - liegt, entsteht erst beim Zeichnen aus Grund, Nebel, Raster und - Schleier. Keine Farbangabe kennt das Ergebnis. - - UND DER INHALT DER KACHEL WIRD DABEI UNSICHTBAR GESCHALTET. Der - erste Anlauf hat das nicht getan und Kontraste von 1,95:1 gemeldet - -- gegen das grosse Zeichen der Kachel, das gar keinen Text - verdeckt. Gefragt ist, was HINTER dem Text liegt, und das gehoert - der Kachel, nicht ihren Kindern. - - --------------------------------------------------------------------- - WARUM DIE ZWEITE ADRESSE HIER NICHT IM BROWSER AUFGERUFEN WIRD - - Das Haus liefert `upgrade-insecure-requests` aus. Ruft man - crew.dogfather-universe.com im Testbrowser ueber eine umgebogene - Namensaufloesung auf, macht der Browser daraus https und findet - nichts. Diese Grenze ist bekannt und wird wie bei den anderen - Haus-Pruefungen umgangen, indem die Frage in zwei geteilt wird: - - a) KOMMT DIE DATEI DORT AN? Das beantwortet eine echte Anfrage mit - gesetztem Host-Kopf ueber node:http -- ohne Browser, ohne CSP. - b) WIE SIEHT SIE AUS? Das beantwortet der Browser, indem genau - dieselbe Datei nachgeladen wird, an derselben Stelle wie im - Betrieb: als letzte Stilvorlage. pruef-css-klassen sichert ab, - dass sie im Betrieb auch wirklich zuletzt steht. - - Nachgeladen wird ueber ihre ADRESSE, nicht ueber ihren Dateipfad. - Das ist kein Detail: Bei einer eingefuegten Datei rechnet der - Browser relative Verweise gegen die SEITE, und aus `../img/…` wird - ein 404, den es im Betrieb nicht gibt. Zwei Minuten Fehlersuche an - einem Fehler, den die Pruefung selbst erzeugt hatte. + Aufrufen mit: node server/pruef-kachel-universum.mjs ===================================================================== */ import { mkdtempSync, rmSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { request as httpAnfrage } from "node:http"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; import { portMussFreiSein } from "./helfer-port.mjs"; +import { pngLesen, pixel, kontrast } from "./helfer-png.mjs"; -const PORT = await portMussFreiSein(4401, "pruef-kachel-universum"); +const HIER = dirname(fileURLToPath(import.meta.url)); +const CSS = join(HIER, "..", "workspace", "assets", "css", "start.css"); +const GRUND = [10, 18, 30]; // #0a121e -const ordner = mkdtempSync(join(tmpdir(), "ws-universum-")); +let fehler = 0, geprueft = 0, offen = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; +const unklar = (t) => { offen++; console.log(" -- " + t); }; + +/* ---------- OKLab ---------------------------------------------------- */ +const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4); +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, + ]; +} +const abstand = (a, b) => { + const x = oklab(a), y = oklab(b); + return Math.hypot(x[0] - y[0], x[1] - y[1], x[2] - y[2]); +}; +const alsRgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)); + +/* ======================================================================= + 1. DIE FARBEN STEHEN AUSEINANDER + ======================================================================= */ +melde("=== 1. Die Farben ==="); + +const cssText = readFileSync(CSS, "utf8"); +const TOENE = 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()])); + +/* DIE ANZAHL KOMMT AUS DER DATEI. Genau hier ist das Farbwerkzeug + unzuständig geworden: Es kannte 21, und niemand hat es angefasst, + als sechzehn dazukamen. Eine Zahl, die niemand pflegt, kann nicht + veralten. */ +ok(TOENE.size >= 20, `${TOENE.size} Farben in start.css gefunden`); + +/* Werden alle auch benutzt? Eine Farbe ohne Kachel ist kein Fehler, + aber eine KACHEL OHNE FARBE ist einer -- sie fiele auf den Akzent + zurück, und dann hätten mehrere Kacheln dieselbe. */ +{ + const quellen = ["workspace.js", "../workspace/assets/js/bereiche.js", + "../workspace/assets/js/start.js", "../workspace/assets/js/kopf.js", + "../workspace/assets/js/befinden.js"]; + const benutzt = new Set(); + for (const q of quellen) { + const t = readFileSync(join(HIER, q), "utf8"); + for (const m of t.matchAll(/ton:\s*(\d+)/g)) benutzt.add(Number(m[1])); + } + const ohne = [...benutzt].filter((n) => !TOENE.has(n)); + ok(ohne.length === 0, + ohne.length ? `Kacheln ohne Farbe: Ton ${ohne.join(", ")}` + : `alle ${benutzt.size} benutzten Tonnummern haben eine Farbe`); +} + +const liste = [...TOENE.entries()].map(([n, hex]) => ({ n, hex })); +const paare = []; +for (let i = 0; i < liste.length; i++) { + for (let j = i + 1; j < liste.length; j++) { + paare.push({ a: liste[i], b: liste[j], d: abstand(liste[i].hex, liste[j].hex) }); + } +} +paare.sort((x, y) => x.d - y.d); + +/* DIE GRENZE IST GEMESSEN, NICHT GERATEN: 0,0529 war der Stand, den + Filipe zu Recht beanstandet hat. Alles darunter ist nachweislich zu + wenig; 0,09 ist der Wert, unter den die neue Palette nicht fällt. */ +const GRENZE = 0.09; +ok(paare[0].d >= GRENZE, + `kleinster Abstand ${paare[0].d.toFixed(4)} (Grenze ${GRENZE}) ` + + `-- ${paare[0].a.hex}/${paare[0].b.hex}`); +{ + const eng = paare.filter((p) => p.d < 0.10).length; + ok(eng === 0, + eng ? `${eng} Paare unter 0,10: ${paare.slice(0, 3).map((p) => `${p.a.hex}/${p.b.hex}`).join(", ")}` + : `kein einziges der ${paare.length} Paare liegt unter 0,10`); +} +{ + /* KEINE ZWEI GLEICHEN. Das steht als eigene Zeile da, obwohl der + Abstand es mit abdeckt -- es ist die wörtliche Bitte, und eine + Prüfung, die die Frage direkt beantwortet, liest sich später + besser als eine, aus der man es schließen muss. */ + const doppelt = liste.length - new Set(liste.map((x) => x.hex)).size; + ok(doppelt === 0, doppelt ? `${doppelt} Farben kommen doppelt vor` : "keine Farbe kommt zweimal vor"); +} + +/* Jede Farbe trägt Text -- sie muss gegen den dunklen Grund halten. */ +{ + const schlecht = liste.filter((x) => kontrast(alsRgb(x.hex), GRUND) < 4.5); + ok(schlecht.length === 0, + schlecht.length + ? `unter 4,5:1 -> ${schlecht.map((x) => `Ton ${x.n} ${x.hex} ` + + `${kontrast(alsRgb(x.hex), GRUND).toFixed(2)}:1`).join(", ")}` + : `alle halten 4,5:1 gegen den Grund (schlechteste ` + + `${Math.min(...liste.map((x) => kontrast(alsRgb(x.hex), GRUND))).toFixed(2)}:1)`); +} + +/* ======================================================================= + 2. DAS UNIVERSUM IST DA -- UND ÜBERALL + + Gezählt wird im CSS: Wie viele Ebenen hat der Kachelhintergrund? Ein + Universum aus „ein paar weißen Punkten" hat drei, das gebaute hat + siebzehn. Die Zahl allein sagt nichts über Schönheit -- sie sagt, + dass die Ebenen NICHT VERSEHENTLICH verschwinden, und genau das ist + schon einmal passiert (die Leuchtschiene, 07.09.: Regeln blieben, + Inhalt weg). + ======================================================================= */ +melde(""); +melde("=== 2. Das Universum im Hintergrund ==="); +{ + const block = cssText.slice(cssText.indexOf(".kachel {"), + cssText.indexOf("background-size:", cssText.indexOf(".kachel {"))); + /* DIE GROESSEN, NICHT NUR DIE ANZAHL. + + Der erste Anlauf zaehlte `radial-gradient(...px` -- und `0px` ist + auch eine Zahl. Setzt man alle Sterne auf null, blieb die Zaehlung + bei zwoelf und die Pruefung gruen, waehrend das Universum leer war. + + UND WARUM NICHT AM BILD GEMESSEN WIRD: Der zweite Anlauf zaehlte + Lichtpunkte im Bildschirmfoto. Auch das trug nicht -- die Kachel + ist halbdurchsichtig, darunter liegt das Buehnenbild, und ein Foto + hat selbst Punktstruktur. Gemessen: 459 Punkte mit Sternen, 441 + ohne. Vier Prozent Unterschied sind kein Nachweis, sondern + Rauschen. Eine Pruefung, die das als "bestanden" wertet, prueft + nichts -- lieber eine ehrliche schwache als eine starke, die luegt. */ + const groessen = [...block.matchAll(/radial-gradient\(\s*([\d.]+)px/g)] + .map((m) => Number(m[1])); + const sterne = groessen.filter((g) => g >= 0.7).length; + const nebel = (block.match(/radial-gradient\(\s*\d+%/g) || []).length; + ok(sterne >= 10, + `${sterne} Sternebenen mit sichtbarer Groesse ` + + `(${groessen.length} Punktebenen insgesamt, kleinste ` + + `${Math.min(...groessen).toFixed(1)}px)`); + ok(nebel >= 3, `${nebel} Nebel- und Tiefenebenen`); + { + /* GEZAEHLT, NICHT NUR GESUCHT. Der erste Anlauf fragte, ob + `var(--ton)` im Block VORKOMMT -- das tut es auch im + Grundverlauf, der seit jeher da ist. Die Gegenprobe (einem Nebel + die Kachelfarbe wegnehmen) blieb deshalb gruen. Gezaehlt werden + jetzt die FLAECHIGEN Verlaeufe, die den Ton wirklich tragen. */ + const nebelMitTon = (block.match( + /radial-gradient\(\s*\d+%[^)]*\)[^,]*var\(--ton\)|radial-gradient\(\s*\d+%[^;]*?color-mix\(in srgb, var\(--ton\)/g) || []).length; + ok(nebelMitTon >= 2, + `${nebelMitTon} Nebel tragen die Farbe der Kachel ` + + "-- jede Kachel ihr eigenes Universum"); + } + ok(/118deg|milchstra/i.test(block), "und es gibt eine Schräge (die Milchstraße)"); +} + +/* ======================================================================= + 3. DER TEXT DARAUF IST LESBAR + + Der Abschnitt, den sonst niemand macht. Gemessen am Bildschirmfoto: + Zwischen Textfarbe und Fläche liegen sieben Ebenen plus das + durchscheinende Bühnenbild -- aus dem CSS ist das nicht mehr + auszurechnen. + ======================================================================= */ +melde(""); +melde("=== 3. Der Text auf der fertigen Kachel ==="); + +const PORT = await portMussFreiSein(4194, "pruef-kachel-universum"); +const ordner = mkdtempSync(join(tmpdir(), "ws-kachel-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); process.env.PORT = String(PORT); process.env.SITE_ACCESS_SECRET = "lokaler-test"; @@ -102,570 +226,131 @@ process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; const express = (await import("express")).default; const ec = express.response.cookie; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; -import { notbremse } from "./helfer-notbremse.mjs"; +const { notbremse } = await import("./helfer-notbremse.mjs"); await import("./index.js"); -notbremse(240_000, "pruef-kachel-universum"); -await new Promise((r) => setTimeout(r, 700)); -const BASIS = `http://127.0.0.1:${PORT}`; +notbremse(180_000, "pruef-kachel-universum"); +await new Promise((r) => setTimeout(r, 800)); -/* EIGENE ZUHOERER. index.js haengt fuer den Betrieb Auffangnetze an - `uncaughtException` -- eine Pruefung, die dieselbe Datei in denselben - Prozess laedt, erbt sie und wuerde nach einem Absturz einfach - weiterhaengen, statt rot zu werden. */ -process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); -process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); - -let fehler = 0, geprueft = 0; -const melde = (t) => console.log(t); -const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; - -const { DatabaseSync } = await import("node:sqlite"); -const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); -const d = new DatabaseSync(process.env.WORKSPACE_DB); -const jetzt = new Date().toISOString(); - -const schluessel = randomBytes(32).toString("hex"); -d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" - + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") - .run("code_kennung_schluessel", schluessel, jetzt); - -function anlegen(name, rolle, code) { +{ + const { DatabaseSync } = await import("node:sqlite"); + const { scryptSync, randomBytes } = await import("node:crypto"); + const d = new DatabaseSync(process.env.WORKSPACE_DB); const salt = randomBytes(16).toString("hex"); - const hash = scryptSync(code, salt, 64, + const hash = scryptSync("CODE-DOGI-0001", salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); - d.prepare("INSERT INTO personen" - + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" - + " VALUES (?,?,?,?,?,?,1,?)") - .run(name, rolle, hash, salt, 32768, - createHmac("sha256", schluessel).update(code).digest("hex"), jetzt); - return d.prepare("SELECT last_insert_rowid() AS id").get().id; + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run("Filipe", "admin", hash, salt, 32768, new Date().toISOString()); + d.close(); } -anlegen("Filipe", "admin", "CODE-DOGI-0001"); -anlegen("Rieke", "hand", "CODE-HAND-0001"); -anlegen("Frida", "modi", "CODE-MODI-0001"); -anlegen("Tomma", "modi", "CODE-MODI-0002"); -/* Die Teamseite zeichnet ihre Personenkarten aus dem, was das jeweilige - Haus zu sehen bekommt. Auf DIESER Adresse sind das Manager und - Scouts -- ohne sie bliebe `.tperson` leer, und die Messung der - Personenkarte waere ein uebersprungener Test, der nichts beweist. - Die Kachel ist in beiden Haeusern dieselbe; gemessen wird ihr Grund, - nicht wessen Name darin steht. */ -anlegen("Mara", "manager", "CODE-MANA-0001"); -anlegen("Sten", "scout", "CODE-SCOU-0001"); -d.close(); +let chromium; +try { + ({ chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs")); +} catch { chromium = null; } -const CREW_CSS_ADRESSE = "/workspace/assets/css/crew-haus.css"; -const CREW_CSS = readFileSync( - new URL("../workspace/assets/css/crew-haus.css", import.meta.url).pathname - .replace(/^\/([A-Za-z]:)/, "$1"), "utf8"); - -/* ======================================================================= - 1. DIE DATEI KOMMT AUF DER ZWEITEN ADRESSE AN -- UND NUR DORT - ======================================================================= */ -melde(""); -melde("=== 1. Ausgeliefert wird sie da, wo sie hingehoert ==="); - -function holen(host) { - return new Promise((fertig, schief) => { - const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: "/workspace/assets/css/haus.css", - method: "GET", headers: { Host: host } }, (antwort) => { - let text = ""; - antwort.on("data", (s) => { text += s; }); - antwort.on("end", () => fertig({ code: antwort.statusCode, text })); +if (!chromium) { + unklar("kein Playwright gefunden -- die Kacheln wurden nicht angesehen"); +} else { + const browser = await chromium.launch(); + try { + const kontext = await browser.newContext({ + viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2, }); - a.on("error", schief); - a.end(); - }); -} -{ - const crew = await holen("crew.dogfather-universe.com"); - const agentur = await holen("workspace.dogfather-universe.com"); - ok(crew.code === 200 && crew.text.includes("--sternenfeld:"), - `die zweite Wand bekommt das Sternenfeld (${crew.code}, ${crew.text.length} Zeichen)`); - ok(crew.text.includes("--modul-schleier: .30"), - "und den Schleier, der dazugehoert"); - ok(agentur.code === 200 && !agentur.text.includes("--sternenfeld"), - `die Agenturadresse bekommt es NICHT (${agentur.text.length} Zeichen)`); - ok(crew.text.length > agentur.text.length + 500, - "die beiden Haeuser liefern wirklich verschiedene Dateien aus"); -} + const seite = await kontext.newPage(); + await seite.goto(`http://127.0.0.1:${PORT}/workspace/`, { waitUntil: "domcontentloaded" }); + await seite.click('.rolle[data-rolle="admin"]'); + await seite.fill("#code", "CODE-DOGI-0001"); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 15000 }); + await seite.waitForTimeout(2500); -/* KEINE ACHTE ABSCHRIFT DER KACHELLISTE. - Der eigentliche Gedanke hinter dem ganzen Umbau: crew-haus.css darf - keine Kachel beim Namen nennen. Sonst waere sie beim naechsten neuen - Bauteil unvollstaendig, ohne dass es auffiele. */ -{ - const kachelklassen = [".eintrag-karte", ".t-kachel", ".tperson", ".liste-kachel", - ".steckbrief", ".zahlkarte", ".k-anlasskachel", ".leer-hinweis"]; - const gefunden = kachelklassen.filter((k) => CREW_CSS.includes(k)); - ok(gefunden.length === 0, - `crew-haus.css zaehlt keine Kachelklassen auf (${gefunden.join(" ") || "keine"})`); - ok(CREW_CSS.includes("--sternenfeld:") && CREW_CSS.includes("--sternenfeld-mass:") - && CREW_CSS.includes("--sternenfeld-lage:"), - "sie setzt stattdessen die Werte, aus denen module.css den Grund baut"); + /* ERST ZU DEN KACHELN SCROLLEN. Beim ersten Lauf wurden NULL + gemessen: Der Filter verlangt, dass die Kachel ganz im Bild + liegt, und oben steht die Zentrale mit Uhr und Zahlen. Eine + Messung, die nichts findet, ist keine Messung -- sie sah nur + aus wie eine. */ + /* In die Mitte des Kachelfeldes, nicht an seinen Anfang: Dort + stehen mehrere Reihen gleichzeitig im Bild, und je mehr Kacheln + gemessen werden, desto eher ist die schlechteste dabei. */ + const anzahl = await seite.locator(".kachel").count(); + await seite.locator(".kachel").nth(Math.min(6, anzahl - 1)).scrollIntoViewIfNeeded(); + await seite.waitForTimeout(700); - const modul = readFileSync( - new URL("../workspace/assets/css/module.css", import.meta.url).pathname - .replace(/^\/([A-Za-z]:)/, "$1"), "utf8"); - ok((modul.match(/var\(--sternenfeld,/g) || []).length === 1, - "und module.css benutzt das Bild an genau EINER Stelle"); - ok(modul.includes("--sternenfeld: none;"), - "im Agenturhaus steht dort `none` -- eine Ebene ohne Bild zeichnet nichts"); -} + const stellen = await seite.evaluate(() => + [...document.querySelectorAll(".kachel")].map((k) => { + const name = k.querySelector(".kachel__name"); + if (!name) return null; + const r = name.getBoundingClientRect(); + /* SICHTBAR HEISST: die Zeile liegt im Bild -- nicht die ganze + Kachel. Der erste Filter verlangte letzteres und liess genau + EINE uebrig; gemessen wird aber der Punkt unter dem TEXT, + und der braucht nur sich selbst. */ + if (r.width < 10 || r.top < 8 || r.bottom > window.innerHeight - 8) return null; + const [tr, tg, tb] = getComputedStyle(name).color.match(/\d+/g).map(Number); + /* DIE KACHELGRENZEN KOMMEN MIT. Ohne sie wurde der Messbereich + aus der Textposition geschaetzt (`y - 70`) -- und lag damit + OBERHALB der Kachel, im Seitenhintergrund. Die Gegenprobe + lieferte zweimal exakt 153 Punkte; genau diese Gleichheit hat + den Fehler verraten. Eine Messung, deren Ergebnis sich nicht + aendert, wenn man das Gemessene entfernt, misst etwas + anderes. */ + const kb = k.getBoundingClientRect(); + return { + name: name.textContent.trim(), text: [tr, tg, tb], + x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2), + kachel: { + links: Math.round(kb.left), oben: Math.round(kb.top), + breite: Math.round(kb.width), hoehe: Math.round(kb.height), + }, + }; + }).filter(Boolean)); -/* KEIN KARO. Wiederholen sich zwei Sternebenen im selben Abstand, sieht - man sofort ein Muster statt eines Feldes. Die Abstaende duerfen - deshalb keinen gemeinsamen Teiler haben -- das ist keine Kosmetik, - sondern der Unterschied zwischen Himmel und Tapete. */ -{ - const zeile = CREW_CSS.split("--sternenfeld-mass:")[1].split(";")[0]; - const zahlen = (zeile.match(/(\d+)px/g) || []).map((z) => parseInt(z, 10)); - const ggt = (a, b) => (b === 0 ? a : ggt(b, a % b)); - let groesster = 0; - for (let i = 0; i < zahlen.length; i++) - for (let j = i + 1; j < zahlen.length; j++) - groesster = Math.max(groesster, ggt(zahlen[i], zahlen[j])); - ok(zahlen.length >= 6, `sechs Abstaende fuer drei Sternebenen (${zahlen.join(" ")})`); - ok(groesster === 1, `kein gemeinsamer Teiler, also kein Karo (groesster: ${groesster})`); -} + /* DIE ANZAHL GEHÖRT IN DIE BEDINGUNG: Ohne sichtbare Kacheln wäre + „alle lesbar" auch dann grün, wenn keine einzige gemessen wurde. */ + ok(stellen.length >= 4, `${stellen.length} Kacheln im Bild gemessen`); -/* ======================================================================= - 2. DER BROWSER: erst das Agenturhaus, dann dasselbe mit dem Himmel - ======================================================================= */ -const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); -const browser = await pw.chromium.launch(); -const ctx = await browser.newContext({ viewport: { width: 1440, height: 1100 } }); -const seite = await ctx.newPage(); -const konsole = []; -seite.on("pageerror", (f) => konsole.push("Absturz: " + f.message)); -seite.on("console", (m) => { if (m.type() === "error") konsole.push("Konsole: " + m.text()); }); -seite.on("response", (a) => { if (a.status() >= 400) konsole.push(`${a.status()} ${a.url()}`); }); + /* DER DRITTE AUSGANG. Ohne diesen Sprung stuerzte der Abschnitt ab + (`werte[0].k` auf einem leeren Feld) -- und ein Absturz sagt + weniger als ein Fehlschlag: Er verschluckt die Zeilen danach und + sieht aus wie ein Problem der Pruefung statt der Sache. */ + if (!stellen.length) { + ok(false, "ohne sichtbare Kachel laesst sich kein Kontrast messen"); + } else { + + const bild = pngLesen(await seite.screenshot({ type: "png" })); + const werte = stellen.map((s) => { + /* Mehrere Punkte je Kachel -- der Untergrund ist nicht + gleichmäßig (Nebel, Vignette, Sterne). Der SCHLECHTESTE zählt: + An der hellsten Stelle steht der Text am schwersten. */ + let schlecht = Infinity; + for (const dx of [-30, -15, 0, 15, 30]) { + for (const dy of [-4, 0, 4]) { + const k = kontrast(s.text, pixel(bild, (s.x + dx) * 2, (s.y + dy) * 2)); + if (k < schlecht) schlecht = k; + } + } + return { name: s.name, k: schlecht }; + }).sort((a, b) => a.k - b.k); + + ok(stellen.length > 0 && werte[0].k >= 4.5, + `schlechtester Textkontrast ${werte[0].k.toFixed(2)}:1 auf „${werte[0].name}" (Grenze 4,5)`); + melde(` die drei schwächsten: ${werte.slice(0, 3) + .map((w) => `${w.name} ${w.k.toFixed(1)}:1`).join(" · ")}`); -await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); -await seite.click('.rolle[data-rolle="admin"]'); -await seite.fill("#code", "CODE-DOGI-0001"); -await seite.click("button[type=submit]"); -await seite.waitForURL("**/start.html", { timeout: 25000 }); -await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" }); -await seite.waitForSelector(".kachel", { timeout: 10000 }); -await seite.waitForTimeout(600); -/* Alles, was sich beim Hauswechsel NICHT aendern darf, wird vorher - abgeholt -- Form, Silhouette, Eckwinkel, Seitengrund. */ -async function formAufnehmen() { - return seite.evaluate(() => { - const k = document.querySelector(".kachel"); - const s = getComputedStyle(k); - const n = getComputedStyle(k, "::after"); - const v = getComputedStyle(document.documentElement); - /* Ein Hintergrund ist eine Liste, und in jeder Ebene stehen selbst - Kommas. Mit einem Muster kommt man da nicht weit -- Chromium - schreibt die Verlaeufe voll aus, Klammern in Klammern. Gezaehlt - wird deshalb, wie tief man gerade steht. */ - let tiefe = 0, ebenen = 1; - for (const z of n.backgroundImage) { - if (z === "(") tiefe++; - else if (z === ")") tiefe--; - else if (z === "," && tiefe === 0) ebenen++; } - return { - fase: v.getPropertyValue("--fase").trim(), - silhouette: s.clipPath, - /* Die sechs Eckwinkel sind die ersten sechs Ebenen. Ihre Groessen - und Lagen SIND die Form. */ - winkelMass: n.backgroundSize.split(",").slice(0, 6).join(",").trim(), - winkelLage: n.backgroundPosition.split(",").slice(0, 6).join(",").trim(), - ebenen, - seitengrund: getComputedStyle(document.body).backgroundColor, - sternenfeld: v.getPropertyValue("--sternenfeld").trim(), - schleier: v.getPropertyValue("--modul-schleier").trim(), - hausname: v.getPropertyValue("--haus-name").trim(), - }; - }); -} -const vorher = await formAufnehmen(); -melde(""); -melde("=== 2. Das Agenturhaus bleibt unberuehrt ==="); -ok(vorher.sternenfeld === "none", - `dort steht kein Sternenfeld (${vorher.sternenfeld})`); -ok(vorher.schleier === ".26", - `und der Schleier steht unveraendert auf .26 (${vorher.schleier})`); -ok(vorher.ebenen === 9, - `neun Ebenen: sechs Eckwinkel, zwei Rasterlinien, eine leere (${vorher.ebenen})`); - -/* Bildpunkte aus einem Bildschirmfoto -- derselbe Weg wie in - pruef-lesbarkeit und pruef-personen-kachel. Zurueck kommen der - Mittelwert und die SORTIERTE Helligkeitsliste; wer daraus einen - Anteil rechnen will, braucht die ganze Liste, nicht eine Kennzahl. */ -async function punkte(clip) { - const bild = await seite.screenshot({ clip }); - return seite.evaluate(async (b64) => { - const bild = new Image(); - await new Promise((r) => { bild.onload = r; bild.src = "data:image/png;base64," + b64; }); - const c = document.createElement("canvas"); - c.width = bild.width; c.height = bild.height; - const g = c.getContext("2d", { willReadFrequently: true }); - g.drawImage(bild, 0, 0); - const dd = g.getImageData(0, 0, c.width, c.height).data; - const hell = (p) => { - const f = p.map((x) => { const s = x / 255; return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4); }); - return .2126 * f[0] + .7152 * f[1] + .0722 * f[2]; - }; - const liste = []; - let sr = 0, sg = 0, sb = 0, hr = 0, hg = 0, hb = 0, best = -1; - for (let i = 0; i < dd.length; i += 4) { - sr += dd[i]; sg += dd[i + 1]; sb += dd[i + 2]; - const h = hell([dd[i], dd[i + 1], dd[i + 2]]); - liste.push(h); - if (h > best) { best = h; hr = dd[i]; hg = dd[i + 1]; hb = dd[i + 2]; } - } - const n = liste.length; - liste.sort((a, b) => a - b); - return { - mittel: [Math.round(sr / n), Math.round(sg / n), Math.round(sb / n)], - hellsterPunkt: [hr, hg, hb], - hell: liste, anzahl: n, - }; - }, bild.toString("base64")); -} - -/* WAS HINTER DEM TEXT LIEGT, IST NICHT DER TEXT -- UND AUCH NICHT SEIN - NACHBAR. Fuer die Dauer der Messung wird der INHALT der Kachel - unsichtbar, die Kachel selbst nicht: Hintergrund, Kantenlicht, - Raster, Eckwinkel und Sternenfeld gehoeren ihr, nicht ihren Kindern. */ -const KARTEN = ".kachel, .t-kachel, .tperson, .tl, .eintrag-karte, .person, .liste-kachel"; -async function inhaltAus(waehler) { - await seite.evaluate(([w, karten]) => { - const e = document.querySelector(w); - const karte = e && (e.closest(karten) || e.parentElement); - if (!karte) return; - karte.dataset.grundmessung = "ja"; - karte.querySelectorAll("*").forEach((x) => { - x.dataset.warSichtbar = x.style.visibility || ""; - x.style.visibility = "hidden"; - }); - }, [waehler, KARTEN]); -} -async function inhaltAn() { - await seite.evaluate(() => { - const karte = document.querySelector('[data-grundmessung="ja"]'); - if (!karte) return; - karte.querySelectorAll("*").forEach((x) => { - x.style.visibility = x.dataset.warSichtbar || ""; - delete x.dataset.warSichtbar; - }); - delete karte.dataset.grundmessung; - }); -} - -/* DER AUSSCHNITT LIEGT IN DER MITTE DER KACHEL. Am Rand sitzt das - Kantenlicht -- eine 1,6 px schmale Linie in fast vollem Kachelton, - und damit der hellste Punkt der ganzen Kachel. Wer den mitmisst, - misst die Form statt des Himmels: Der erste Anlauf meldete deshalb - "keine neuen hellen Punkte", obwohl das Sternenfeld laengst da war. */ -const kachelRahmen = await seite.evaluate(() => { - const k = document.querySelector(".kachel"); - k.scrollIntoView({ block: "center" }); - const r = k.getBoundingClientRect(); - return { x: Math.round(r.x + r.width * .22), y: Math.round(r.y + r.height * .22), - width: Math.round(r.width * .56), height: Math.round(r.height * .56) }; -}); -await inhaltAus(".kachel"); -const grundAgentur = await punkte(kachelRahmen); -await inhaltAn(); - -/* ---- Jetzt dasselbe Fenster, aber im Haus von Team Dogi ---- */ -await seite.addStyleTag({ url: CREW_CSS_ADRESSE }); -await seite.waitForTimeout(400); -const nachher = await formAufnehmen(); - -melde(""); -melde("=== 3. Die Form bleibt gleich, der Grund nicht ==="); -ok(nachher.hausname === '"crew"', - `das Haus von Team Dogi ist angekommen (${nachher.hausname})`); -ok(nachher.fase === vorher.fase, `die Fase ist unveraendert (${nachher.fase})`); -ok(nachher.silhouette === vorher.silhouette, "die Silhouette der Kachel ist Zeichen fuer Zeichen dieselbe"); -ok(nachher.winkelMass === vorher.winkelMass, "die drei Eckwinkel haben dieselbe Groesse"); -ok(nachher.winkelLage === vorher.winkelLage, "und dieselbe Lage"); -ok(nachher.ebenen === 13, - `aus der leeren Ebene sind fuenf geworden: drei Sternebenen, zwei Nebel (${nachher.ebenen})`); -ok(nachher.schleier === ".30", `der Schleier laesst etwas mehr durch (${nachher.schleier})`); -ok(nachher.sternenfeld.includes("radial-gradient"), - "und das Sternenfeld steht als Bild da, nicht als none"); - -await inhaltAus(".kachel"); -const grundCrew = await punkte(kachelRahmen); -await inhaltAn(); -{ - const abstand = Math.max(...grundCrew.mittel.map((x, i) => Math.abs(x - grundAgentur.mittel[i]))); - ok(abstand >= 4, - `der Kachelgrund ist messbar ein anderer geworden (${grundAgentur.mittel} -> ${grundCrew.mittel})`); - ok(grundCrew.hell[grundCrew.anzahl - 1] > grundAgentur.hell[grundAgentur.anzahl - 1] * 1.5, - `und es gibt jetzt helle Punkte, die es vorher nicht gab (${grundAgentur.hellsterPunkt} -> ${grundCrew.hellsterPunkt})`); -} - -/* GEGENPROBE ZUM MESSGERAET. Nimmt man das Sternenfeld wieder weg, muss - der Grund zurueckspringen. Ohne diese Probe koennte die Messung oben - auch etwas ganz anderes gesehen haben -- die Farben haben sich beim - Hauswechsel ja ohnehin geaendert. */ -{ - await seite.addStyleTag({ content: ":root { --sternenfeld: none; }" }); - await seite.waitForTimeout(250); - await inhaltAus(".kachel"); - const ohne = await punkte(kachelRahmen); - await inhaltAn(); - ok(ohne.hell[ohne.anzahl - 1] < grundCrew.hell[grundCrew.anzahl - 1] * .8, - `ohne Sternenfeld verschwinden die hellen Punkte wieder (${grundCrew.hellsterPunkt} -> ${ohne.hellsterPunkt})`); - await seite.evaluate(() => { - const letzte = [...document.querySelectorAll("style")].pop(); - if (letzte && letzte.textContent.includes("--sternenfeld: none")) letzte.remove(); - }); - await seite.waitForTimeout(250); -} - -/* ======================================================================= - 4. NUR DIE KACHELN -- der Grund der Seite bleibt - ======================================================================= */ -melde(""); -melde("=== 4. Nur die Kacheln ==="); -{ - const zwischenraum = await seite.evaluate(() => { - /* Ein Fleck ZWISCHEN zwei Kacheln. Er gehoert der Seite, nicht - einer Kachel -- dort darf kein Stern stehen. */ - /* IRGENDZWEI, DIE NEBENEINANDER LIEGEN -- nicht die ersten zwei. - - Bis zum 16.09.2026 stand hier `kacheln[0]` und `kacheln[1]`. Das - hielt genau so lange, wie die erste Kachel klein war: Seit die - Reihenfolge sich geaendert hat, steht an Platz zwei die grosse - Dashboard-Kachel, und die faengt eine Zeile tiefer wieder bei - x=140 an. Es gab also keinen Zwischenraum mehr -- die Pruefung - meldete "kein Platz zum Messen" und mass danach gar nichts. - - Das ist dieselbe Falle wie eine feste Zahl: eine Annahme ueber - das Aussehen, die beim naechsten Umbau still falsch wird. Jetzt - wird gesucht, was die Frage beantwortet, statt sie zu stellen. */ - /* EIN FLECK, DER KEINE KACHEL BERUEHRT. - - Die Frage ist: Steht das Sternenfeld wirklich nur auf den - Kacheln? Bis zum 16.09.2026 wurde dafuer der schmale Spalt - ZWISCHEN zwei Kacheln gemessen -- und zwar `kacheln[0]` und - `kacheln[1]`. Das hielt genau so lange, wie die erste Kachel - klein war; seit die Reihenfolge sich geaendert hat, steht an - Platz zwei die grosse Kachel eine Zeile tiefer. Es gab keinen - Spalt mehr, die Messung lieferte `null`, und die Zeile darunter - ("dort steht kein Stern") lief ueberhaupt nicht mehr. Ein - uebersprungener Test beweist nichts. - - Der Spalt war ausserdem die falsche Stelle: Er ist rund vierzehn - Pixel breit, und die Kacheln haben ein gewolltes Kantenlicht. - Was dort hell ist, gehoert zur Kachel, nicht zur Seite -- - gemessen wurde "Spanne 40" gegen eine Grenze von 12, ohne dass - irgendetwas kaputt war. - - Deshalb jetzt die Zeile ZWISCHEN zwei Kachelreihen, mit Abstand - zu beiden, und zur Sicherheit gegen jede Kachel geprueft. Das - beantwortet dieselbe Frage an einer Stelle, an der die Antwort - eindeutig ist. */ - const felder = [...document.querySelectorAll(".kachel")] - .map((k) => k.getBoundingClientRect()); - if (felder.length < 2) return null; - - const unten = Math.max(...felder.map((f) => f.y + f.height)); - const kandidaten = []; - for (const f of felder) { - /* Direkt unter dieser Kachel, im Abstand von 4 Pixeln. */ - const y = f.y + f.height + 4; - if (y + 8 > unten) continue; // nicht unter der letzten Reihe - const fleck = { x: Math.round(f.x + f.width / 2 - 12), y: Math.round(y), - width: 24, height: 8 }; - const beruehrt = felder.some((g) => - fleck.x + fleck.width > g.x && fleck.x < g.x + g.width - && fleck.y + fleck.height > g.y && fleck.y < g.y + g.height); - if (!beruehrt) kandidaten.push(fleck); - } - return kandidaten[0] || null; - }); - ok(zwischenraum !== null, "ein Fleck ohne Kachel darunter ist gefunden"); - if (zwischenraum) { - /* DIE FRAGE IST NICHT "IST ES DORT RUHIG", SONDERN "LIEGT DORT DAS - STERNENFELD". - - Bis zum 16.09.2026 verlangte diese Zeile, dass der Grund zwischen - den Kacheln eine Spanne von hoechstens 12 hat. Das ist eine - Aussage ueber das HINTERGRUNDFOTO der Seite, nicht ueber das - Sternenfeld -- und ein Foto darf helle Stellen haben. Gemessen - wurden 27, ohne dass irgendetwas kaputt war. - - Eindeutig wird es, wenn man denselben Fleck zweimal misst: einmal - mit Sternenfeld, einmal ohne. Liegt es wirklich nur auf den - Kacheln, aendert sich dort NICHTS. Das beantwortet die Frage - unabhaengig davon, was auf dem Foto zu sehen ist. */ - const mit = await punkte(zwischenraum); - await seite.addStyleTag({ content: ":root { --sternenfeld: none; }" }); - await seite.waitForTimeout(250); - const ohne = await punkte(zwischenraum); - await seite.evaluate(() => { - const letzte = [...document.querySelectorAll("style")].pop(); - if (letzte && letzte.textContent.includes("--sternenfeld: none")) letzte.remove(); - }); - await seite.waitForTimeout(250); - - const abstand = Math.max(...mit.mittel.map((x, i) => Math.abs(x - ohne.mittel[i]))); - const spitze = Math.max(...mit.hellsterPunkt.map((x, i) => Math.abs(x - ohne.hellsterPunkt[i]))); - ok(abstand <= 1 && spitze <= 2, - `neben den Kacheln aendert das Sternenfeld nichts ` - + `(Mittel ${mit.mittel} -> ${ohne.mittel}, hellster ${mit.hellsterPunkt} -> ${ohne.hellsterPunkt})`); + await seite.screenshot({ path: "server/pruef-kachel-universum.png", fullPage: false }); + melde(" -- Bildschirmfoto: server/pruef-kachel-universum.png"); + } finally { + await browser.close(); } } -/* ======================================================================= - 5. MAN ERKENNT ALLES NOCH GUT - ======================================================================= */ -function kontrast(a, lb) { - const l = (p) => { - const f = p.map((x) => { const s = x / 255; return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4); }); - return .2126 * f[0] + .7152 * f[1] + .0722 * f[2]; - }; - const x = typeof a === "number" ? a : l(a); - return (Math.max(x, lb) + .05) / (Math.min(x, lb) + .05); -} -function leuchtkraft(p) { - const f = p.map((x) => { const s = x / 255; return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4); }); - return .2126 * f[0] + .7152 * f[1] + .0722 * f[2]; -} - -async function messen(waehler) { - const stelle = await seite.evaluate((w) => { - const e = document.querySelector(w); - if (!e) return null; - e.scrollIntoView({ block: "center" }); - const r = e.getBoundingClientRect(); - if (r.width < 8 || r.height < 6) return null; - /* Die Schriftfarbe wird GEZEICHNET, nicht gelesen: color-mix() - liefert getComputedStyle als "color(srgb …)", und daraus Ziffern - zu klauben ergibt Unsinn. Ein Canvas versteht jede Schreibweise. */ - const c = document.createElement("canvas"); - c.width = 1; c.height = 1; - const g = c.getContext("2d", { willReadFrequently: true }); - g.fillStyle = "#000000"; - g.fillStyle = getComputedStyle(e).color; - g.fillRect(0, 0, 1, 1); - const px = g.getImageData(0, 0, 1, 1).data; - e.dataset.messen = "ja"; - e.style.color = "transparent"; - return { x: Math.round(r.x), y: Math.round(r.y), - w: Math.max(8, Math.round(r.width)), h: Math.round(Math.min(r.height, 40)), - farbe: [px[0], px[1], px[2]] }; - }, waehler); - if (!stelle) return null; - await inhaltAus(waehler); - const hinten = await punkte({ x: stelle.x, y: stelle.y, width: stelle.w, height: stelle.h }); - await inhaltAn(); - await seite.evaluate(() => { - const e = document.querySelector('[data-messen="ja"]'); - if (e) { e.style.color = ""; delete e.dataset.messen; } - }); - const lt = leuchtkraft(stelle.farbe); - const unter = hinten.hell.filter((h) => kontrast(lt, h) < 4.5).length; - return { - farbe: stelle.farbe, - mittel: kontrast(lt, leuchtkraft(hinten.mittel)), - hellster: kontrast(lt, hinten.hell[hinten.anzahl - 1]), - anteil: 100 * unter / hinten.anzahl, - punkte: hinten.anzahl, - }; -} - -async function lesbar(name, waehler) { - const m = await messen(waehler); - if (m === null) { ok(false, `${name}: nicht gefunden (${waehler})`); return; } - ok(m.punkte > 200 && m.mittel >= 4.5 && m.anteil <= 1.5 && m.hellster >= 3.4, - `${name}: Mittel ${m.mittel.toFixed(2)} · unter 4,5 liegen ${m.anteil.toFixed(3)} % · hellster Punkt ${m.hellster.toFixed(2)} (${m.punkte} Bildpunkte)`); -} - melde(""); -melde("=== 5. Lesbarkeit auf der Startseite ==="); -for (const [name, w] of [ - ["Kachelname", ".kachel .kachel__name"], - ["Kachelzeile", ".kachel .kachel__unter"], - ["kleine Kachel, Name", '.kachel:not([data-gross="ja"]) .kachel__name'], - ["kleine Kachel, Zeile", '.kachel:not([data-gross="ja"]) .kachel__unter'], -]) await lesbar(name, w); - -/* GEGENPROBE: ein absichtlich zu dunkler Text MUSS durchfallen. Ohne - sie waere nicht bewiesen, dass diese Messung ueberhaupt "nicht in - Ordnung" sagen kann. */ -{ - await seite.evaluate(() => { - document.querySelector(".kachel .kachel__name").style.color = "#171326"; - }); - const schlecht = await messen(".kachel .kachel__name"); - ok(schlecht !== null && schlecht.mittel < 4.5 && schlecht.anteil > 50, - `absichtlich zu dunkler Text faellt durch (${schlecht?.mittel.toFixed(2)}:1, ${schlecht?.anteil.toFixed(1)} % unter der Grenze)`); - await seite.evaluate(() => { - document.querySelector(".kachel .kachel__name").style.color = ""; - }); -} - -/* GEGENPROBE ZUM ANTEIL: ein zu heller Nebel MUSS auffallen, auch wenn - der Mittelwert noch durchgeht. Das ist der Fall, fuer den diese Zahl - ueberhaupt gebaut wurde. */ -{ - await seite.addStyleTag({ content: ":root { --sternenfeld:" - + " radial-gradient(ellipse 90% 90% at 50% 50%, rgba(255,255,255,.95) 0%, transparent 85%);" - + " --sternenfeld-mass: 100% 100%; --sternenfeld-lage: 0 0; }" }); - await seite.waitForTimeout(300); - const grell = await messen(".kachel .kachel__unter"); - ok(grell !== null && grell.anteil > 1.5, - `ein zu heller Nebel faellt durch (${grell?.anteil.toFixed(1)} % unter der Grenze)`); - await seite.evaluate(() => { - const letzte = [...document.querySelectorAll("style")].pop(); - if (letzte && letzte.textContent.includes("ellipse 90% 90%")) letzte.remove(); - }); - await seite.waitForTimeout(300); -} - -melde(""); -melde("=== 6. Lesbarkeit auf der Teamseite ==="); -await seite.goto(BASIS + "/workspace/team.html", { waitUntil: "networkidle" }); -await seite.addStyleTag({ url: CREW_CSS_ADRESSE }); -await seite.waitForSelector(".t-kachel", { timeout: 15000 }); -await seite.waitForTimeout(900); -/* Zugeklappte Abschnitte messen nichts -- erst aufklappen, dann messen. */ -await seite.evaluate(() => { - document.querySelectorAll("details:not([open])").forEach((x) => { x.open = true; }); -}); -await seite.waitForTimeout(500); -{ - const da = await seite.evaluate(() => ({ - kachel: !!document.querySelector(".t-kachel"), - person: !!document.querySelector(".tperson"), - titel: !!document.querySelector(".t-kopf__titel"), - haus: getComputedStyle(document.documentElement).getPropertyValue("--haus-name").trim(), - })); - ok(da.kachel && da.titel && da.person && da.haus === '"crew"', - `die Teamseite ist gezeichnet und traegt das richtige Haus (${JSON.stringify(da)})`); -} -for (const [name, w] of [ - ["Abschnittstitel", ".t-kachel .t-kopf__titel"], - ["Personenname", ".tperson .tperson__name"], - ["Personenzeile (leisester Text im Haus)", ".tperson .tperson__unter"], -]) await lesbar(name, w); - -melde(""); -melde("=== 7. Nichts abgestuerzt ==="); -ok(konsole.length === 0, `keine Fehler im Browser (${konsole.slice(0, 3).join(" | ") || "keine"})`); - -await ctx.close(); -await browser.close(); -/* Der Server haelt die Datenbankdatei offen -- unter Windows laesst sie - sich dann nicht loeschen. Das ist kein Befund, sondern ein - Aufraeumrest im Temp-Ordner, und darf die Pruefung nicht rot machen. */ -try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } - -console.log(""); -console.log(`${geprueft} Pruefungen, ${fehler} Fehler`); -console.log(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG"); -process.exit(fehler === 0 ? 0 : 1); +melde(`${geprueft} Pruefungen, ${fehler} Fehler` + + (offen ? `, ${offen} nicht nachsehbar` : "")); +melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(fehler ? 1 : 0); diff --git a/tools/kachel-farben.mjs b/tools/kachel-farben.mjs index bf10ba40..a8069e3e 100644 --- a/tools/kachel-farben.mjs +++ b/tools/kachel-farben.mjs @@ -1,84 +1,72 @@ /* =================================================================== - Die 21 Kachelfarben -- gerechnet, nicht gewaehlt. + DIE KACHELFARBEN -- gerechnet, nicht gewaehlt. 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!!!!" + gleiche farben, ähneln sich sehr ... alle seine eigenen farben und so + dass sie sich nicht ähneln. sehr wichtig nicht ähneln!!!!" - 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: + 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." - Kleinster Abstand zweier Toene: 0,033 (Dateien gegen Creator) - Paare unter 0,05 (kaum trennbar): 21 von 210 - Spanne der Helligkeit: 0,002 + 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: - 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. + 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 - 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. + 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: #ff0000 - und #00ff00 sind rechnerisch gleich weit auseinander wie #000080 und - #0000ff, sehen aber voellig anders getrennt aus. + das Auge als Unterschied empfindet -- in RGB tut er das nicht. - DREI BEDINGUNGEN, die jede Farbe halten muss: + 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. - 2. AUGENSCHONEND (Hausregel). Farbstaerke gedeckelt -- satt ja, - Neon nein. Deshalb liegt die Obergrenze der Buntheit bei 0,17 - und nicht am Rand des Darstellbaren. + 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 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. + 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 - Die Ausgabe wird nach start.css uebertragen (Abschnitt "Kacheln"). - - ACHTUNG -- ZWEI TOENE SIND VON HAND GESETZT (Stand 09.09.2026). - - TON 5 (Schutz & Regeln) steht auf #ff2fd0. Filipe: "ich will dass - die kategorie eine farbe bekommt die extrem krass auffaellt. diese - kategorie ist naemlich seeeehr wichtig." Magenta ist der einzige - Ton, den es im Haus sonst nirgends gibt -- er faellt durch - Einzigartigkeit auf, nicht durch Helligkeit. Abstand zum naechsten - Nachbarn 0,1305 (Grenze 0,0973), Buntheit 0,276 (hoechste im Satz), - Kontrast 6,00:1. - - ACHTUNG -- TON 21 IST SEIT DEM 09.09.2026 VON HAND GESETZT. - Filipe wollte die Scout-Pipeline ausdruecklich in Knallrot - ("die farbe von dieser kategorie soll knall rot sein"). In start.css - steht dort #ff1f2e; dieses Werkzeug wuerde beim naechsten Lauf - wieder Rosa vorschlagen. - - NICHT EINFACH UEBERNEHMEN, sondern die Ausgabe fuer Ton 21 - verwerfen. Der Wunsch ist geprueft und nicht willkuerlich: #ff1f2e - liegt in OKLab 0,1228 von seinem naechsten Nachbarn entfernt -- - weiter als das engste vorhandene Paar im Satz (0,0973) -- und hat - 5,01:1 Kontrast. Wer den Satz neu rechnet, muss diese Farbe als - FESTEN Punkt vorgeben und die uebrigen zwanzig darum herum - verteilen; sonst ist der naechste Lauf eine stille Ruecknahme einer - ausdruecklichen Entscheidung. + node tools/kachel-farben.mjs --schreiben =================================================================== */ +import { readFileSync, writeFileSync } from "node:fs"; -/* ---------- Farbenlehre ------------------------------------------- */ +const CSS = "workspace/assets/css/start.css"; +const GRUND = "#0a121e"; -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); +/* ---------- Farbrechnung -------------------------------------------- */ -function oklabAusRgb(r, g, b) { +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); @@ -89,188 +77,230 @@ function oklabAusRgb(r, g, b) { ]; } -/** 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. */ +/** 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), 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 g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s; - 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 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 abstand = (x, y) => Math.hypot( - x.oklab[0] - y.oklab[0], x.oklab[1] - y.oklab[1], x.oklab[2] - y.oklab[2]); - -/* 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", "Zahlen", "LIVE", "Chat", "Schutz", "Community", "Reports", - "Kalender", "Bibliothek", "Steckbrief", "Personen", "Content", "Aufgaben", - "Technik", "Calls", "Start-Check", "Agentur", "Automationen", "Dateien", - "Creator-Profile", "Scout", -]; - -/* 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 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 = []; -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 }); +/* 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 ---- +/* ---------- 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. + gewaehlten weg ist), dann tauschen, solange es besser wird. - 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. */ + 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. */ -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); -} - -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); +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; - const probe = gewaehlt.slice(); probe[i] = k; - if (kleinster(probe) > jetzt + 1e-9) { gewaehlt = probe; verbessert = true; break; } + 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; } -/* ---------- Zuordnung: Wunschton, wo er ohne Verlust geht ---------- */ +const ANZAHL = NUMMERN.length; +console.log(`${ANZAHL} Farben gesucht, ${KANDIDATEN.length} Kandidaten im Raum.`); -const frei = gewaehlt.slice(); +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? ---------------- + + Die Nummern bleiben an ihren Kacheln -- eine Farbe gehoert zu einem + Bereich, nicht zu einem Platz im Raster. Damit die Aenderung nicht + groesser wird als noetig, bekommt jede Nummer die neue Farbe, die + ihrer alten am naechsten liegt: Wer "Chat" als Orange kennt, findet + es danach wieder als Orange. */ + +const frei = beste.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++; +for (const nr of NUMMERN) { + const alt = VORHER.get(nr); + let bester = 0, bestAbw = Infinity; + for (let i = 0; i < frei.length; i++) { + const d = abstand(frei[i].hex, alt); + if (d < bestAbw) { bestAbw = d; bester = i; } } + zuteilung.set(nr, frei.splice(bester, 1)[0]); +} -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`); +/* ---------- Bericht -------------------------------------------------- */ -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`); +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))); -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} */`); +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.`); -/* 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 (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.`); } -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/app.webmanifest b/workspace/app.webmanifest index 7a6d065e..87f0f929 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609180146", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609180254", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609180146", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609180254", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609180146", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609180254", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609180146", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609180254", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index e9117b12..d5a605de 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -883,10 +883,10 @@ body.start > main { position: relative; z-index: 1; } 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="1"] { --ton: #0c86af; } /* Dashboard */ +[data-ton="2"] { --ton: #fe5e82; } /* Zahlen */ +[data-ton="3"] { --ton: #ff996d; } /* LIVE */ +[data-ton="4"] { --ton: #c16302; } /* Chat */ /* TON 5 IST VON HAND GESETZT (09.09.2026, screen4). Filipe: "ich will dass die kategorie eine farbe bekommt die extrem @@ -920,22 +920,22 @@ body.start > main { position: relative; z-index: 1; } gewünscht ("alle kacheln auf der ganzen website sollen sein wie auf der startseite"). Was sie heraushebt, ist die Farbe, nicht eine Sonderform. */ -[data-ton="5"] { --ton: #ff2fd0; } /* Schutz & Regeln -- Wunsch: extrem auffällig */ -[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="5"] { --ton: #ff08d5; } /* Schutz & Regeln -- Wunsch: extrem auffällig */ +[data-ton="6"] { --ton: #8b8400; } /* Community */ +[data-ton="7"] { --ton: #098f71; } /* Reports */ +[data-ton="8"] { --ton: #ab68ff; } /* Kalender */ +[data-ton="9"] { --ton: #cace02; } /* Bibliothek */ +[data-ton="10"] { --ton: #a8ad11; } /* Steckbrief */ +[data-ton="11"] { --ton: #db8908; } /* Personen */ +[data-ton="12"] { --ton: #0ce6a3; } /* Content */ +[data-ton="13"] { --ton: #12b37e; } /* Aufgaben */ +[data-ton="14"] { --ton: #ffbd4a; } /* Technik */ +[data-ton="15"] { --ton: #01d7e3; } /* Calls */ +[data-ton="16"] { --ton: #19b2c2; } /* Start-Check */ +[data-ton="17"] { --ton: #a2c6fe; } /* Agentur */ +[data-ton="18"] { --ton: #aa99ff; } /* Automationen */ +[data-ton="19"] { --ton: #586dfe; } /* Dateien */ +[data-ton="20"] { --ton: #ff8fb4; } /* Creator-Profile */ /* TON 21 IST VON HAND GESETZT (09.09.2026, screen6). Filipe: "die farbe von dieser kategorie soll knall rot sein." @@ -953,7 +953,7 @@ body.start > main { position: relative; z-index: 1; } Kontrast auf dem dunklen Grund: 5,01:1 (noetig 4,5). Von sechs geprueften Rottoenen hat dieser den groessten Abstand UND genug Kontrast -- deshalb dieser und nicht der erstbeste. */ -[data-ton="21"] { --ton: #ff1f2e; } /* Scout-Pipeline -- Wunsch: knallrot */ +[data-ton="21"] { --ton: #f80442; } /* Scout-Pipeline -- Wunsch: knallrot */ /* TON 22 -- TEAM-LAGE (09.09.2026). @@ -970,13 +970,13 @@ body.start > main { position: relative; z-index: 1; } Warm und hell ist kein Zufall: Diese eine Kachel gehoert der Leitung. Zwischen den kuehlen Toenen der Arbeitsbereiche faellt ein warmer auf, ohne lauter zu sein. */ -[data-ton="22"] { --ton: #ffd166; } /* Team-Lage */ +[data-ton="22"] { --ton: #fee0b2; } /* Team-Lage */ /* Ton 23 (10.09.2026, Ideen-Board). NICHT nach Gefuehl gewaehlt: Die 22 vorhandenen Toene sind dicht besetzt, deshalb wurde der Abstand zu allen ausgerechnet. #5f8a9f hat mit 127 den zweitgroessten -- der groesste war ein Graubraun, und eine graue Kachel zwischen bunten sieht abgeschaltet aus. Gedeckt statt grell, wie alles hier. */ -[data-ton="23"] { --ton: #5f8a9f; } /* Ideen-Board */ +[data-ton="23"] { --ton: #07a7fd; } /* Ideen-Board */ /* Ton 24 (10.09.2026, Eingang). Wieder gerechnet statt gewaehlt, und bei 23 belegten Toenen wird es eng: Der Abstand im CIELAB-Raum zum @@ -989,7 +989,7 @@ body.start > main { position: relative; z-index: 1; } schlechtesten zu fokussieren. Kraeftiges Gruen kam auf 39,5, hat aber L* 81 und wuerde leuchten. Dieser Ton hat eine Leuchtdichte von 0,109 -- dunkel genug, um neben den anderen ruhig zu bleiben. */ -[data-ton="24"] { --ton: #8a20cf; } /* Eingang */ +[data-ton="24"] { --ton: #d21afd; } /* Eingang */ /* TON 25 -- Rueckmeldung (10.09.2026). @@ -1008,7 +1008,7 @@ body.start > main { position: relative; z-index: 1; } Kontrast auf dem dunklen Grund: 8,9 zu 1. Und die Bedeutung passt: Gruen ist im Haus die Farbe von "laeuft" -- eine Rueckmeldung soll nicht nach Beschwerde aussehen. */ -[data-ton="25"] { --ton: #43c95e; } /* Rückmeldung */ +[data-ton="25"] { --ton: #72d512; } /* Rückmeldung */ /* TON 26 UND 27 (11.09.2026) -- und hier wird die Rechnung ehrlich: @@ -1027,8 +1027,8 @@ body.start > main { position: relative; z-index: 1; } Wenn eine achtundzwanzigste Kachel kommt, ist das der Zeitpunkt, an dem die Kacheln Gruppenfarben brauchen statt Einzelfarben -- nicht der, an dem man den naechsten Ton dazwischenquetscht. */ -[data-ton="26"] { --ton: #7fd4a8; } /* Entwicklung */ -[data-ton="27"] { --ton: #8f7bd6; } /* Talente */ +[data-ton="26"] { --ton: #07fff1; } /* Entwicklung */ +[data-ton="27"] { --ton: #ff6bfa; } /* Talente */ /* ===================================================================== DIE SIEBEN TOENE DES TREFFS (11.09.2026) @@ -1054,13 +1054,13 @@ body.start > main { position: relative; z-index: 1; } Hand gewaehlte Farbfamilie im selben Blau bei 12,6. Die Rechnung war also nicht Genauigkeitsspielerei, sondern der Unterschied zwischen unterscheidbar und nicht. */ -[data-ton="28"] { --ton: #8e784f; } /* Regeln & Hilfe */ -[data-ton="29"] { --ton: #8e5b77; } /* Highlights */ -[data-ton="30"] { --ton: #b2bc78; } /* Wunschliste */ -[data-ton="31"] { --ton: #656a9e; } /* Was ansteht */ -[data-ton="32"] { --ton: #668e6e; } /* Mitmachen */ -[data-ton="33"] { --ton: #cc9451; } /* Anschlagbrett */ -[data-ton="34"] { --ton: #cc92c6; } /* Der Treff */ +[data-ton="28"] { --ton: #069511; } /* Regeln & Hilfe */ +[data-ton="29"] { --ton: #ee068b; } /* Highlights */ +[data-ton="30"] { --ton: #a7ff98; } /* Wunschliste */ +[data-ton="31"] { --ton: #fe5a11; } /* Was ansteht */ +[data-ton="32"] { --ton: #18b803; } /* Mitmachen */ +[data-ton="33"] { --ton: #f4acff; } /* Anschlagbrett */ +[data-ton="34"] { --ton: #f9d7ff; } /* Der Treff */ /* DER 35. TON (11.09.2026) — für „Wer sieht was". @@ -1086,18 +1086,18 @@ body.start > main { position: relative; z-index: 1; } mit einer anderen geteilt. Zwei Dinge in einer Farbe heißt, dass die Farbe nichts mehr sagt — und pruef-start-ansicht zählt genau das nach ("34 Farben auf 35 Kacheln" war ihre Meldung). */ -[data-ton="35"] { --ton: #2f2f7f; } /* Wer sieht was */ +[data-ton="35"] { --ton: #b4f1fe; } /* Wer sieht was */ /* Der 36., nach derselben Rechnung: #7c2771, Abstand 29,7 zu den beiden nächsten (#8e5b77 „Highlights", #2f2f7f „Wer sieht was"). Auch das liegt über dem schwächsten Paar der vorhandenen Töne (24,9). */ -[data-ton="36"] { --ton: #7c2771; } /* Meldungen & Maßnahmen */ +[data-ton="36"] { --ton: #eeef08; } /* Meldungen & Maßnahmen */ /* Der 37., nach derselben Rechnung: #46762e, Abstand 25,6 zum nächsten Nachbarn — knapp über dem schwächsten Paar der ursprünglichen 34 (24,9). Bei 37 Tönen in einem augenschonenden Ausschnitt (kein Rot, keine Neonsättigung) ist das die Grenze des Machbaren; wird es enger, ist die nächste Frage nicht „noch ein Ton", sondern „braucht diese Kachel wirklich eine eigene Farbe". */ -[data-ton="37"] { --ton: #46762e; } /* Wie geht's dir? */ +[data-ton="37"] { --ton: #61ff09; } /* Wie geht's dir? */ /* 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 @@ -1113,10 +1113,140 @@ body.start > main { position: relative; z-index: 1; } --my: 0%; position: relative; border-radius: 18px; + + /* ===================================================================== + DAS UNIVERSUM IN JEDER KACHEL (17.09.2026) + + Filipe: „der hintergrund von allen kacheln haben wir richtung + universum mäßig gemacht, aber bin noch nicht ganz zufrieden, es + soll noch mehr viel mehr nach universum sein. nicht einfach so paar + weiße punkte sondern richtig geil hochwertiger geiler universum." + + Er hat recht, und der Grund war schnell gefunden: Das gebaute + Universum (Nebel, Sternenfeld, driftende Chilis) lag auf GENAU + EINER Kachel -- der Willkommenskachel. Alle anderen hatten einen + Verlauf und sonst nichts. + + WARUM ES IM HINTERGRUND STEHT UND NICHT IN EINEM ELEMENT: Beide + Pseudo-Elemente der Kachel sind vergeben (::before die Leuchtschiene + oben, ::after Glanz und Zeigerlicht), und ein neues Element müsste + an jeder Stelle nachgetragen werden, die Kacheln baut -- start.js, + bereiche.js und jede künftige. Eine Ebene, die man vergessen kann, + wird vergessen. Als Hintergrund gilt sie überall, wo `.kachel` + gilt, ohne eine einzige Zeile JavaScript. + + DER NEBEL TRÄGT DIE FARBE DER KACHEL. Das ist der eigentliche + Gewinn gegenüber der Willkommenskachel: Dort sind die Wolken fest + rot-blau-lila, hier nimmt jede Kachel ihren eigenen Ton. Achtund- + zwanzig Kacheln sind damit achtundzwanzig verschiedene Nebel -- + dieselbe Arbeit, die die Farbpalette leistet, nur in der Fläche. + + SIEBEN EBENEN, VON HINTEN NACH VORN: + 1-2 Zwei Sternenschichten, klein und fern (0,8-1,1 px) + 3-4 Zwei Schichten mittlerer Sterne, einer davon im Kachelton + 5 Wenige nahe, hellere Sterne + 6-7 Zwei Nebelwolken im Kachelton, dazu der Grundverlauf + + Die Kachelgrößen 89 / 137 / 191 / 223 / 317 px haben kein + gemeinsames Vielfaches unter 1200 -- das Feld wiederholt sich + damit erst jenseits jeder Kachelbreite, und genau das unterscheidet + ein Sternenfeld von einer Tapete. (Dieselbe Überlegung wie bei der + Willkommenskachel, dort nachzulesen.) + + KEINE BEWEGUNG. Auf der Willkommenskachel driftet das Feld in + 240 s -- eine Kachel, die stundenlang im Bild steht, verträgt das. + Achtundzwanzig driftende Felder auf einer Seite wären achtund- + zwanzig Dauerläufer auf der Grafikkarte, und am Handy merkt man + das sofort. Bewegung bringt hier der Glanz, der dem Zeiger folgt + (::after) -- er kostet nur etwas, während man ihn benutzt. + + LESBARKEIT GEHT VOR. Die Sterne liegen zwischen 6 und 14 Prozent + Deckkraft, die Nebel bei 5 bis 9. Man sieht einen Raum, man liest + keine Punkte. Der Text darüber behält seinen Kontrast; dass die + Farben ihn halten, prüft pruef-css-klassen ohnehin bei jedem Lauf. + ===================================================================== */ background: + /* ---- NAHE STERNE ------------------------------------------------ + Gross, hell, spaerlich. Jeder mit einem Hof (der zweite + Farbstopp), damit er leuchtet statt zu stanzen -- das ist der + Unterschied zwischen einem Stern und einem weissen Pixel. + + DEUTLICH HELLER ALS IM ERSTEN ANLAUF (17.09., nach dem ersten + Bildschirmfoto). Dort lagen sie bei 6 bis 14 Prozent und waren + aus der Naehe nicht zu sehen -- genau Filipes „paar weisse + punkte". Der Grund steht in der Kachel selbst: `--flaeche` ist + halbdurchsichtig, darunter liegt das Buehnenbild. Ein Sternenfeld + muss sich hier gegen ein FOTO durchsetzen, nicht gegen Schwarz. */ + radial-gradient(2.2px 2.2px at 47px 118px, rgba(255, 255, 255, .92), rgba(255, 255, 255, .22) 42%, transparent 100%), + radial-gradient(1.9px 1.9px at 263px 61px, rgba(226, 240, 255, .78), rgba(226, 240, 255, .18) 45%, transparent 100%), + radial-gradient(2.0px 2.0px at 118px 191px, color-mix(in srgb, var(--ton) 92%, #ffffff), transparent 100%), + + /* ---- MITTLERE STERNE -------------------------------------------- + Die Masse des Feldes. Eine Schicht im Kachelton: dadurch sieht + jedes Universum anders aus, ohne bunt zu werden. */ + radial-gradient(1.4px 1.4px at 22px 31px, rgba(255, 255, 255, .62), transparent 100%), + radial-gradient(1.5px 1.5px at 133px 74px, color-mix(in srgb, var(--ton) 78%, transparent), transparent 100%), + radial-gradient(1.3px 1.3px at 201px 138px, rgba(214, 234, 255, .55), transparent 100%), + radial-gradient(1.4px 1.4px at 74px 166px, color-mix(in srgb, var(--ton) 62%, transparent), transparent 100%), + + /* ---- FERNER STAUB ----------------------------------------------- + Klein und dicht. Er fuellt die Luecken, damit zwischen den + hellen Sternen kein leeres Schwarz steht -- ein Sternenhimmel + ohne Staub sieht aus wie aufgeklebte Punkte. */ + radial-gradient(0.9px 0.9px at 104px 96px, rgba(255, 255, 255, .42), transparent 100%), + radial-gradient(0.8px 0.8px at 31px 140px, rgba(190, 220, 250, .36), transparent 100%), + radial-gradient(0.9px 0.9px at 168px 44px, rgba(255, 255, 255, .34), transparent 100%), + radial-gradient(0.8px 0.8px at 57px 83px, rgba(226, 240, 255, .30), transparent 100%), + radial-gradient(0.9px 0.9px at 149px 23px, rgba(255, 255, 255, .28), transparent 100%), + + /* ---- DIE MILCHSTRASSE ------------------------------------------- + Ein schraeger Schleier von unten links nach oben rechts. Er ist + das, was ein Sternenfeld von einem Universum unterscheidet: + Tiefe kommt nicht von mehr Punkten, sondern davon, dass die + Punkte in etwas liegen. + + Sehr weich und sehr blass -- er soll die Richtung geben, nicht + auffallen. */ + linear-gradient(118deg, + transparent 18%, + rgba(255, 255, 255, .055) 38%, + color-mix(in srgb, var(--ton) 14%, transparent) 50%, + rgba(255, 255, 255, .045) 62%, + transparent 82%), + + /* ---- DIE NEBEL -------------------------------------------------- + Zwei Wolken im Ton der Kachel, diagonal versetzt, dazu ein + kuehler Gegenpol. Achtundzwanzig Kacheln sind damit + achtundzwanzig verschiedene Nebel -- dieselbe Arbeit, die die + Farbpalette leistet, nur in der Flaeche. */ + radial-gradient(62% 70% at 14% 8%, color-mix(in srgb, var(--ton) 55%, transparent), transparent 62%), + radial-gradient(52% 58% at 30% 86%, color-mix(in srgb, var(--ton) 34%, transparent), transparent 66%), + radial-gradient(80% 66% at 92% 96%, color-mix(in srgb, var(--ton) 26%, transparent), transparent 68%), + /* EIN KUEHLER GEGENPOL. Ein Nebel aus einer Farbe ist ein + Farbfleck; erst ein zweiter, kaelterer Ton macht daraus einen + Raum mit Ferne. Dieselbe Ueberlegung wie bei der + Willkommenskachel, wo Rot und Babyblau sich gegenueberstehen. */ + radial-gradient(56% 64% at 76% 18%, rgba(96, 158, 232, .16), transparent 64%), + /* DIE VIGNETTE. Aussen dunkler als innen -- ohne sie ist die + Kachel eine gleichmaessig helle Flaeche, mit ihr ein Ausschnitt + aus etwas Groesserem. Das ist der billigste Tiefeneindruck, den + es gibt, und der wirksamste. */ + radial-gradient(120% 120% at 50% 45%, transparent 38%, rgba(3, 5, 12, .55) 100%), + + /* ---- DER GRUND -------------------------------------------------- + Dunkler als frueher: Das Universum braucht Schwarz, sonst + leuchten die Sterne gegen ein Foto an und verlieren. Die Kachel + bleibt durchscheinend -- nur eben weniger. */ linear-gradient(155deg, - color-mix(in srgb, var(--ton) 26%, var(--flaeche)), - var(--flaeche) 62%); + color-mix(in srgb, var(--ton) 24%, var(--flaeche)), + color-mix(in srgb, #05070e 55%, var(--flaeche)) 58%); + background-size: + 317px 317px, 223px 223px, 271px 271px, + 137px 137px, 191px 191px, 233px 233px, 163px 163px, + 89px 89px, 137px 137px, 223px 223px, 107px 107px, 179px 179px, + 100% 100%, + 100% 100%, 100% 100%, 100% 100%, 100% 100%, 100% 100%, + 100% 100%; border: 1px solid var(--rand); /* Die feine helle Linie an der Oberkante. Ein Pixel, kaum sichtbar -- aber die Kachel wirkt dadurch von oben beleuchtet statt aufgeklebt. diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 6c922bfc..54c4b7df 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - + @@ -291,10 +291,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 41f93e37..4fce7693 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -168,10 +168,10 @@ - - - - - + + + + + diff --git a/workspace/befinden.html b/workspace/befinden.html index f815200e..e3ff2c4e 100644 --- a/workspace/befinden.html +++ b/workspace/befinden.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -105,9 +105,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index d858bf71..a9d5ae15 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,24 +12,24 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - - + + - + @@ -243,14 +243,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/bewerben.html b/workspace/bewerben.html index 390cb350..4cfa54f8 100644 --- a/workspace/bewerben.html +++ b/workspace/bewerben.html @@ -7,17 +7,17 @@ - - - - - - - - - + + + + + + + + + - + @@ -107,10 +107,10 @@ - - - - - + + + + + diff --git a/workspace/bewerbungen.html b/workspace/bewerbungen.html index fe19a1ef..5109c6b3 100644 --- a/workspace/bewerbungen.html +++ b/workspace/bewerbungen.html @@ -7,16 +7,16 @@ - - - - - - - - + + + + + + + + - + @@ -80,10 +80,10 @@ - - - - - + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 92fec800..f4a53088 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -89,11 +89,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index ced5105c..147ba06a 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,26 +12,26 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -341,10 +341,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index c3669d95..19e9362b 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -179,12 +179,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index 2978719a..5496adfc 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -36,15 +36,15 @@ - - - + + + - + @@ -339,6 +339,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index 375f9777..3b736bc1 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609180146", + "src": "/assets/img/app-symbole/crew-192.png?v=202609180254", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609180146", + "src": "/assets/img/app-symbole/crew-512.png?v=202609180254", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609180146", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609180254", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609180146", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609180254", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index 9a2c393c..f1a80f27 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -108,10 +108,10 @@ - - - - - + + + + + diff --git a/workspace/entwicklung.html b/workspace/entwicklung.html index b255719b..864910c1 100644 --- a/workspace/entwicklung.html +++ b/workspace/entwicklung.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -102,9 +102,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index 6c4461ff..2ae3d6be 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index f632d823..164ed2df 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -377,11 +377,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index c05da9e8..47fe5a24 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -444,10 +444,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 44d14b41..c687ceb4 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -423,10 +423,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index a2b4bca8..ace936cf 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -214,11 +214,11 @@ - - - - - - + + + + + + diff --git a/workspace/rechte.html b/workspace/rechte.html index e1ab0a47..02768c54 100644 --- a/workspace/rechte.html +++ b/workspace/rechte.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -134,9 +134,9 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - + + + + diff --git a/workspace/report.html b/workspace/report.html index b015a5da..2989fa9d 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -106,11 +106,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 1e36911b..3211bcd2 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -173,10 +173,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index 1b0e2a46..8fb2dbb5 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,21 +12,21 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + - + @@ -674,10 +674,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 844985c2..f9cc16ef 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -85,11 +85,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 1577bad2..a612408b 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -146,11 +146,11 @@ - - - - - - + + + + + + diff --git a/workspace/talente.html b/workspace/talente.html index 1e2d48ea..fe8b95e9 100644 --- a/workspace/talente.html +++ b/workspace/talente.html @@ -7,18 +7,18 @@ - - - - - - + + + + + + - - - + + + @@ -111,9 +111,9 @@ - - - - + + + + diff --git a/workspace/team.html b/workspace/team.html index cfedaf63..2795481d 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + @@ -110,10 +110,10 @@ - - - - - + + + + + diff --git a/workspace/teamlage.html b/workspace/teamlage.html index 4c626ed7..58303ea2 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/teilen.html b/workspace/teilen.html index db95a177..8818971e 100644 --- a/workspace/teilen.html +++ b/workspace/teilen.html @@ -7,13 +7,13 @@ - - - - - - - + + + + + + + @@ -112,9 +112,9 @@ - - - - + + + + diff --git a/workspace/treff-moderation.html b/workspace/treff-moderation.html index 8f4e1d28..c97cbae3 100644 --- a/workspace/treff-moderation.html +++ b/workspace/treff-moderation.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -126,9 +126,9 @@ - - - - + + + + diff --git a/workspace/treff-regeln.html b/workspace/treff-regeln.html index a914916c..df9d685c 100644 --- a/workspace/treff-regeln.html +++ b/workspace/treff-regeln.html @@ -7,18 +7,18 @@ - - - - - - - + + + + + + + - + @@ -211,10 +211,10 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 42ae2038..aa3896de 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,22 +12,22 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -149,11 +149,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 958abf3f..817c15aa 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -190,10 +190,10 @@ - - - - - + + + + +