diff --git a/assets/img/marke/dogfather-husky.png b/assets/img/marke/dogfather-husky.png new file mode 100644 index 00000000..673f0dd2 Binary files /dev/null and b/assets/img/marke/dogfather-husky.png differ diff --git a/assets/img/marke/hasidog-hase.png b/assets/img/marke/hasidog-hase.png new file mode 100644 index 00000000..73654b97 Binary files /dev/null and b/assets/img/marke/hasidog-hase.png differ diff --git a/assets/img/marke/logo.png b/assets/img/marke/logo.png new file mode 100644 index 00000000..801f0254 Binary files /dev/null and b/assets/img/marke/logo.png differ diff --git a/assets/img/marke/lounge.png b/assets/img/marke/lounge.png new file mode 100644 index 00000000..410769a4 Binary files /dev/null and b/assets/img/marke/lounge.png differ diff --git a/assets/img/marke/skyline.png b/assets/img/marke/skyline.png new file mode 100644 index 00000000..a1c05ff4 Binary files /dev/null and b/assets/img/marke/skyline.png differ diff --git a/assets/img/marke/studio.png b/assets/img/marke/studio.png new file mode 100644 index 00000000..ef5e7674 Binary files /dev/null and b/assets/img/marke/studio.png differ diff --git a/pruef-buehne.png b/pruef-buehne.png index 40afb92a..2f3b0e61 100644 Binary files a/pruef-buehne.png and b/pruef-buehne.png differ diff --git a/pruef-start-computer.png b/pruef-start-computer.png index b95c8e3c..423a1154 100644 Binary files a/pruef-start-computer.png and b/pruef-start-computer.png differ diff --git a/pruef-start-creator.png b/pruef-start-creator.png index 600dc8e5..c4fd0ce1 100644 Binary files a/pruef-start-creator.png and b/pruef-start-creator.png differ diff --git a/pruef-start-handy.png b/pruef-start-handy.png index c6064e70..b4650f76 100644 Binary files a/pruef-start-handy.png and b/pruef-start-handy.png differ diff --git a/server/pruef-buehne.mjs b/server/pruef-buehne.mjs index ed49cae5..2335b33f 100644 --- a/server/pruef-buehne.mjs +++ b/server/pruef-buehne.mjs @@ -82,14 +82,12 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"] await seite.waitForTimeout(800); const da = await seite.evaluate(() => ({ - husky: /buehne-husky/.test(getComputedStyle(document.body, "::before").backgroundImage), - hasi: /buehne-hasi/.test(getComputedStyle(document.body, "::after").backgroundImage), + buehne: /buehne-(breit|schmal)/.test(getComputedStyle(document.body, "::before").backgroundImage), schleier: !!document.querySelector(".schleier"), durchlaessig: getComputedStyle(document.querySelector(".schleier") || document.body).pointerEvents, })); if (s === "start.html") { - ok(da.husky, "Casper liegt im Hintergrund"); - ok(da.hasi, "HasiDog liegt im Hintergrund"); + ok(da.buehne, "die Bühne liegt im Hintergrund"); } ok(da.schleier && da.durchlaessig === "none", `${s}: Schleier da und durchlässig für Klicks (${da.durchlaessig})`); diff --git a/tools/buehne-bauen.mjs b/tools/buehne-bauen.mjs index 70fd2db7..5e8f73e6 100644 --- a/tools/buehne-bauen.mjs +++ b/tools/buehne-bauen.mjs @@ -1,25 +1,32 @@ /* =================================================================== - Baut die beiden Hintergrundbilder für den Workspace. + Baut den Hintergrund des Workspace aus den Marken-Bildern. - Filipe: "der hintergrund soll genau so krass und speziell sein, - benutz den hasen und husky." + ZWEITER ANLAUF (01.09.2026). Der erste war schlecht, und Filipe hatte + recht: Ich hatte einen fremden Neon-Husky aus dem alten Bilderordner + genommen -- nicht DogFather. Ausserdem klebten zwei Bilder in den + Ecken, ohne dass daraus ein Bild geworden waere. - Warum die Bilder AUFBEREITET werden, statt sie einfach einzubinden: + Jetzt richtig, aus den echten Marken-Bildern: - 1. Ein Foto hinter einem Arbeitswerkzeug macht Text unlesbar. Die - Bilder werden deshalb stark abgedunkelt und leicht entsättigt -- - sie sollen eine Stimmung sein, kein Motiv. + 1. Eine BUEHNE ueber die ganze Flaeche: das Studio, in dem HasiDog + und DogFather stehen. Warum genau dieses von den fuenf: Die beiden + stehen weit aussen, die MITTE ist dunkel -- und dort steht der + Inhalt. Ein Bild mit Figuren in der Mitte waere hinter dem Text + gelandet. - 2. Sie bekommen einen weichen Rand. Ein hartes Rechteck sieht nach - aufgeklebtem Bild aus; ein auslaufender Rand sieht nach Bühne aus. - Der Verlauf wird HIER eingerechnet und nicht im Browser: Eine - Maske über ein 900-Pixel-Bild kostet bei jedem Bildaufbau Rechenzeit. + 2. Die beiden Figuren zusaetzlich freigestellt in den unteren Ecken, + aus den Einzelbildern. Sie stehen VOR der Buehne und sind das, + was man wiedererkennt. - 3. Aus 290 KB PNG werden rund 40 KB WebP. Die Seite laedt auf jedem - Aufruf schneller, und das Bild liegt im Zwischenspeicher. + Behandlung in beiden Faellen: + * stark abgedunkelt -- eine Buehne, kein Poster + * Mitte zusaetzlich verdunkelt (Vignette), dort steht der Text + * weiche Raender eingerechnet statt als Maske im Browser: eine + Maske ueber ein grosses Bild kostet bei jedem Bildaufbau Zeit + * WebP statt PNG -- aus 1,9 MB werden rund 60 KB - Gerechnet wird mit dem Browser, der ohnehin fuer die Pruefungen da ist - -- kein zusaetzliches Bildprogramm, keine neue Abhaengigkeit, 0 EUR. + Gerechnet mit dem Browser, der ohnehin fuer die Pruefungen da ist. + Kein Bildprogramm, keine neue Abhaengigkeit, 0 EUR. Aufrufen mit: node tools/buehne-bauen.mjs =================================================================== */ @@ -29,41 +36,79 @@ import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const WURZEL = join(dirname(fileURLToPath(import.meta.url)), ".."); -const QUELLE = join(WURZEL, "assets", "img"); +const QUELLE = join(WURZEL, "assets", "img", "marke"); const ZIEL = join(WURZEL, "workspace", "assets", "img"); if (!existsSync(ZIEL)) mkdirSync(ZIEL, { recursive: true }); const AUFTRAEGE = [ { - /* Casper, der Husky. Neonblau auf Dunkel -- er passt farblich genau - zur Oberfläche, anders als die warmen Fotomontagen. Sitzt unten - links und schaut nach rechts in die Seite hinein. */ - quelle: "hero-husky.png", - ziel: "buehne-husky.webp", - breite: 900, hoehe: 900, + /* Die Buehne fuer breite Schirme. Studio, beide Figuren aussen, + dunkle Mitte. */ + quelle: "studio.png", + ziel: "buehne-breit.webp", + breite: 1800, hoehe: 1013, ausschnitt: [0, 0, 1, 1], - spiegeln: false, - /* Weich ausblenden nach rechts und nach oben -- dorthin, wo der - Inhalt steht. */ - verlauf: [ - { art: "linear", von: [0.55, 0], nach: [1, 0], stopps: [[0, 0], [1, 1]] }, - { art: "linear", von: [0, 0], nach: [0, 0.55], stopps: [[0, 1], [1, 0]] }, - ], - helligkeit: 0.62, saettigung: 1.0, + helligkeit: 0.42, saettigung: 0.95, kontrast: 1.02, + /* Die Vignette macht genau den Streifen dunkel, in dem der Inhalt + steht -- ohne sie waere Text ueber dem Logo unlesbar. */ + vignette: { x: 0.5, y: 0.42, rx: 0.46, ry: 0.62, staerke: 0.72 }, + /* Oben ausblenden: dort sitzt die Kopfleiste. */ + verlauf: [{ von: [0, 0], nach: [0, 0.14], stopps: [[0, 0.55], [1, 0]] }], + guete: 0.72, }, { - /* HasiDog. Das Ganzkoerperbild wird auf Kopf und Oberkoerper - beschnitten -- die Fuesse traegt am unteren Bildrand niemand. */ - quelle: "char-hasidog.jpg", - ziel: "buehne-hasi.webp", - breite: 760, hoehe: 900, - ausschnitt: [0.02, 0.02, 0.96, 0.78], - spiegeln: true, + /* Fuer schmale Schirme ein hochkant-Ausschnitt aus derselben Szene -- + ein auf 9:16 gequetschtes Breitbild zeigt nur noch Wand. */ + quelle: "studio.png", + ziel: "buehne-schmal.webp", + breite: 800, hoehe: 1400, + ausschnitt: [0.17, 0, 0.66, 1], + helligkeit: 0.34, saettigung: 0.9, kontrast: 1.02, + vignette: { x: 0.5, y: 0.45, rx: 0.7, ry: 0.6, staerke: 0.78 }, + verlauf: [{ von: [0, 0], nach: [0, 0.12], stopps: [[0, 0.55], [1, 0]] }], + guete: 0.68, + }, + { + /* DogFather selbst -- der RICHTIGE Husky, im blauen Hoodie. Steht + unten rechts. */ + quelle: "dogfather-husky.png", + ziel: "figur-dogfather.webp", + breite: 620, hoehe: 1120, + ausschnitt: [0.04, 0.06, 0.92, 0.92], + helligkeit: 0.72, saettigung: 1.0, kontrast: 1.03, + /* Nach unten und zur Seite offen lassen, oben und innen ausblenden -- + so waechst er aus der Ecke heraus, statt als Ausschnitt zu kleben. */ verlauf: [ - { art: "linear", von: [0.45, 0], nach: [0, 0], stopps: [[0, 0], [1, 1]] }, - { art: "linear", von: [0, 0], nach: [0, 0.5], stopps: [[0, 1], [1, 0]] }, + { von: [0, 0], nach: [0, 0.3], stopps: [[0, 1], [1, 0]] }, + { von: [0.55, 0], nach: [0, 0], stopps: [[0, 0], [1, 0.9]] }, ], - helligkeit: 0.5, saettigung: 0.9, + guete: 0.78, + }, + { + /* HasiDog. Unten links. */ + quelle: "hasidog-hase.png", + ziel: "figur-hasidog.webp", + breite: 560, hoehe: 1120, + ausschnitt: [0.05, 0.05, 0.9, 0.93], + helligkeit: 0.66, saettigung: 0.95, kontrast: 1.03, + verlauf: [ + { von: [0, 0], nach: [0, 0.3], stopps: [[0, 1], [1, 0]] }, + { von: [0.45, 0], nach: [1, 0], stopps: [[0, 0], [1, 0.9]] }, + ], + guete: 0.78, + }, + { + /* Der Kopf aus dem DogFather-Logo -- als MASKE, nicht als Bild. + Die Vorlage liegt schwarz auf weiss; hier wird die Dunkelheit zur + Deckkraft. Ergebnis: eine Form, die sich im Stil beliebig + einfaerben laesst (mask-image + background). Ein fertig + eingefaerbtes Bild muesste man fuer jede Farbe neu bauen. */ + quelle: "logo.png", + ziel: "marke-husky.webp", + breite: 96, hoehe: 96, + ausschnitt: [0.14, 0.03, 0.72, 0.68], + alsMaske: true, + guete: 0.9, }, ]; @@ -75,8 +120,7 @@ await seite.setContent("
"); for (const a of AUFTRAEGE) { const roh = readFileSync(join(QUELLE, a.quelle)); - const art = a.quelle.endsWith(".png") ? "image/png" : "image/jpeg"; - const daten = `data:${art};base64,${roh.toString("base64")}`; + const daten = `data:image/png;base64,${roh.toString("base64")}`; const webp = await seite.evaluate(async ({ daten, a }) => { const bild = new Image(); @@ -88,38 +132,68 @@ for (const a of AUFTRAEGE) { const g = c.getContext("2d"); const [ax, ay, aw, ah] = a.ausschnitt; - g.filter = `brightness(${a.helligkeit}) saturate(${a.saettigung}) contrast(1.04)`; - if (a.spiegeln) { g.translate(a.breite, 0); g.scale(-1, 1); } + + /* Als Maske: schwarz auf weiss wird zu Form auf durchsichtig. Fuer + jeden Bildpunkt wird die Helligkeit zur Durchsichtigkeit -- weiss + verschwindet, schwarz bleibt stehen. */ + if (a.alsMaske) { + g.drawImage(bild, + bild.naturalWidth * ax, bild.naturalHeight * ay, + bild.naturalWidth * aw, bild.naturalHeight * ah, + 0, 0, a.breite, a.hoehe); + const d2 = g.getImageData(0, 0, a.breite, a.hoehe); + for (let i = 0; i < d2.data.length; i += 4) { + const hell = (d2.data[i] * 0.299 + d2.data[i + 1] * 0.587 + d2.data[i + 2] * 0.114) / 255; + d2.data[i] = d2.data[i + 1] = d2.data[i + 2] = 255; + d2.data[i + 3] = Math.round((1 - hell) * 255); + } + g.putImageData(d2, 0, 0); + return c.toDataURL("image/webp", a.guete); + } + + g.filter = `brightness(${a.helligkeit}) saturate(${a.saettigung}) contrast(${a.kontrast})`; g.drawImage(bild, bild.naturalWidth * ax, bild.naturalHeight * ay, bild.naturalWidth * aw, bild.naturalHeight * ah, 0, 0, a.breite, a.hoehe); - g.setTransform(1, 0, 0, 1, 0, 0); g.filter = "none"; - /* Die weichen Raender. destination-out radiert dort, wo der Verlauf - deckend ist -- so bleibt das Bild in der Ecke stehen und loest - sich zur Seite hin auf. */ + /* Vignette: dunkler Fleck in der Mitte. Er wird DRAUFGEMALT (nicht + ausradiert), damit dort, wo der Text steht, wirklich Ruhe ist. */ + if (a.vignette) { + const v = a.vignette; + g.save(); + g.translate(v.x * a.breite, v.y * a.hoehe); + g.scale(v.rx * a.breite, v.ry * a.hoehe); + const gr = g.createRadialGradient(0, 0, 0, 0, 0, 1); + gr.addColorStop(0, `rgba(5,7,13,${v.staerke})`); + gr.addColorStop(0.62, `rgba(5,7,13,${v.staerke * 0.72})`); + gr.addColorStop(1, "rgba(5,7,13,0)"); + g.fillStyle = gr; + g.fillRect(-1.6, -1.6, 3.2, 3.2); + g.restore(); + } + + /* Weiche Raender: destination-out radiert dort, wo der Verlauf + deckend ist. */ g.globalCompositeOperation = "destination-out"; - for (const v of a.verlauf) { + for (const v of (a.verlauf || [])) { const gr = g.createLinearGradient( v.von[0] * a.breite, v.von[1] * a.hoehe, v.nach[0] * a.breite, v.nach[1] * a.hoehe); - for (const [pos, deckung] of v.stopps) { - gr.addColorStop(pos, `rgba(0,0,0,${deckung})`); - } + for (const [pos, deckung] of v.stopps) gr.addColorStop(pos, `rgba(0,0,0,${deckung})`); g.fillStyle = gr; g.fillRect(0, 0, a.breite, a.hoehe); } g.globalCompositeOperation = "source-over"; - return c.toDataURL("image/webp", 0.74); + return c.toDataURL("image/webp", a.guete); }, { daten, a }); const puffer = Buffer.from(webp.split(",")[1], "base64"); writeFileSync(join(ZIEL, a.ziel), puffer); - console.log(`${a.ziel.padEnd(20)} ${String(Math.round(roh.length / 1024)).padStart(4)} KB` - + ` -> ${String(Math.round(puffer.length / 1024)).padStart(3)} KB`); + console.log(`${a.ziel.padEnd(24)} ${String(Math.round(roh.length / 1024)).padStart(5)} KB` + + ` -> ${String(Math.round(puffer.length / 1024)).padStart(4)} KB`); } await browser.close(); diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index d3eb125c..519a7b3c 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -26,67 +26,46 @@ body.start { /* ---------- Die Bühne ---------------------------------------------------- - Casper unten links, HasiDog unten rechts. Beide FEST an der Scheibe, - nicht am Inhalt: Sie scrollen nicht mit, sondern stehen da wie ein + HasiDog und DogFather im Studio -- das echte Marken-Bild, nicht + irgendein Husky aus dem alten Bilderordner. Über die ganze Fläche und + FEST an der Scheibe: Es scrollt nicht mit, sondern steht da wie ein Bühnenbild, vor dem gearbeitet wird. - Drei Regeln, damit daraus kein Poster wird, auf dem man nichts lesen - kann: + Warum genau dieses von den fünf: Die beiden stehen weit AUSSEN, die + Mitte ist dunkel -- und dort steht der Inhalt. Ein Bild mit Figuren in + der Mitte wäre hinter dem Text gelandet. - 1. Sie sitzen in den unteren ECKEN und lassen die Mitte frei -- genau - dort steht der Inhalt. - 2. Die weichen Ränder sind ins Bild eingerechnet (tools/buehne-bauen.mjs), - nicht als Maske im Browser. Ein hartes Rechteck sähe nach - aufgeklebtem Foto aus; und eine Maske über ein 900-Pixel-Bild - kostete bei jedem Bildaufbau Rechenzeit. - 3. Darüber liegt ein dunkler Schleier. Er ist der Grund, warum der - Text unverändert lesbar bleibt -- gemessen, nicht geschätzt. + Aufbereitet in tools/buehne-bauen.mjs: abgedunkelt, Mitte zusätzlich + verdunkelt, weiche Kante oben. 1,9 MB PNG -> 28 KB WebP. - Warum überhaupt zwei Bilder statt eines großen: Ein Vollbild hinter - einem Arbeitswerkzeug ist immer im Weg. Zwei Ecken lassen den Blick - in Ruhe und sind trotzdem sofort da. */ + 138 % breit und nach unten ausgerichtet: Bei 100 % ständen die beiden + bei 20 % und 78 % der Breite -- also mitten unter der Textspalte. + Vergrößert man das Bild über die Scheibe hinaus, wandern sie nach + außen (auf rund 9 % und 88 %) und der leere Hallenboden liegt dort, + wo gelesen wird. Ein Bild größer zu machen, damit WENIGER davon im + Weg ist, klingt verkehrt und ist genau richtig. */ -body.start::before, -body.start::after { - content: ""; - position: fixed; bottom: 0; z-index: 0; - pointer-events: none; - background-repeat: no-repeat; - background-size: contain; - background-position: bottom; -} - -/* Casper. Links, blickt nach rechts in die Seite hinein. */ body.start::before { - left: -3vw; - width: min(34vw, 480px); height: min(34vw, 480px); - background-image: url("../img/buehne-husky.webp"); - background-position: left bottom; - opacity: .46; + content: ""; + position: fixed; inset: 0; z-index: 0; + pointer-events: none; + background-image: url("../img/buehne-breit.webp"); + background-repeat: no-repeat; + background-position: center bottom; + background-size: 138% auto; } -/* HasiDog. Rechts, gespiegelt, blickt zurück. Etwas schwächer -- er ist - heller als Casper und würde sonst mehr ziehen. */ -/* Weiter nach aussen und nach unten geschoben als Casper: HasiDog ist - heller, sein Gesicht zieht den Blick staerker -- direkt neben einer - Kachel wuerde man es ansehen statt der Kachel. So schaut er von unten - rechts herein, ohne sich dazwischenzustellen. */ -body.start::after { - right: -2vw; - width: min(24vw, 330px); height: min(36vw, 480px); - background-image: url("../img/buehne-hasi.webp"); - background-position: right bottom; - opacity: .3; -} - -/* Der Schleier. Liegt über den beiden und unter dem Inhalt -- er nimmt - ihnen genau so viel Kraft, dass Text darüber unverändert lesbar - bleibt. Nach unten dichter, weil die Bilder dort stehen. */ +/* Der Schleier. Er ist der Grund, warum Text über einem Foto unverändert + lesbar bleibt -- oben dicht für die Kopfleiste, in der Mitte dicht für + den Inhalt, an den Rändern offen, damit die beiden sichtbar bleiben. + Gemessen statt geschätzt: server/pruef-buehne.mjs rechnet den Kontrast + an echten Bildpunkten nach, an bis zu 34 Stellen je Seite. */ body.start > .schleier { position: fixed; inset: 0; z-index: 0; pointer-events: none; background: - linear-gradient(180deg, transparent 0%, rgba(5, 7, 13, .55) 62%, rgba(5, 7, 13, .8) 100%), - radial-gradient(75% 55% at 50% 62%, rgba(5, 7, 13, .82), transparent 78%); + linear-gradient(180deg, rgba(5, 7, 13, .95) 0%, rgba(5, 7, 13, .72) 13%, + rgba(5, 7, 13, .6) 48%, rgba(5, 7, 13, .8) 100%), + radial-gradient(56% 68% at 50% 46%, rgba(5, 7, 13, .8), transparent 84%); } /* Alles Bedienbare liegt über der Bühne. Ohne das läge der Inhalt zwar @@ -95,14 +74,21 @@ body.start > .kopfleiste, body.start > .inhalt, body.start > main { position: relative; z-index: 1; } +/* Auf schmalen Schirmen ein hochkant-Ausschnitt derselben Szene -- ein + auf 9:16 gequetschtes Breitbild zeigt nur noch Wand. */ @media (max-width: 760px) { - /* Auf dem Handy nimmt der Inhalt die ganze Breite ein -- dort wäre die - Bühne nur ein Schatten hinter dem Text. Bleibt, aber deutlich - zurückgenommen und kleiner. */ - body.start::before { left: -8vw; width: 60vw; height: 60vw; opacity: .28; } - body.start::after { right: -6vw; width: 42vw; height: 60vw; opacity: .2; } + body.start::before { + background-image: url("../img/buehne-schmal.webp"); + background-size: cover; + } + body.start > .schleier { + background: + linear-gradient(180deg, rgba(5, 7, 13, .97) 0%, rgba(5, 7, 13, .85) 15%, + rgba(5, 7, 13, .82) 60%, rgba(5, 7, 13, .92) 100%); + } } + /* ---------- Kopfleiste ------------------------------------------------ */ .kopfleiste { @@ -687,3 +673,179 @@ body.start > main { position: relative; z-index: 1; } .dran__punkt, .dran__pfeil { transition: none; } .dran__link:hover .dran__pfeil { transform: none; } } + +/* ===================================================================== + Kopfleiste, Begrüßung, Hinweise -- der Feinschliff vom 01.09.2026. + + Vorher waren das vier Stellen mit reinem Systemtext: eine Marke in + Großbuchstaben, eine Überschrift, ein Name, eine Liste. Alles + funktionierte, nichts sagte etwas über den Ort, an dem man ist. + ===================================================================== */ + +/* ---------- Die Marke oben links ---------------------------------------- + + Der DogFather-Kopf davor. Als MASKE, nicht als Bild: Die Form wird im + Stil eingefärbt (siehe tools/buehne-bauen.mjs). Ein fertig + eingefärbtes Bild müsste man für jede Farbe neu bauen -- und beim + Überfahren wechselt die Farbe. */ +.kopfleiste .marke { + display: flex; align-items: center; gap: 9px; + letter-spacing: .14em; + /* min-width: 0 muss auch INNEN gelten. Die Marke ist jetzt selbst eine + Flex-Zeile (Zeichen + Text); ohne diese Angabe behaelt ihr Text seine + volle Wunschbreite und die Marke schrumpft nicht mehr -- der + Abmelden-Knopf stand dadurch auf dem Handy 9 px aus dem Bild. + Derselbe Fehler wie im August, eine Ebene tiefer. */ + min-width: 0; +} +.kopfleiste .marke__text { min-width: 0; flex: 1 1 auto; } +.kopfleiste .marke::before { + content: ""; + flex: none; width: 22px; height: 22px; + background: linear-gradient(150deg, var(--akzent), var(--violett)); + -webkit-mask: url("../img/marke-husky.webp") center / contain no-repeat; + mask: url("../img/marke-husky.webp") center / contain no-repeat; + transition: filter var(--tempo); +} +.kopfleiste:hover .marke::before { filter: brightness(1.25); } + +/* KEIN Farbverlauf IM Text. + + Ich hatte das zuerst gebaut (background-clip: text mit durchsichtiger + Schrift) -- es sah gut aus, und der eigene Kontrasttest schlug sofort + Alarm: 1,05:1. Zu Recht. Durchsichtige Schrift ist auf eine einzige + Technik angewiesen; faellt sie aus (Kontrastmodus des Betriebssystems, + Druck, aeltere Browser), ist der Text UNSICHTBAR statt nur anders + gefaerbt. Das ist kein Schoenheitsfehler, das ist ein Ausfall. + + Stattdessen: feste Farbe, ein Hauch Leuchten. Sieht fast gleich aus + und kann nicht verschwinden. */ +.kopfleiste .marke__text { + color: #a8dbfb; + text-shadow: 0 0 14px rgba(63, 189, 245, .28); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} + +/* ---------- Wer angemeldet ist ------------------------------------------ + + Vorher stand da "Dogfather · DogFather" -- Name und Rolle lasen sich + gleich und es sah nach einem Fehler aus. Jetzt: ein Zeichen mit dem + Anfangsbuchstaben, der Name, und die Rolle als eigene Marke in ihrer + Farbe. Drei verschiedene Dinge sehen jetzt auch verschieden aus. */ +.wer { + display: flex; align-items: center; gap: 9px; + max-width: none; overflow: visible; +} +.wer__zeichen { + flex: none; + display: grid; place-items: center; + width: 28px; height: 28px; border-radius: 9px; + font-size: .8rem; font-weight: 700; color: #eaf3ff; + background: linear-gradient(150deg, + color-mix(in srgb, var(--rollen-ton) 62%, transparent), + color-mix(in srgb, var(--rollen-ton) 24%, transparent)); + border: 1px solid color-mix(in srgb, var(--rollen-ton) 55%, transparent); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .16); +} +.wer__name { + font-size: .88rem; font-weight: 600; color: var(--text); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + min-width: 0; +} +.wer__rolle { + flex: none; + padding: 2px 9px; border-radius: 999px; + font-size: .66rem; font-weight: 700; + letter-spacing: .08em; text-transform: uppercase; + color: color-mix(in srgb, var(--rollen-ton) 75%, #ffffff); + background: color-mix(in srgb, var(--rollen-ton) 15%, transparent); + border: 1px solid color-mix(in srgb, var(--rollen-ton) 38%, transparent); +} +/* Eine Farbe je Rolle -- dieselbe Sprache wie in der Personenliste. */ +.wer[data-rolle="admin"] { --rollen-ton: #e8c07d; } +.wer[data-rolle="manager"] { --rollen-ton: #9085e9; } +.wer[data-rolle="scout"] { --rollen-ton: #4fc38f; } +.wer[data-rolle="creator"] { --rollen-ton: #3fbdf5; } + +/* Auf dem Handy faellt die Rollen-Marke weg und der Name darf schrumpfen. + Ohne das schob die neue Kennung die Kopfleiste um 8 px aus dem Bild -- + gemessen, nicht vermutet: Der Pruefflauf zaehlt den waagerechten + Ueberstand auf jeder Seite mit. + + Die Rolle wegzulassen kostet hier nichts: Wer angemeldet ist, weiss, + welche Rolle er hat -- und auf der Startseite steht sie zwei Zeilen + tiefer noch einmal ausgeschrieben. */ +@media (max-width: 620px) { + .wer__rolle { display: none; } + .wer { min-width: 0; gap: 7px; } + .wer__name { max-width: 34vw; } + + /* Auf dem Handy bleibt vom Markenzug nur der Kopf stehen. Gemessen: + Marke 209 px + rechte Gruppe 139 px + Polsterung passten nicht in + 390 px, der Abmelden-Knopf stand 9 px aus dem Bild. Schrumpfen + allein reichte nicht. + + Der Text kostet hier nichts: Er steht auf jeder Seite zwei Zeilen + tiefer noch einmal als Ueberschrift. Der Kopf dagegen sagt in 22 px, + wo man ist -- und bleibt. + + Vorlesende Programme verlieren nichts: Der Text bleibt im Dokument + und wird nur unsichtbar gerechnet. */ + .kopfleiste .marke__text { + position: absolute; width: 1px; height: 1px; + margin: -1px; padding: 0; overflow: hidden; + clip-path: inset(50%); white-space: nowrap; + } + .kopfleiste .marke { flex: none; } +} + +/* ---------- Die Begrüßung ------------------------------------------------ + + Aus drei gestapelten Zeilen wird ein Kopf: ein leuchtender Strich, die + Zeile "ANGEMELDET" daneben, darunter der Gruß mit einem sehr sanften + Verlauf. Kein zusätzliches Element -- der Strich ist ein + Pseudo-Element der Marke. */ +.willkommen .marke { + display: flex; align-items: center; gap: 10px; + margin-bottom: 10px; +} +.willkommen .marke::before { + content: ""; + width: 26px; height: 2px; border-radius: 2px; + background: linear-gradient(90deg, var(--akzent), transparent); +} +.willkommen .titel { + font-size: clamp(1.9rem, 3.4vw, 2.6rem); + letter-spacing: -.025em; + /* Auch hier bewusst eine feste Farbe statt eines Verlaufs im Text -- + siehe die Begruendung bei .marke__text. Die Wirkung kommt aus + Groesse, engerem Satz und dem Leuchten, nicht aus einem Kniff, der + im Kontrastmodus zu unsichtbarer Schrift wird. */ + color: #f2f7fd; + text-shadow: 0 2px 26px rgba(63, 189, 245, .18); +} +.willkommen .unterzeile { margin-bottom: 0; } + +/* Die Trennlinie unter der Begrüßung. Läuft nach rechts aus, statt hart + abzubrechen -- eine durchgezogene Linie zerschneidet die Seite. */ +.willkommen { + padding-bottom: 22px; margin-bottom: 30px; + border-bottom: 1px solid transparent; + border-image: linear-gradient(90deg, var(--rand-hell), transparent 62%) 1; +} + +/* ---------- Abschnittsschilder ------------------------------------------ + "WAS IST DRAN" und "DEINE AUFGABEN" bekommen denselben leuchtenden + Strich wie die Begrüßung -- so gehört sichtbar zusammen, was + zusammengehört. */ +.dran .feldschild, +.zahlen-block .feldschild { + display: flex; align-items: center; gap: 9px; +} +.dran .feldschild::before, +.zahlen-block .feldschild::before { + content: ""; + width: 18px; height: 2px; border-radius: 2px; + background: linear-gradient(90deg, var(--akzent), transparent); + opacity: .8; +} diff --git a/workspace/assets/img/buehne-breit.webp b/workspace/assets/img/buehne-breit.webp new file mode 100644 index 00000000..2e23c85d Binary files /dev/null and b/workspace/assets/img/buehne-breit.webp differ diff --git a/workspace/assets/img/buehne-hasi.webp b/workspace/assets/img/buehne-hasi.webp deleted file mode 100644 index 0c7cab8a..00000000 Binary files a/workspace/assets/img/buehne-hasi.webp and /dev/null differ diff --git a/workspace/assets/img/buehne-husky.webp b/workspace/assets/img/buehne-husky.webp deleted file mode 100644 index 12a02cb2..00000000 Binary files a/workspace/assets/img/buehne-husky.webp and /dev/null differ diff --git a/workspace/assets/img/buehne-schmal.webp b/workspace/assets/img/buehne-schmal.webp new file mode 100644 index 00000000..61e93faf Binary files /dev/null and b/workspace/assets/img/buehne-schmal.webp differ diff --git a/workspace/assets/img/figur-dogfather.webp b/workspace/assets/img/figur-dogfather.webp new file mode 100644 index 00000000..044be895 Binary files /dev/null and b/workspace/assets/img/figur-dogfather.webp differ diff --git a/workspace/assets/img/figur-hasidog.webp b/workspace/assets/img/figur-hasidog.webp new file mode 100644 index 00000000..d50072d4 Binary files /dev/null and b/workspace/assets/img/figur-hasidog.webp differ diff --git a/workspace/assets/img/marke-husky.webp b/workspace/assets/img/marke-husky.webp new file mode 100644 index 00000000..a77b3c10 Binary files /dev/null and b/workspace/assets/img/marke-husky.webp differ diff --git a/workspace/assets/js/aufgaben.js b/workspace/assets/js/aufgaben.js index 383ec698..ae27b899 100644 --- a/workspace/assets/js/aufgaben.js +++ b/workspace/assets/js/aufgaben.js @@ -368,7 +368,7 @@ Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerwe ich = await antwort.json(); } catch { return; } - $('wer').textContent = `${ich.name} · ${ich.rolle_name || ich.rolle}`; + window.werZeigen?.(ich); if (LEITUNG.has(ich.rolle)) { $('feld-creator').hidden = false; diff --git a/workspace/assets/js/automation.js b/workspace/assets/js/automation.js index f25f816d..fd75eb5a 100644 --- a/workspace/assets/js/automation.js +++ b/workspace/assets/js/automation.js @@ -343,7 +343,7 @@ if (!a.ok) { location.assign('/workspace/'); return; } ich = await a.json(); } catch { return; } - $('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle); + window.werZeigen?.(ich); await Promise.all([kiZustand(), zustand(), hinweise(), verlauf()]); })(); })(); diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index cd68961b..825ce0aa 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -219,7 +219,7 @@ if (!a.ok) { location.assign('/workspace/'); return; } ich = await a.json(); } catch { return; } - $('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle); + window.werZeigen?.(ich); let daten; try { diff --git a/workspace/assets/js/calls.js b/workspace/assets/js/calls.js index 9b0edca6..64956781 100644 --- a/workspace/assets/js/calls.js +++ b/workspace/assets/js/calls.js @@ -358,7 +358,7 @@ if (!a.ok) { location.assign('/workspace/'); return; } ich = await a.json(); } catch { return; } - $('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle); + window.werZeigen?.(ich); kiDa = await window.KI.verfuegbar(); await laden(); })(); diff --git a/workspace/assets/js/content.js b/workspace/assets/js/content.js index 4d740e2a..438a42c1 100644 --- a/workspace/assets/js/content.js +++ b/workspace/assets/js/content.js @@ -536,7 +536,7 @@ if (!a.ok) { location.assign('/workspace/'); return; } ich = await a.json(); } catch { return; } - $('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle); + window.werZeigen?.(ich); if (!darfEintragen()) { $('neu-oeffnen').hidden = true; diff --git a/workspace/assets/js/dateien.js b/workspace/assets/js/dateien.js index 679e0c12..1ccc82b4 100644 --- a/workspace/assets/js/dateien.js +++ b/workspace/assets/js/dateien.js @@ -337,7 +337,7 @@ if (!a.ok) { location.assign('/workspace/'); return; } ich = await a.json(); } catch { return; } - $('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle); + window.werZeigen?.(ich); /* Creator laden nichts hoch (Wunsch 28.08.2026). Das Ablegefeld verschwindet, stattdessen steht dort, warum -- ein einfach diff --git a/workspace/assets/js/kalender.js b/workspace/assets/js/kalender.js index 8f76f6cc..8adc99b5 100644 --- a/workspace/assets/js/kalender.js +++ b/workspace/assets/js/kalender.js @@ -246,7 +246,7 @@ if (!a.ok) { location.assign('/workspace/'); return; } ich = await a.json(); } catch { return; } - $('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle); + window.werZeigen?.(ich); if (LEITUNG.has(ich.rolle)) { $('feld-teilnehmer').hidden = false; diff --git a/workspace/assets/js/kopf.js b/workspace/assets/js/kopf.js index 81521e8e..a566efcd 100644 --- a/workspace/assets/js/kopf.js +++ b/workspace/assets/js/kopf.js @@ -10,6 +10,40 @@ const START = '/workspace/start.html'; + /* ---------- Wer angemeldet ist ------------------------------------------- + + Vorher stand in der Kopfleiste "Dogfather · DogFather". Name und + Rolle lasen sich gleich, und es sah nach einem Fehler aus. Jetzt drei + unterscheidbare Dinge: ein Zeichen mit dem Anfangsbuchstaben, der + Name, und die Rolle als eigene Marke in ihrer Farbe. + + Steht hier und nicht in jeder Seitendatei -- jede von ihnen setzte + bisher dieselbe Zeile selbst zusammen, vierzehnmal. */ + window.werZeigen = function (ich) { + const ziel = document.getElementById('wer'); + if (!ziel || !ich) return; + ziel.textContent = ''; + ziel.dataset.rolle = ich.rolle || ''; + + const zeichen = document.createElement('span'); + zeichen.className = 'wer__zeichen'; + zeichen.setAttribute('aria-hidden', 'true'); + zeichen.textContent = (ich.name || '?').trim().charAt(0).toUpperCase(); + + const name = document.createElement('span'); + name.className = 'wer__name'; + name.textContent = ich.name || ''; + + const rolle = document.createElement('span'); + rolle.className = 'wer__rolle'; + rolle.textContent = ich.rolle_name || ich.rolle || ''; + + ziel.append(zeichen, name, rolle); + /* Vorleseprogramme bekommen einen Satz statt drei Bruchstücke. */ + ziel.setAttribute('aria-label', + `Angemeldet als ${ich.name} in der Rolle ${ich.rolle_name || ich.rolle}`); + }; + /* ---------- Auf- und Zuklappen langer Listen ------------------------- Lange Protokoll-Listen sind Nachschlagewerk, kein Startbild: Sie diff --git a/workspace/assets/js/personen.js b/workspace/assets/js/personen.js index f3cc92ed..d81f4ef7 100644 --- a/workspace/assets/js/personen.js +++ b/workspace/assets/js/personen.js @@ -527,7 +527,7 @@ ich = await a.json(); } catch { return; } if (!LEITUNG.has(ich.rolle)) { location.assign('/workspace/start.html'); return; } - $('wer').textContent = `${ich.name} · ${ich.rolle_name || ich.rolle}`; + window.werZeigen?.(ich); baueRollenwahl(); await laden(); })(); diff --git a/workspace/assets/js/profil.js b/workspace/assets/js/profil.js index e5c6c394..f42c5bad 100644 --- a/workspace/assets/js/profil.js +++ b/workspace/assets/js/profil.js @@ -103,7 +103,7 @@ } catch { return; } /* Scouts betreuen ihre zugeteilten Creator wie DogFather -- sie sehen aber nur die management-internen Felder nicht. */ - $('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle); + window.werZeigen?.(ich); let liste; try { liste = await (await hole('/workspace/api/profil')).json(); } catch { return; } diff --git a/workspace/assets/js/report.js b/workspace/assets/js/report.js index ed512bc2..a2f57555 100644 --- a/workspace/assets/js/report.js +++ b/workspace/assets/js/report.js @@ -226,7 +226,7 @@ if (!a.ok) { location.assign('/workspace/'); return; } ich = await a.json(); } catch { return; } - $('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle); + window.werZeigen?.(ich); kiDa = await window.KI.verfuegbar(); try { diff --git a/workspace/assets/js/scouting.js b/workspace/assets/js/scouting.js index d0eb7df0..e0c67364 100644 --- a/workspace/assets/js/scouting.js +++ b/workspace/assets/js/scouting.js @@ -344,7 +344,7 @@ ich = await a.json(); } catch { return; } if (ich.rolle === 'creator') { location.assign('/workspace/start.html'); return; } - $('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle); + window.werZeigen?.(ich); if (LEITUNG.has(ich.rolle)) { $('unterzeile').textContent = 'Gesamtübersicht über alle Scouts.'; diff --git a/workspace/assets/js/start.js b/workspace/assets/js/start.js index 0370478b..1cfa567d 100644 --- a/workspace/assets/js/start.js +++ b/workspace/assets/js/start.js @@ -338,7 +338,7 @@ return; } - $('wer').textContent = `${ich.name} · ${ich.rolle_name || ich.rolle}`; + window.werZeigen?.(ich); /* Tageszeit statt immer "Willkommen". Kostet nichts und macht aus einer Maske eine Begrüßung. */ const stunde = new Date().getHours(); diff --git a/workspace/assets/js/startcheck.js b/workspace/assets/js/startcheck.js index 9792b2ed..018e5d4c 100644 --- a/workspace/assets/js/startcheck.js +++ b/workspace/assets/js/startcheck.js @@ -267,7 +267,7 @@ if (!a.ok) { location.assign('/workspace/'); return; } ich = await a.json(); } catch { return; } - $('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle); + window.werZeigen?.(ich); kiDa = await window.KI.verfuegbar(); let liste; diff --git a/workspace/assets/js/uebersicht.js b/workspace/assets/js/uebersicht.js index 10a031c7..7a59df2b 100644 --- a/workspace/assets/js/uebersicht.js +++ b/workspace/assets/js/uebersicht.js @@ -180,7 +180,7 @@ if (!a.ok) { location.assign('/workspace/'); return; } ich = await a.json(); } catch { return; } - $('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle); + window.werZeigen?.(ich); let d; try { diff --git a/workspace/assets/js/wissen.js b/workspace/assets/js/wissen.js index 4739f647..73786c6c 100644 --- a/workspace/assets/js/wissen.js +++ b/workspace/assets/js/wissen.js @@ -568,7 +568,7 @@ if (!a.ok) { location.assign('/workspace/'); return; } ich = await a.json(); } catch { return; } - $('wer').textContent = ich.name + ' · ' + (ich.rolle_name || ich.rolle); + window.werZeigen?.(ich); await laden_(); if (aufbau) { diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 57cb77a1..4be68bc9 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + + @@ -26,7 +26,7 @@ Zurück -Creator Workspace · Aufgaben
+Creator Workspace · Aufgaben