diff --git a/server/pruef-backstage-import.mjs b/server/pruef-backstage-import.mjs index 70070e71..3f27e73d 100644 --- a/server/pruef-backstage-import.mjs +++ b/server/pruef-backstage-import.mjs @@ -40,9 +40,26 @@ const BASIS = `http://127.0.0.1:${PORT}`; const ordner = mkdtempSync(join(tmpdir(), "ws-backstage-")); const DB = join(ordner, "workspace.db"); const jetzt = new Date().toISOString(); -const heute = new Date().toISOString().slice(0, 10); -const gestern = new Date(Date.now() - 86400000).toISOString().slice(0, 10); -const morgen = new Date(Date.now() + 86400000).toISOString().slice(0, 10); +/* HEUTE IST DER TAG, DEN DER SERVER MEINT -- nicht der in Greenwich. + 16.09.2026, 00:40 Berlin: `toISOString()` sagte "2026-09-15", der + Server (heuteLokal(), Ortszeit) sagte "2026-09-16". Damit war das + hier berechnete "morgen" in Wahrheit HEUTE, und die Gegenprobe + "ein Datum in der Zukunft wird abgelehnt" schlug fehl -- an der + Zeitzone, nicht am Code. Zwischen Mitternacht und zwei Uhr morgens + lag diese Pruefung also taeglich zwei Stunden lang falsch. + + Eine Zeitbombe genau der Sorte, die schon einmal notiert wurde + (Shop, 06.09.2026): Die Wanduhr darf Eingabe sein, nie Annahme. + ALLE DREI WERDEN AUS EINER STELLE ABGELEITET -- drei eigene + Rechnungen waeren drei Gelegenheiten, wieder auseinanderzulaufen. */ +const tagLokal = (versatz) => { + const d = new Date(Date.now() + versatz * 86400000); + const p2 = (n) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())}`; +}; +const heute = tagLokal(0); +const gestern = tagLokal(-1); +const morgen = tagLokal(1); let gemacht = 0, fehler = 0; const ok = (b, text) => { @@ -782,6 +799,135 @@ console.log("\n=== Die Seite ==="); `und die Spanne groesser als ein Nebenwert (${groessen.spanne} px)`); ok(/\d/.test(groessen.text), `die Hauptzahl traegt Diamanten ("${groessen.text}")`); + /* ---- "11 VON 13 TAGEN LIVE" (16.09.2026) ------------------------ + Die Spalte "Gültige LIVE-Gehen-Tage" steht seit dem 14.09. in + dieser Pruefdatei -- und wurde die ganze Zeit weggeworfen, weil + `spaltenRaten` kein Muster dafuer hatte. Die Pruefung hat den Wert + zwar GESCHICKT, aber nie nachgesehen, ob er ankommt. + + GEMESSEN WIRD DER ANTEIL IN PIXELN, nicht das Vorhandensein des + Streifens: Ein Streifen, der immer voll ist, zeigt keine Zahl, + sondern eine Dekoration. 11 von 13 sind 85 %. */ + const lauf = await seite.evaluate(() => { + const bahn = document.querySelector(".spanne__lauf-bahn"); + const teil = document.querySelector(".spanne__lauf-teil"); + if (!bahn || !teil) return null; + return { + anteil: Math.round((teil.getBoundingClientRect().width + / bahn.getBoundingClientRect().width) * 100), + text: document.querySelector(".spanne__lauf-text")?.textContent || "", + }; + }); + ok(!!lauf, "der Streifen der gueltigen Tage steht auf der Karte"); + ok(lauf && lauf.text.includes("11 von 13 Tagen live"), + `er nennt die Zahl im Klartext ("${lauf?.text}")`); + ok(lauf && Math.abs(lauf.anteil - 85) <= 2, + `und ist zu ${lauf?.anteil} % gefuellt (11 von 13 = 85 %)`); + + /* ---- DER EIGENTLICHE WUNSCH: LESBARKEIT (16.09.2026) ------------ + Filipe: "ich will dass es auch seeeehr gut erkennbar ist, genau + wie der text drüber." + + Hinter dem Inhalt dieser Seite liegt die Buehne -- ein Foto mit + einem HELLROTEN Berg. Ueberschrift und Erklaersatz standen bis + heute direkt darauf. + + GEMESSEN WIRD AN ECHTEN BILDPUNKTEN, nicht an der Farbangabe im + Stil: Die weiss nichts davon, was hinter ihr liegt. Das Foto ist + stark rot, das Innenglas blaeulich-dunkel (#0b111c: R=11, B=28) -- + ein Bildpunkt mit R deutlich ueber B kann dort also nur vom Foto + stammen. + + WAS BEIM ERSTEN ANLAUF SCHIEFGING, und es ist lehrreich: Die + Messung meldete zehn rote Bildpunkte -- und hatte recht. Sie kamen + aber nicht vom Foto, sondern vom ANSCHLAG des Typenschilds, den + drei roten Kerben, die ich selbst dorthin gesetzt habe. Eine + Messung, die "alles Rote ist das Foto" annimmt, misst irgendwann + die eigene Gestaltung. Der Anschlag wird deshalb ausgespart -- und + zwar mit seiner EIGENEN Breite aus dem Stil, nicht mit einer + abgeschriebenen Zahl. */ + const roteBildpunkte = async (b64) => { + const bild = new Image(); + bild.src = "data:image/png;base64," + b64; + await bild.decode(); + const c = document.createElement("canvas"); + c.width = bild.width; c.height = bild.height; + const g = c.getContext("2d"); + g.drawImage(bild, 0, 0); + const skala = bild.width / window.innerWidth; + + const schild = document.querySelector(".spannen__schild"); + const satz = document.querySelector(".spannen__satz"); + if (!schild || !satz) return null; + + /* Der Anschlag ist ein ::before und hat kein eigenes Rechteck. + Seine Breite und der Abstand dahinter stehen aber im Stil -- + daraus wird die Stelle abgeleitet, ab der die Zeile beginnt. */ + const vor = getComputedStyle(schild, "::before"); + const st = getComputedStyle(schild); + const versatz = (parseFloat(vor.width) || 0) + (parseFloat(st.columnGap) || 0) + 2; + + const sr = schild.getBoundingClientRect(); + const zr = satz.getBoundingClientRect(); + const felder = [ + { name: "Ueberschrift", l: sr.left + versatz, t: sr.top, w: sr.width - versatz, h: sr.height }, + { name: "Erklaersatz", l: zr.left, t: zr.top, w: zr.width, h: zr.height }, + ]; + + let punkte = 0, rot = 0; + for (const f of felder) { + const w = Math.max(1, Math.round(f.w * skala)); + const h = Math.max(1, Math.round(f.h * skala)); + const p = g.getImageData(Math.round(f.l * skala), Math.round(f.t * skala), w, h).data; + for (let i = 0; i < p.length; i += 4) { + punkte++; + if (p[i] > p[i + 2] + 20) rot++; + } + } + return { punkte, rot }; + }; + + const durchsicht = await seite.evaluate(roteBildpunkte, + (await seite.screenshot({ type: "png" })).toString("base64")); + + ok(durchsicht && durchsicht.punkte > 5000, + `hinter Ueberschrift und Erklaersatz wurden ${durchsicht?.punkte} Bildpunkte abgetastet`); + ok(durchsicht && durchsicht.rot === 0, + `kein einziger davon traegt das Rot des Fotos (${durchsicht?.rot})`); + + /* Das Bild der fertigen Tafel -- MIT Glas, also so, wie Filipe sie + sieht. Es entsteht hier und nicht am Ende: Am Ende ist ein anderer + Creator geoeffnet, dessen Zeitraum keine gueltigen Tage hat. */ + if (process.env.SCHIRM) { + await seite.locator("#spannen-block").screenshot( + { path: join(HIER, "pruef-spannen.png") }); + } + + /* GEGENPROBE -- und sie ist hier das Wichtigste. + + "Null rote Bildpunkte" koennte auch heissen, dass gar kein Foto + dahinterliegt, dass das Fenster zu klein ist oder dass ich die + falsche Stelle abtaste. Also wird das Innenglas fuer einen + Augenblick weggenommen und werden DIESELBEN Stellen mit + DEMSELBEN Abtaster noch einmal gemessen: Kommt das Rot dann durch, + war es vorher das Glas, das es aufgehalten hat -- und die Messung + kann ueberhaupt Rot sehen. + + Genau die Sorte Pruefung, die ohne Gegenprobe jahrelang gruen + bleibt und nichts beweist. */ + await seite.addStyleTag({ content: + ".spannen-tafel { background: none !important; }" + + ".spannen-tafel::before { display: none !important; }" + + ".spannen, .spanne { background: none !important; }" }); + await seite.waitForTimeout(250); + const ohneGlas = await seite.evaluate(roteBildpunkte, + (await seite.screenshot({ type: "png" })).toString("base64")); + ok(ohneGlas && ohneGlas.rot > 200, + `ohne das Innenglas kommt das Foto sofort durch (${ohneGlas?.rot} rote Bildpunkte) ` + + "-- die Messung kann Rot also sehen"); + await seite.reload({ waitUntil: "networkidle" }); + await seite.waitForTimeout(1500); + /* ---- EINE WOCHE OHNE EINEN EINZIGEN TAG (14.09.2026) ------------ Filipe: "wieso werden die zahlen oben nicht ausgefuellt von der excel datei?" @@ -851,11 +997,6 @@ console.log("\n=== Die Seite ==="); "mit einem einzigen Tag sind die Karten wieder da"); - if (process.env.SCHIRM) { - await seite.locator("#spannen-block").screenshot( - { path: join(HIER, "pruef-spannen.png") }); - } - await seite.click("#netz-zu").catch(() => {}); await seite.waitForTimeout(300); /* Gemessen wird SATZ FUER SATZ, nicht der Block. Ein Block, der diff --git a/server/pruef-xlsx.mjs b/server/pruef-xlsx.mjs index 40528572..9368c34f 100644 --- a/server/pruef-xlsx.mjs +++ b/server/pruef-xlsx.mjs @@ -201,6 +201,30 @@ console.log("\n=== Vom Excel bis zur Spaltenzuordnung ==="); `"${name}" ${sollTreffen ? "ist" : "ist NICHT"} die Datumsspalte (erkannt: ${trifft})`); } + /* ---- UND WO LANDET SIE DANN? (16.09.2026) ---------------------- + Die Gegenprobe oben sagte nur, dass "Gültige LIVE-Gehen-Tage" + KEINE Datumsspalte ist. Das war richtig -- und unvollständig: Die + Spalte hatte danach ueberhaupt keinen Empfaenger mehr und lief ins + Leere. Datenbankspalte, Anzeigefeld und Importzeile waren da, nur + das Muster fehlte. + + DAS IST DIE GENAUE SORTE FEHLER, DIE EINE GEGENPROBE ALLEIN NICHT + FINDET: "trifft das Falsche nicht" ist kein Beweis dafuer, dass es + das Richtige trifft. Deshalb steht beides jetzt nebeneinander. */ + ok(tabelle[0][zu.gueltige_tage] === "Gültige LIVE-Gehen-Tage", + `"Gültige LIVE-Gehen-Tage" landet als gueltige_tage (Spalte ${zu.gueltige_tage})`); + ok(zahlLesen(tabelle[1][zu.gueltige_tage]) === 11, + `und traegt die Zahl ${zahlLesen(tabelle[1][zu.gueltige_tage])}`); + for (const [name, soll] of [ + ["Gültige LIVE-Gehen-Tage", true], ["Valid go LIVE days", true], + ["Tage seit dem Beitritt", false], ["Livestreams", false], + ["Diamanten", false], ["LIVE-Dauer", false], + ]) { + const trifft = spaltenRaten([name]).gueltige_tage === 0; + ok(trifft === soll, + `"${name}" ${soll ? "ist" : "ist NICHT"} die Spalte der gueltigen Tage (erkannt: ${trifft})`); + } + /* Und die Werte, die dahinter herauskommen. */ ok(zahlLesen(tabelle[1][zu.diamanten]) === 12480, `"12.480" wird zu ${zahlLesen(tabelle[1][zu.diamanten])} und nicht zu 12,48`); diff --git a/server/workspace-leistung.js b/server/workspace-leistung.js index acc96fd3..54f46c1d 100644 --- a/server/workspace-leistung.js +++ b/server/workspace-leistung.js @@ -762,6 +762,27 @@ export function spaltenRaten(kopf) { "Daten" die englische Silbe "date" steckt. Auf Zufall soll das nicht beruhen. */ tag: /datum|date|zeitraum|^\s*tage?\s*$|^\s*day\s*$/i, + /* "Gültige LIVE-Gehen-Tage" (16.09.2026). + + DIESE SPALTE STEHT IN JEDER BACKSTAGE-AUSGABE UND WURDE BIS + HEUTE WEGGEWORFEN. Die Datenbank hat die Spalte, die Anzeige hat + das Feld, der Import hat die Zeile -- nur hier fehlte das + Muster, und damit blieb `gueltige_tage` auf dem ganzen Weg leer. + Aufgefallen ist es nicht beim Lesen des Codes, sondern beim + Nachsehen, warum "Gültige Tage" auf der Karte nie erschien. + + WARUM ES VORHER NICHT AUFFIEL: Bis zum 14.09. hat das Muster + `tag: /.../` mit einem blossen `/tag/` genau diese Spalte + gefressen und als DATUMSSPALTE verbucht (Wert: "11"). Als das + repariert wurde, fiel die Spalte durchs Raster -- die Reparatur + hat den falschen Empfänger entfernt, aber keinen richtigen + bestellt. + + Es trifft die deutsche und die englische Schreibweise. Kein + anderes Muster oben oder unten greift daneben: "dauer_min" + verlangt `live` ZUSAMMEN mit `zeit`, und `tag` nur ganze + Wörter. */ + gueltige_tage: /g(?:ü|ue)ltig.*tag|valid.*day|live.*geh|go.*live.*day/i, diamanten: /diamant|diamond/i, dauer_min: /dauer|duration|minut|zeit.*live|live.*zeit/i, zuschauer_avg: /(durchschnitt|avg|average|⌀|ø).*(zuschauer|viewer)|(zuschauer|viewer).*(durchschnitt|avg)/i, diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 1d7ee5e0..780bc775 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609151738", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609160059", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609151738", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609160059", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609151738", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609160059", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609151738", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609160059", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/leistung.css b/workspace/assets/css/leistung.css index 081f0584..b1b335a4 100644 --- a/workspace/assets/css/leistung.css +++ b/workspace/assets/css/leistung.css @@ -432,56 +432,218 @@ .dialog--breit .neu { max-height: min(86vh, 900px); overflow-y: auto; } /* ===================================================================== - ZEITRÄUME AUS BACKSTAGE — zweite Fassung (14.09.2026) + ZEITRÄUME AUS BACKSTAGE — dritte Fassung (16.09.2026) - Filipe zur ersten: "das muss viel geiler aussehen und viel klarer." + Filipe: "das muss viel geiler aussehen bitte. ich will dass es auch + seeeehr gut erkennbar ist, genau wie der text drüber. mach das bitte + alles so dass es mega speziell, hochwertig und lesbar ist. mach was + richtig krass geiles." - Er hatte recht. Die erste Fassung war eine Liste kleiner Werte ohne - Rangfolge: Spanne, Diamanten, LIVE, Follower -- alles gleich groß, - nichts sprang heraus. Wer draufschaut, will ZWEI Dinge in einer - halben Sekunde wissen: WELCHER Zeitraum und WIE VIELE Diamanten. - Genau die sind jetzt groß, alles andere ordnet sich darunter. + --------------------------------------------------------------------- + WAS WIRKLICH NICHT STIMMTE — im Bildschirmfoto gemessen, nicht geraten - DER RAHMEN IST BEWUSST ANDERS ALS BEI DEN WOCHENKARTEN. Wer einen - Zeitraum für einen Tageswert hält, rechnet mit ihm weiter -- die - Karte muss auf den ersten Blick sagen, dass sie etwas anderes ist. - Deshalb: eigene Farbe an der Kante, die Spanne als Überschrift, die - Zahl der Tage als Marke daneben. + Die zweite Fassung hatte eine ordentliche Rangfolge. Ihr Fehler lag + woanders, und er war ein LESBARKEITSFEHLER: + + 1. ÜBERSCHRIFT UND SATZ STANDEN AUF DEM FOTO. Hinter dem Inhalt + dieser Seite liegt die Bühne — ein Bild mit einem hellroten + Berg. "ZEITRÄUME AUS BACKSTAGE" stand in `--text-still` + (#7f8fa6) mitten darin, der Erklärsatz daneben. Genau davon + redet er mit "seeeehr gut erkennbar". + 2. DIE KARTE WAR DURCHSICHTIG. Sie griff nach `--flaeche-tief` — + eine Variable, die es IM GANZEN HAUS NICHT GIBT. Gegriffen hat + also der Ersatzwert `rgba(10,16,24,.72)`, und durch die + fehlenden 28 % kam der Berg mit. Eine Variable, die nirgends + steht, fällt nicht auf: Sie wird still durch etwas Schlechteres + ersetzt. + 3. SIE STAND ALLEIN IN EINER REIHE FÜR VIER. `auto-fill` hält + leere Spalten offen; neben der einen Karte lagen 800 px blankes + Foto. + + --------------------------------------------------------------------- + DIE ANTWORT IST KEINE NEUE FARBE, SONDERN EIN KÖRPER + + Überschrift, Satz und Zeiträume sitzen ab jetzt IN EINER TAFEL: + Fassung außen, deckendes Innenglas darin, die Zeiträume als + eingelassene Felder. Damit steht jede Zeile auf einem bekannten + Grund — der Kontrast ist eine feste Zahl statt einer Frage an das + Hintergrundbild. + + DIE BAUART IST NICHT NEU ERFUNDEN. Es ist die Rollenkachel der + Anmeldeseite (`.rollen` in gate.css): vierfarbige Fassung in den + Hausfarben, Innenglas mit dem feinen Messraster, dunkle Fugen + zwischen den Feldern. Ein Haus, eine Handschrift. + + WARUM NICHT DIE MODULLISTE AUS module.css? Die gibt jeder Karte EINE + abgeschnittene Ecke und ein Kantenlicht. Bei fünf Zeiträumen wären + das fünf Silhouetten nebeneinander — wieder eine Liste. Hier ist es + EINE Sache mit mehreren Einträgen, also EIN Körper. Dieselbe + Entscheidung wie damals bei den fünf Rollen. + + AUGENSCHONEND BLEIBT ES: Die Fassung ist 2 px breit, das Raster + liegt bei 5 % Deckkraft, die hellste Fläche im Glas ist #0b111c. + Nichts leuchtet, nichts blinkt, nichts bewegt sich. ===================================================================== */ -.leistung-spannen { margin: 26px 0 0; } -.spannen__satz { - margin: 4px 0 14px; - font-size: .82rem; line-height: 1.6; - color: var(--text-still); - max-width: 64ch; +.leistung-spannen { margin: 26px 0 30px; } + +.spannen-tafel { + /* Die Form steht EINMAL als Variable da und wird von Fassung und + Innenglas benutzt. Zwei Abschriften desselben Polygons wären zwei + Gelegenheiten, dass die Fase auseinanderläuft. */ + --sp-fase: 16px; + --sp-rand: 2px; + --sp-form: polygon( + var(--sp-fase) 0, calc(100% - var(--sp-fase)) 0, + 100% var(--sp-fase), 100% calc(100% - var(--sp-fase)), + calc(100% - var(--sp-fase)) 100%, var(--sp-fase) 100%, + 0 calc(100% - var(--sp-fase)), 0 var(--sp-fase)); + + position: relative; + padding: var(--sp-rand); + clip-path: var(--sp-form); + + /* DIE FASSUNG: links Rot, rechts Babyblau, Silber als Treffpunkt, + Schwarz als Fuge — dieselben vier Farben wie am Rand der Zentrale + und an der Rollenkachel. */ + background: linear-gradient(96deg, + #20060b 0%, #c8323f 6%, #ff5f6d 14%, #ffd9dd 20%, + #b3242f 30%, #0a0c12 40%, #f4faff 50%, #0a0c12 60%, + #4f95bd 70%, #8fd8ff 78%, #dff2ff 84%, #3f7fa3 92%, #06080f 100%); + box-shadow: 0 14px 30px -18px rgba(0, 0, 0, .9); } + +/* DAS INNENGLAS — und es ist DECKEND. Das ist der eigentliche + Unterschied zur zweiten Fassung: Die letzte Ebene ist ein + undurchsichtiger Verlauf, kein rgba(). Kein einziger Bildpunkt des + Fotos kommt hier durch. */ +.spannen-tafel::before { + content: ""; position: absolute; inset: var(--sp-rand); + z-index: 0; pointer-events: none; + clip-path: var(--sp-form); + background: + linear-gradient(180deg, rgba(255, 255, 255, .05), transparent 34%), + repeating-linear-gradient(0deg, rgba(150, 186, 220, .05) 0 1px, transparent 1px 22px), + repeating-linear-gradient(90deg, rgba(150, 186, 220, .05) 0 1px, transparent 1px 22px), + radial-gradient(120% 80% at 12% 100%, rgba(255, 74, 92, .10), transparent 62%), + radial-gradient(110% 75% at 88% 0%, rgba(126, 200, 242, .09), transparent 60%), + linear-gradient(180deg, #0b111c, #070b13); +} +.spannen-tafel > * { position: relative; z-index: 1; } + +/* ---------- Der Kopf der Tafel ---------------------------------------- */ +/* Überschrift und Erklärsatz sitzen IN der Tafel, nicht darüber — + sonst wäre die Tafel eine Liste ohne Titel und der Titel ein Satz + ohne Bezug. Dieselbe Überlegung wie bei der Rollenkachel. */ +.spannen-tafel__kopf { + padding: 14px 18px 14px; + border-bottom: 1px solid #05070c; + box-shadow: 0 1px 0 rgba(255, 255, 255, .045); +} + +/* DAS TYPENSCHILD IN GEBÜRSTETEM METALL. + + "genau wie der text drüber" — der Text drüber soll nicht nur + mithalten, er soll der Anker sein. Oben hell, in der Mitte dunkler, + unten wieder hell: erst der WECHSEL liest sich als Material, ein + Verlauf von hell nach dunkel wäre bloß eine Färbung. + + DER RÜCKFALL IST VOLLWERTIG: `color` steht fest auf #e6eef8 und wird + erst in einem `@supports` durch den Verlauf ersetzt. Fällt + `background-clip: text` aus, steht dort heller Text — nicht + unsichtbarer. Der dunkelste Punkt des Verlaufs (#93a8bf) liegt auf + dem Glas (#0b111c) bei 8,8:1; gerechnet, nicht geschätzt. + + ZWEI KLASSEN IM WAEHLER, UND ZWAR MIT ABSICHT (16.09.2026). + + Der erste Anlauf schrieb `.spannen__schild` -- eine Klasse, Gewicht + (0,1,0). Im Browser gemessen kam davon NICHTS an: `display` stand + auf `block`, die Farbe auf `--text-leise`, das Metall war nicht da. + Schuld war `.inhalt .feldschild` in start.css, Gewicht (0,2,0) -- + zwei Klassen schlagen eine, egal wer spaeter geladen wird. + + Mit `.spannen-tafel .spannen__schild` sind es auch (0,2,0), und + DANN entscheidet der Ladeweg: leistung.css kommt nach start.css. + + Genau davor warnt start.css an anderer Stelle selbst ("Wer eine + Flaeche neu baut, muss zuerst nachsehen, wer sie vorher beschrieben + hat -- und mit welchem Gewicht"). Der Hinweis stand da; gefunden hat + es trotzdem erst die Messung. */ +.spannen-tafel .spannen__schild { + position: relative; + display: flex; align-items: center; gap: 11px; + margin: 0 0 7px; + /* Groesser als ein gewoehnliches Feldschild (.74rem): Es ist hier + nicht die Beschriftung eines Feldes, sondern der Titel der Tafel -- + und Filipe will ihn "seeeehr gut erkennbar". */ + font-size: .82rem; + color: #e6eef8; + letter-spacing: .1em; +} +/* Der Anschlag links: drei Kerben, oben rot, unten babyblau — dieselbe + Richtung wie die Fassung ringsum. */ +.spannen-tafel .spannen__schild::before { + content: ""; + width: 13px; height: 13px; flex: none; + background-image: + linear-gradient(180deg, transparent 0 46%, rgba(143, 216, 255, .95) 46%), + repeating-linear-gradient(180deg, #ff5f6d 0 2px, transparent 2px 5px); + background-blend-mode: multiply, normal; + -webkit-mask: linear-gradient(180deg, #000 0 45%, rgba(0, 0, 0, .55) 100%); + mask: linear-gradient(180deg, #000 0 45%, rgba(0, 0, 0, .55) 100%); +} +@supports ((-webkit-background-clip: text) or (background-clip: text)) { + .spannen-tafel .spannen__schild { + background: linear-gradient(178deg, + #f4f9ff 0%, #dfeaf6 32%, #93a8bf 60%, #d7e6f5 86%, #f4f9ff 100%) text; + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } + /* Der Anschlag ist ein ::before und würde vom `background-clip: text` + der Elternregel mit ausgeschnitten — er hat keinen Text, also + bliebe nichts übrig. Er bekommt deshalb seine eigene Ansage. */ + .spannen-tafel .spannen__schild::before { + -webkit-background-clip: border-box; background-clip: border-box; + } +} + +/* Der Erklärsatz. Er stand in `--text-still` (#7f8fa6) auf einem Foto. + Jetzt in `--text-leise` auf dem deckenden Glas. */ +.spannen__satz { + margin: 0; + font-size: .82rem; line-height: 1.6; + color: var(--text-leise); + max-width: 72ch; +} + +/* ---------- Die Zeiträume als eingelassene Felder --------------------- */ +/* DIE FUGE MACHT DIE TAFEL. Nicht der Rand außen — der ist Fassung —, + sondern die dunkle Linie ZWISCHEN den Feldern. Ein Spalt, durch den + der Untergrund scheint, trennt; eine dunkle Fuge verbindet, weil sie + zum selben Körper gehört. Deshalb `#05070c` und nicht `transparent`. + + `auto-fit` STATT `auto-fill` (16.09.2026): Bei EINEM Zeitraum hielt + `auto-fill` drei leere Spalten offen, und daneben lag blankes Foto. + `auto-fit` lässt die leeren Spuren zusammenfallen — ein Zeitraum + füllt die Breite, vier teilen sie sich. */ .spannen { - display: grid; gap: 12px; - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + display: grid; gap: 1px; + background: #05070c; + grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); } .spanne { - position: relative; overflow: hidden; - border: 1px solid var(--rand); border-radius: 16px; - padding: 14px 16px 15px; + position: relative; + padding: 15px 18px 16px; background: - linear-gradient(180deg, rgba(255, 255, 255, .045), rgba(255, 255, 255, .012)), - var(--flaeche-tief, rgba(10, 16, 24, .72)); -} -/* Die Kante links: ein Streifen in der Hausfarbe. Er sagt "andere - Sorte Zahl", ohne dass ein einziges Wort dafür nötig wäre. */ -.spanne::before { - content: ""; position: absolute; inset: 0 auto 0 0; - width: 3px; - background: linear-gradient(180deg, - color-mix(in srgb, var(--akzent) 75%, transparent), - color-mix(in srgb, var(--akzent) 15%, transparent)); + linear-gradient(180deg, rgba(255, 255, 255, .028), transparent 42%), + #0a1018; } .spanne__kopf { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; - padding-bottom: 10px; margin-bottom: 11px; - border-bottom: 1px solid var(--rand); + padding-bottom: 10px; margin-bottom: 12px; + border-bottom: 1px solid rgba(150, 186, 220, .13); } .spanne__spanne { display: inline-flex; align-items: baseline; gap: 5px; @@ -490,28 +652,39 @@ } .spanne__strich { color: var(--text-still); font-weight: 400; } .spanne__tage { - font-size: .73rem; color: var(--text-leise); - border: 1px solid color-mix(in srgb, var(--akzent) 30%, var(--rand)); - background: color-mix(in srgb, var(--akzent) 10%, transparent); + font-size: .73rem; color: #cfe4f4; + border: 1px solid color-mix(in srgb, var(--akzent) 42%, transparent); + background: color-mix(in srgb, var(--akzent) 14%, transparent); border-radius: 999px; padding: 2px 9px 3px; white-space: nowrap; } -/* Die Hauptzahl. Groß, aber nicht grell -- sie soll die erste sein, - die man liest, nicht die einzige, die man sieht. */ +/* Die Hauptzahl. Groß, aber nicht grell — sie soll die erste sein, die + man liest, nicht die einzige, die man sieht. Dasselbe gebürstete + Metall wie das Typenschild: Die Tafel hat damit ZWEI Anker, oben und + in der Mitte, und beide sprechen dieselbe Sprache. */ .spanne__haupt { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 9px; margin-bottom: 12px; } .spanne__haupt-zahl { - font-size: 1.85rem; font-weight: 700; line-height: 1.05; + font-size: 2.05rem; font-weight: 700; line-height: 1.05; color: var(--text); font-variant-numeric: tabular-nums; } +@supports ((-webkit-background-clip: text) or (background-clip: text)) { + .spanne__haupt-zahl { + background: linear-gradient(176deg, + #f4f9ff 0%, #dbe7f4 38%, #9fb3c9 72%, #e4eef9 100%) text; + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} .spanne__haupt-name { font-size: .78rem; letter-spacing: .06em; text-transform: uppercase; - color: var(--text-still); + color: var(--text-leise); } /* Der Durchschnitt steht in derselben Zeile und deutlich kleiner: Er ist eine Einordnung, kein zweiter Messwert. */ @@ -521,23 +694,110 @@ font-variant-numeric: tabular-nums; } -.spanne__werte { - display: grid; gap: 8px 14px; - grid-template-columns: repeat(auto-fit, minmax(92px, 1fr)); - padding-top: 11px; - border-top: 1px solid var(--rand); +/* ---------- "11 von 13 Tagen live" ------------------------------------ */ +/* KEINE DEKORATION, SONDERN DIE ZAHL, DIE BACKSTAGE MITLIEFERT und die + bis heute weggeworfen wurde: `spaltenRaten` kannte kein Muster für + "Gültige LIVE-Gehen-Tage", die Spalte lief also ins Leere. Jetzt + steht sie da — und ein Streifen zeigt sie, statt sie nur zu nennen. + + ER FÜLLT SICH NICHT WIE EIN LADEBALKEN. Das wäre "Fortschritt" und + damit gelogen: Elf von dreizehn Tagen ist kein halb fertiger + Vorgang, sondern ein fertiges Ergebnis. */ +.spanne__lauf { margin: 0 0 12px; } +.spanne__lauf-bahn { + position: relative; + height: 8px; border-radius: 3px; overflow: hidden; + background: rgba(150, 186, 220, .14); +} + +/* IN TAGE GETEILT, NICHT IN PROZENT. + + Ein durchgehender Balken sagt "85 %". Das ist richtig gerechnet und + trotzdem die schlechtere Auskunft: Gezaehlt wurden TAGE, und dreizehn + Kaestchen, von denen elf leuchten, liest man ohne eine einzige Zahl. + Auf 1050 px Breite taeuscht ein glatter Balken ausserdem eine + Genauigkeit vor, die es nicht gibt -- ein Bildpunkt waere ein + Zehntelprozent, die kleinste echte Einheit ist ein ganzer Tag. + + EINE MASKE, KEINE DREIZEHN ELEMENTE. Sie schneidet Fugen in den + fertigen Streifen und wirkt dadurch auf Bahn UND Fuellung zugleich -- + die beiden koennen also gar nicht auseinanderlaufen. + + AB 32 TAGEN WIRD NICHT MEHR GETEILT (`data-teilen` kommt aus dem + Skript): Bei einem Zeitraum ueber ein Quartal waere jede Zelle + schmaler als ihre eigene Fuge -- der Streifen verschwaende sich + selbst. Dann traegt wieder die glatte Bahn. */ +.spanne__lauf-bahn[data-teilen="ja"] { + --zelle: calc(100% / var(--tage, 1)); + border-radius: 0; + -webkit-mask: repeating-linear-gradient(90deg, + #000 0 calc(var(--zelle) - 2px), transparent calc(var(--zelle) - 2px) var(--zelle)); + mask: repeating-linear-gradient(90deg, + #000 0 calc(var(--zelle) - 2px), transparent calc(var(--zelle) - 2px) var(--zelle)); +} +.spanne__lauf-teil { + position: absolute; inset: 0 auto 0 0; + border-radius: 3px; + /* Gedeckt, nicht grell: Der Streifen ist der einzige farbige Fleck + der Karte und soll ihn nicht zum Leuchtkasten machen. */ + background: linear-gradient(90deg, + color-mix(in srgb, var(--akzent) 72%, transparent), + color-mix(in srgb, var(--akzent) 40%, transparent)); +} +.spanne__lauf-text { + display: block; margin-top: 5px; + font-size: .73rem; color: var(--text-leise); + font-variant-numeric: tabular-nums; +} + +/* FLIESSEND STATT IM RASTER (16.09.2026). + + Als Gitter mit `1fr`-Spuren zog ein einzelner Zeitraum ueber die + ganze Tafelbreite zwei Werte auf 1050 px auseinander -- "LIVE-ZEIT" + ganz links, "NEUE FOLLOWER" in der Mitte, dazwischen nichts. Das + liest sich als Tabelle mit fehlenden Spalten. + + Fliessend gesetzt stehen sie beieinander und brechen erst um, wenn + der Platz wirklich endet. Dieselbe Ueberlegung wie bei der + Kopfleiste: eine REGEL, die misst, statt einer Zahl, die rechnet. */ +.spanne__werte { + display: flex; flex-wrap: wrap; gap: 9px 26px; + padding-top: 11px; + border-top: 1px solid rgba(150, 186, 220, .13); +} +.spanne__wert { + display: flex; flex-direction: column; gap: 2px; + flex: 0 1 auto; min-width: 92px; } -.spanne__wert { display: flex; flex-direction: column; gap: 2px; min-width: 0; } .spanne__name { font-size: .72rem; letter-spacing: .05em; text-transform: uppercase; color: var(--text-still); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .spanne__zahl { - font-size: .95rem; color: var(--text-leise); + font-size: .95rem; color: var(--text); font-variant-numeric: tabular-nums; } +@media (max-width: 620px) { + .spannen-tafel { --sp-fase: 12px; } + .spannen-tafel__kopf { padding: 12px 14px; } + .spanne { padding: 13px 14px 14px; } +} +@media (forced-colors: active) { + /* Im Kontrastmodus trägt allein die Kante. Fassung, Innenglas und + Metallschrift wären dort graue Flecken — und eine Schrift, die + ihre Farbe aus einem Verlauf holt, wäre unsichtbar. */ + .spannen-tafel { background: none; border: 1px solid CanvasText; box-shadow: none; } + .spannen-tafel::before { display: none; } + .spannen { gap: 0; background: none; } + .spanne { border-top: 1px solid CanvasText; background: none; } + .spannen-tafel .spannen__schild, .spanne__haupt-zahl { + background: none; -webkit-text-fill-color: CanvasText; color: CanvasText; + } + .spannen-tafel .spannen__schild::before { display: none; } +} + /* ===================================================================== EINE WOCHE OHNE EINEN EINZIGEN TAG (14.09.2026) diff --git a/workspace/assets/js/leistung.js b/workspace/assets/js/leistung.js index cb5fd3ef..ec49bc6c 100644 --- a/workspace/assets/js/leistung.js +++ b/workspace/assets/js/leistung.js @@ -692,6 +692,42 @@ karte.append(haupt); } + /* ---- Wie viele Tage davon wirklich live waren (16.09.2026) ----- + Backstage liefert "Gültige LIVE-Gehen-Tage" mit. Die Zahl stand + bisher als eine von fünf gleich aussehenden unten im Raster -- + dabei ist sie die EINZIGE, die sagt, wie die Diamantenzahl + zustande kam: 13.210 an elf Tagen ist etwas anderes als + 13.210 an dreien. + + EIN STREIFEN STATT EINES BALKENS. Er zeigt einen Anteil, keinen + Fortschritt -- deshalb keine Bewegung, kein Füllen, kein Ziel. + + GERECHNET WIRD NUR, WENN BEIDE ZAHLEN DA SIND. Ohne die Zahl + der Tage wäre der Anteil eine Division durch nichts, und ein + Streifen ohne Bezug zeigt eine Genauigkeit, die es nicht gibt. */ + if (z.gueltige_tage !== null && z.gueltige_tage !== undefined && z.tage > 0) { + /* NICHT GERUNDET. 11 von 13 sind 84,615 %; auf 85 gerundet + ragte die Füllung vier Pixel in den zwölften Tag hinein -- + auf einem in Tage geteilten Streifen sieht das aus wie ein + angefangener Tag, den es nicht gibt. */ + const anteil = Math.max(0, Math.min(100, (z.gueltige_tage / z.tage) * 100)); + const lauf = el('div', 'spanne__lauf'); + const bahn = el('div', 'spanne__lauf-bahn'); + const teil = el('div', 'spanne__lauf-teil'); + teil.style.width = anteil + '%'; + /* Die Zahl der Tage geht als Wert an den Stil -- er teilt den + Streifen daraufhin in genau so viele Kästchen. Ab 32 Tagen + wäre jede Zelle schmaler als ihre Fuge, dann bleibt es bei + der glatten Bahn. Die Grenze steht im Stil erklärt. */ + bahn.style.setProperty('--tage', String(z.tage)); + if (z.tage <= 31) bahn.dataset.teilen = 'ja'; + bahn.append(teil); + lauf.append(bahn); + lauf.append(el('span', 'spanne__lauf-text', + `${z.gueltige_tage} von ${z.tage} Tagen live`)); + karte.append(lauf); + } + /* ---- Die uebrigen Werte --------------------------------------- */ const werte = el('div', 'spanne__werte'); const zeig = (name, wert, form) => { @@ -702,7 +738,9 @@ werte.append(w); }; zeig('LIVE-Zeit', z.dauer_min, stunden); - zeig('Gültige Tage', z.gueltige_tage, (n) => `${n} von ${z.tage}`); + /* "Gültige Tage" steht NICHT mehr hier -- es steht als Streifen + darüber. Zweimal dieselbe Zahl wäre zweimal dieselbe Auskunft + mit zwei verschiedenen Rängen. */ zeig('Ø Zuschauer', z.zuschauer_avg, zahl); zeig('Spitze', z.zuschauer_max, zahl); zeig('Neue Follower', z.follower_neu, zahl); diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 5baf16fd..2bb833a0 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. --> - - - - - - + + + + + + - +
@@ -282,10 +282,10 @@ - - - - - + + + + +