diff --git a/server/pruef-spicy.mjs b/server/pruef-spicy.mjs
index 650241bf..e1f815ac 100644
--- a/server/pruef-spicy.mjs
+++ b/server/pruef-spicy.mjs
@@ -326,6 +326,70 @@ const ruf = async (art, weg, keks, koerper) => {
ok(rein.status === 403 || rein.status === 404,
`und kommt auch mit der Raumnummer nicht hinein (HTTP ${rein.status})`);
}
+{
+ /* ===================================================================
+ WAS DOGFATHER GEHOERT, SIEHT SPICY MEDIA NICHT (07.09.2026)
+
+ Filipe: "wieso sieht die spicy rolle meine daten die ich habe und
+ speichere, meins sollen die nicht sehen von der rolle dogfather."
+
+ Beim ersten Anlauf bekam Spicy Media dieselbe Regel wie DogFather
+ (`1=1`) -- damit stimmte der Ueberblick und nebenbei standen seine
+ eigenen Termine, Aufgaben, Dateien und Eintraege mit drin.
+
+ Geprueft wird mit ZWEI Datensaetzen je Art: einem von DogFather und
+ einem von Max. Mit nur einem waere jedes Ergebnis mehrdeutig -- eine
+ leere Liste koennte auch heissen, dass die Regel ALLES ausblendet.
+ Genau das ist der Unterschied zwischen "sieht das Richtige nicht"
+ und "sieht gar nichts". */
+ /* ORTSZEIT, nicht UTC. `toISOString().slice(0,10)` liegt zwischen
+ Mitternacht und zwei Uhr einen Tag daneben -- die Pruefung waere
+ dann nachts rot, ohne dass sich etwas geaendert haette. Gefunden
+ von pruef-struktur, das genau danach sucht. */
+ const jetztHier = new Date();
+ const zz = (n) => String(n).padStart(2, "0");
+ const heute = `${jetztHier.getFullYear()}-${zz(jetztHier.getMonth() + 1)}-${zz(jetztHier.getDate())}`;
+ await ruf("POST", "/workspace/api/bereich/technik", keksDogi,
+ { art: "problem", titel: "DogFathers Notiz", datum: heute });
+ await ruf("POST", "/workspace/api/bereich/technik", keksMax,
+ { art: "problem", titel: "Max seine Notiz", datum: heute, creator_id: idLuna });
+
+ const { daten } = await ruf("GET", "/workspace/api/bereich/technik", keksSpicy);
+ const titel = (daten?.eintraege || []).map((e) => e.titel);
+ ok(!titel.includes("DogFathers Notiz"),
+ `DogFathers Eintrag ist fuer Spicy Media unsichtbar (sie sieht: ${titel.join(", ") || "nichts"})`);
+ ok(titel.includes("Max seine Notiz"),
+ "Max seiner steht aber da -- die Regel blendet nicht einfach alles aus");
+
+ /* Dasselbe fuer Aufgaben und Termine -- drei Module, drei Wege, und
+ jeder haette einzeln vergessen werden koennen. */
+ await ruf("POST", "/workspace/api/aufgaben", keksDogi, { titel: "DogFathers Aufgabe" });
+ await ruf("POST", "/workspace/api/aufgaben", keksMax,
+ { titel: "Max seine Aufgabe", creator_id: idLuna });
+ const auf = await ruf("GET", "/workspace/api/aufgaben", keksSpicy);
+ const aTitel = (auf.daten?.aufgaben || []).map((a) => a.titel);
+ ok(!aTitel.includes("DogFathers Aufgabe"),
+ `auch seine Aufgabe nicht (sie sieht: ${aTitel.join(", ") || "nichts"})`);
+ ok(aTitel.includes("Max seine Aufgabe"), "Max seine schon");
+
+ await ruf("POST", "/workspace/api/termine", keksDogi,
+ { titel: "DogFathers Termin", art: "termin", beginn: heute + "T20:00", dauer_min: 30 });
+ await ruf("POST", "/workspace/api/termine", keksMax,
+ { titel: "Max sein Termin", art: "termin", beginn: heute + "T21:00", dauer_min: 30,
+ creator_id: idLuna });
+ const term = await ruf("GET", `/workspace/api/termine?von=${heute}&bis=${heute}`, keksSpicy);
+ const tTitel = (term.daten?.termine || []).map((t) => t.titel);
+ ok(!tTitel.includes("DogFathers Termin"),
+ `und seinen Termin nicht (sie sieht: ${tTitel.join(", ") || "nichts"})`);
+ ok(tTitel.includes("Max sein Termin"), "Max seinen schon");
+
+ /* GEGENPROBE: DogFather selbst sieht weiterhin BEIDE. Ohne sie
+ bewiesen die Zeilen oben nur, dass irgendetwas gefiltert wird. */
+ const beiDogi = await ruf("GET", "/workspace/api/bereich/technik", keksDogi);
+ const dTitel = (beiDogi.daten?.eintraege || []).map((e) => e.titel);
+ ok(dTitel.includes("DogFathers Notiz") && dTitel.includes("Max seine Notiz"),
+ `DogFather sieht weiterhin beide (${dTitel.length})`);
+}
{
/* Und DogFather bleibt der Einzige mit dem letzten Wort. */
const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksDogi);
diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js
index 49b05279..ac23d26b 100644
--- a/server/workspace-aufgaben.js
+++ b/server/workspace-aufgaben.js
@@ -10,7 +10,7 @@
import express from "express";
import {
- db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, heuteLokal, ROLLEN_SORTIERUNG, betreuteIds,
+ db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, betreuteIds,
externPruefen, externSql, sichtbarePersonenIds, sichtbareCreatorIds,
} from "./workspace.js";
@@ -58,6 +58,13 @@ export function sichtbar(person) {
(01.09.2026) -- und ausdruecklich NUR er. */
case "admin":
return { wo: "1=1", werte: [] };
+ /* SPICY MEDIA sieht alles ausser dem, was DogFather gehoert
+ (07.09.2026). Die Bedingung kommt aus workspace.js -- eine
+ abgeschriebene Fassung waere die Stelle, an der es beim naechsten
+ Umbau wieder durchsickert. */
+ case "spicy":
+ return { wo: ohneDogFather("a", ["creator_id", "verantwortlich_id", "erstellt_von"]),
+ werte: [] };
case "creator":
return {
wo: "(a.creator_id = ? OR a.verantwortlich_id = ? OR a.erstellt_von = ?)",
diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js
index 472dc29e..b8ae0463 100644
--- a/server/workspace-bereiche.js
+++ b/server/workspace-bereiche.js
@@ -16,7 +16,7 @@
import express from "express";
import {
- db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles,
+ db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather,
externPruefen, externSql,
} from "./workspace.js";
@@ -262,6 +262,7 @@ export function sichtbar(person) {
darf (freigeben, aendern, Personen verwalten), haengt weiterhin an
istLeitung und bleibt unveraendert -- sonst haette dieser eine
Wunsch stillschweigend seine halben Rechte mitgenommen. */
+ if (istSpicy(person)) return { wo: ohneDogFather("e"), werte: [] };
if (siehtAlles(person)) return { wo: "1=1", werte: [] };
/* "ODER ich habe ihn selbst angelegt" kam am 02.09.2026 dazu -- aus
diff --git a/server/workspace-chat.js b/server/workspace-chat.js
index 3a8705e5..7ded71d7 100644
--- a/server/workspace-chat.js
+++ b/server/workspace-chat.js
@@ -88,13 +88,27 @@ function istDrin(raumId, personId) {
}
/** Alle (noch aktiven) Teilnehmer eines Raums. */
+/* Das Profilbild gehoert dazu (07.09.2026). Ohne es zeigt die
+ Gespraechsliste Initialen, obwohl ein Bild hochgeladen ist -- der
+ Fehler liegt dann nicht in der Anzeige, sondern in der Abfrage. Die
+ fertige ADRESSE wird danach gebaut, nicht der Dateiname
+ weitergereicht: Sonst setzt jede Stelle im Browser denselben Pfad
+ zusammen.
+
+ UND DER KOMMENTAR STEHT AUSSERHALB DER ABFRAGE. Beim ersten Anlauf
+ stand er darin -- mit dem Wort `bild` in schraegen Anfuehrungszeichen.
+ Die beenden in JavaScript eine Vorlagenzeichenkette; die halbe SQL
+ wurde damit zu Programmtext, und die Datei liess sich nicht mehr
+ lesen. Ein Kommentar, der die Zeichenkette schliesst, in der er
+ steht. */
function teilnehmerVon(raumId) {
return db().prepare(`
- SELECT t.person_id AS id, p.name, p.rolle, t.leitung, t.gelesen_bis
+ SELECT t.person_id AS id, p.name, p.rolle, p.bild, t.leitung, t.gelesen_bis
FROM chat_teilnehmer t
JOIN personen p ON p.id = t.person_id
WHERE t.raum_id = ? AND t.raus_am IS NULL
- ORDER BY ` + ROLLEN_SORTIERUNG + ", p.name").all(raumId);
+ ORDER BY ` + ROLLEN_SORTIERUNG + ", p.name").all(raumId)
+ .map((z) => ({ ...z, bild: z.bild ? `/workspace/api/steckbrief/bild/${z.bild}` : null }));
}
/** Findet das Zweier-Gespräch der beiden -- oder legt es an.
@@ -287,7 +301,8 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
über `vor`. */
const vor = Number(req.query.vor) || 0;
const reihen = db().prepare(`
- SELECT n.id, n.person_id, n.text, n.erstellt, n.weg_am, p.name AS von, p.rolle
+ SELECT n.id, n.person_id, n.text, n.erstellt, n.weg_am, p.name AS von, p.rolle,
+ p.bild AS von_bild
FROM chat_nachrichten n
LEFT JOIN personen p ON p.id = n.person_id
WHERE n.raum_id = ? ${vor ? "AND n.id < ?" : ""}
@@ -300,11 +315,13 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
raum: {
id: raum.id, art: raum.art,
name: raumName(raum, leute, req.person.id),
- teilnehmer: leute.map((t) => ({ id: t.id, name: t.name, rolle: t.rolle, leitung: !!t.leitung })),
+ teilnehmer: leute.map((t) => ({ id: t.id, name: t.name, rolle: t.rolle,
+ bild: t.bild, leitung: !!t.leitung })),
},
/* Aufsteigend zurück -- gelesen wird von oben nach unten. */
nachrichten: reihen.reverse().map((n) => ({
id: n.id, von_id: n.person_id, von: n.von || "Gelöscht", rolle: n.rolle,
+ von_bild: n.von_bild ? `/workspace/api/steckbrief/bild/${n.von_bild}` : null,
text: n.weg_am ? null : n.text,
zurueckgenommen: !!n.weg_am,
erstellt: n.erstellt,
diff --git a/server/workspace-dateien.js b/server/workspace-dateien.js
index 253665b9..5ffeb585 100644
--- a/server/workspace-dateien.js
+++ b/server/workspace-dateien.js
@@ -29,7 +29,7 @@ import { randomBytes } from "node:crypto";
import { join, extname, basename } from "node:path";
import { mkdirSync, createReadStream, writeFileSync, unlinkSync, statSync } from "node:fs";
import {
- db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, ROLLEN_SORTIERUNG,
+ db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather, ROLLEN_SORTIERUNG,
externSql, EXTERN_MAX, sichtbarePersonenIds,
} from "./workspace.js";
@@ -79,6 +79,7 @@ export function sichtbar(person) {
darf (freigeben, aendern, Personen verwalten), haengt weiterhin an
istLeitung und bleibt unveraendert -- sonst haette dieser eine
Wunsch stillschweigend seine halben Rechte mitgenommen. */
+ if (istSpicy(person)) return { wo: ohneDogFather("d", ["creator_id", "hochgeladen_von"]), werte: [] };
if (siehtAlles(person)) return { wo: "1=1", werte: [] };
/* Ausdrueckliche Freigabe -- gilt fuer jede Rolle gleich. Wer in der
diff --git a/server/workspace.js b/server/workspace.js
index 47fa1249..99a968dc 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -112,6 +112,44 @@ export const istDogFather = (person) => !!person && person.rolle === "admin";
gar keinen Weg gibt -- nicht, weil eine Abfrage es verbietet.
======================================================================= */
export const istSpicy = (person) => !!person && person.rolle === "spicy";
+
+/* =======================================================================
+ SPICY MEDIA SIEHT ALLES -- AUSSER DEM, WAS DOGFATHER GEHOERT
+ (07.09.2026, Nachtrag am selben Tag)
+
+ Filipe: "wieso sieht die spicy rolle meine daten die ich habe und
+ speichere, meins sollen die nicht sehen von der rolle dogfather."
+
+ Beim ersten Anlauf bekam Spicy Media dieselbe Regel wie DogFather:
+ `1=1`. Damit stimmte der Ueberblick ueber Manager, Scouts und Creator
+ -- und nebenbei standen DogFathers eigene Termine, Aufgaben, Dateien
+ und Eintraege mit drin. Der Chat war ausgenommen (er haengt an der
+ Teilnehmerliste), alles andere nicht.
+
+ DIESE FUNKTION IST DIE EINE STELLE DAFUER. Sie baut die Bedingung
+ "gehoert keinem DogFather" fuer eine beliebige Tabelle. Jede
+ Sichtbarkeitsregel im Haus benutzt sie -- eine zweite, abgeschriebene
+ Fassung waere die Stelle, an der es beim naechsten Modul wieder
+ durchsickert.
+
+ ZWEI SPALTEN, NICHT EINE. `creator_id` sagt, UM WEN es geht;
+ `erstellt_von` sagt, WER es geschrieben hat. Beide muessen gepruegt
+ werden: Ein Termin, den DogFather fuer sich selbst anlegt, hat
+ moeglicherweise gar keine creator_id -- er haenge dann nur an
+ erstellt_von. Und eine Akte UEBER einen DogFather traegt seine
+ creator_id, auch wenn ein Manager sie geschrieben hat.
+
+ `IS NULL OR NOT IN` und nicht bloss `NOT IN`: In SQL ist
+ `NULL NOT IN (...)` weder wahr noch falsch, sondern NULL -- die Zeile
+ fiele stillschweigend heraus. Genau so verschwinden Daten, ohne dass
+ jemand einen Fehler sieht.
+ ======================================================================= */
+export function ohneDogFather(praefix, spalten = ["creator_id", "erstellt_von"]) {
+ return spalten
+ .map((sp) => `(${praefix}.${sp} IS NULL OR ${praefix}.${sp} NOT IN `
+ + `(SELECT id FROM personen WHERE rolle = 'admin'))`)
+ .join(" AND ");
+}
export const siehtAlles = (person) => !!person
&& (person.rolle === "admin" || person.rolle === "spicy");
@@ -2329,6 +2367,7 @@ export const externSql = (personSpalte, externSpalte) =>
spaeter auseinandergelaufen -- und dann haette eine Wiederholung
jemandem etwas gezeigt, was der einzelne Termin ihm verbirgt. */
export function termineSichtbar(person, praefix = "t") {
+ if (istSpicy(person)) return { wo: ohneDogFather(praefix), werte: [] };
if (siehtAlles(person)) return { wo: "1=1", werte: [] };
const p = praefix;
diff --git a/workspace/assets/css/chat.css b/workspace/assets/css/chat.css
index bcae75c9..37faabb2 100644
--- a/workspace/assets/css/chat.css
+++ b/workspace/assets/css/chat.css
@@ -480,3 +480,159 @@
.chat-tag::before, .chat-tag::after { background: CanvasText; }
.chat__verlauf { mask-image: none; }
}
+
+
+/* =====================================================================
+ DER CHAT (07.09.2026)
+
+ Wunsch Filipe: "mach die chat fenster auch viel geiler, viel
+ detaillierter und viel perfekter. ich will dass die leute auch bock
+ bekommen diesen chat zu benutzen."
+
+ ---------------------------------------------------------------------
+ WORAN MAN EINEN GUTEN CHAT ERKENNT
+
+ Nicht an Effekten. An vier Dingen, die alle mit ORIENTIERUNG zu tun
+ haben -- und alle vier fehlten:
+
+ 1. MAN SIEHT, MIT WEM MAN SCHREIBT. Initialen sind ein Platzhalter,
+ kein Gesicht. Die Bilder liefert der Server seit heute mit; hier
+ werden sie gezeichnet -- rund, mit einem feinen Rand in der Farbe
+ der Rolle.
+
+ 2. EINE NACHRICHT HAT EINE RICHTUNG. Eigene Nachrichten kommen von
+ rechts und tragen den Hausakzent, fremde von links und bleiben
+ neutral. Die Ecke am Anfang einer Folge ist eckig, die übrigen
+ rund -- so sieht man, wo ein Gedanke anfängt, ohne den Namen zu
+ lesen.
+
+ 3. EIN GESPRÄCH HAT EIN GEWICHT. Ungelesenes ist nicht "auch da",
+ sondern das, weshalb man die Seite geöffnet hat: kräftigerer
+ Grund, farbige Schiene, die Zahl als Marke.
+
+ 4. DAS EINGABEFELD IST DAS WICHTIGSTE ELEMENT DER SEITE. Es war
+ eines von vielen. Jetzt sitzt es in einer eigenen Leiste, die
+ sich beim Hineinschreiben hebt.
+
+ Und was NICHT passiert ist: kein Blinken, kein Springen, keine
+ Verläufe im Text. Ein Chat wird stundenlang angesehen -- alles, was
+ einmal auffällt, nervt beim zwanzigsten Mal.
+ ===================================================================== */
+
+/* ---------- Das Gesicht ---------------------------------------------- */
+.chat-raum__zeichen,
+.chat-nachricht__zeichen {
+ position: relative;
+ overflow: hidden;
+ background: linear-gradient(150deg,
+ color-mix(in srgb, var(--rton, var(--akzent)) 30%, rgba(20, 27, 40, .9)),
+ rgba(12, 17, 26, .95));
+ box-shadow:
+ inset 0 0 0 1px color-mix(in srgb, var(--rton, var(--akzent)) 38%, transparent),
+ 0 2px 6px -3px rgba(0, 0, 0, .8);
+}
+/* Die Farbe der Rolle -- dieselbe Zuordnung wie überall im Haus. */
+.chat-raum__zeichen[data-rolle="spicy"],
+.chat-nachricht__zeichen[data-rolle="spicy"] { --rton: #e9848c; }
+.chat-raum__zeichen[data-rolle="admin"],
+.chat-nachricht__zeichen[data-rolle="admin"] { --rton: var(--k-gold); }
+.chat-raum__zeichen[data-rolle="manager"],
+.chat-nachricht__zeichen[data-rolle="manager"] { --rton: var(--k-mittel); }
+.chat-raum__zeichen[data-rolle="scout"],
+.chat-nachricht__zeichen[data-rolle="scout"] { --rton: var(--k-gut); }
+.chat-raum__zeichen[data-rolle="creator"],
+.chat-nachricht__zeichen[data-rolle="creator"] { --rton: var(--akzent); }
+
+/* Das Bild liegt ÜBER dem Buchstaben, nicht an seiner Stelle. Fällt es
+ aus, steht der Buchstabe weiterhin da. */
+.chat-raum__zeichen img,
+.chat-nachricht__zeichen img {
+ position: absolute; inset: 0;
+ width: 100%; height: 100%;
+ object-fit: cover;
+ border-radius: inherit;
+}
+
+/* ---------- Die Gesprächsliste --------------------------------------- */
+.chat-raum__knopf {
+ position: relative;
+ border-radius: 12px;
+ margin: 0 8px 4px;
+ transition: background var(--tempo), box-shadow var(--tempo);
+}
+/* Das offene Gespräch trägt eine Schiene -- man sieht auch beim Scrollen
+ der Liste, wo man gerade ist. */
+.chat-raum__knopf[data-an="ja"] {
+ background: linear-gradient(90deg,
+ color-mix(in srgb, var(--akzent) 16%, transparent),
+ rgba(255, 255, 255, .03) 55%);
+ box-shadow: inset 3px 0 0 var(--akzent);
+}
+/* Ungelesenes ist der Grund, warum man hier ist. */
+.chat-raum__knopf:has(.chat-raum__zahl) {
+ background: rgba(255, 255, 255, .045);
+}
+
+/* ---------- Die Nachrichten ------------------------------------------ */
+.chat-nachricht__blase {
+ border-radius: 16px 16px 16px 5px;
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, .06),
+ 0 2px 6px -3px rgba(0, 0, 0, .55);
+}
+/* Die erste Blase einer Folge bekommt oben eine eckige Ecke -- dort
+ fängt der Gedanke an. Die folgenden sind rundum weich. */
+.chat-nachricht[data-folge="ja"] .chat-nachricht__blase {
+ border-top-left-radius: 8px;
+}
+.chat-nachricht[data-selbst="ja"] .chat-nachricht__blase {
+ border-radius: 16px 16px 5px 16px;
+ background: linear-gradient(160deg,
+ color-mix(in srgb, var(--akzent) 26%, rgba(17, 24, 37, .95)),
+ color-mix(in srgb, var(--akzent) 12%, rgba(12, 17, 27, .95)));
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, .10),
+ 0 2px 8px -3px color-mix(in srgb, var(--akzent) 45%, transparent);
+}
+.chat-nachricht[data-selbst="ja"][data-folge="ja"] .chat-nachricht__blase {
+ border-top-right-radius: 8px;
+ border-top-left-radius: 16px;
+}
+
+/* Der Tagestrenner: eine Linie mit dem Datum darin statt eines Wortes
+ im Nichts. */
+.chat-tag {
+ display: flex; align-items: center; gap: 12px;
+ margin: 18px 0 12px;
+}
+.chat-tag::before, .chat-tag::after {
+ content: ""; flex: 1 1 auto; height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(150, 186, 220, .18), transparent);
+}
+
+/* ---------- Das Eingabefeld ------------------------------------------ */
+/* Es ist das wichtigste Element der Seite und sah aus wie eines von
+ vielen. Eine eigene Leiste, die sich beim Hineinschreiben hebt. */
+/* Sie heisst .chat__eingabe (chat.html), nicht .chat-neu -- ich hatte
+ den Namen aus dem Kopf geschrieben. pruef-struktur meldete es als
+ totes CSS: eine Regel, die auf nichts zeigt, sieht aus wie erledigte
+ Arbeit und ist keine. */
+.chat__eingabe {
+ position: relative;
+ padding: 12px 14px;
+ border-top: 1px solid rgba(150, 186, 220, .12);
+ background: linear-gradient(180deg, rgba(13, 18, 28, .6), rgba(9, 12, 20, .92));
+ transition: box-shadow var(--tempo);
+}
+.chat__eingabe:focus-within {
+ box-shadow: inset 0 1px 0 color-mix(in srgb, var(--akzent) 34%, transparent);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .chat-raum__knopf, .chat__eingabe { transition: none; }
+}
+@media (forced-colors: active) {
+ .chat-nachricht__blase,
+ .chat-raum__knopf[data-an="ja"] { box-shadow: none; border: 1px solid CanvasText; }
+ .chat-raum__zeichen, .chat-nachricht__zeichen { background: Canvas; box-shadow: none; }
+}
diff --git a/workspace/assets/css/gate.css b/workspace/assets/css/gate.css
index ca754260..02d1a794 100644
--- a/workspace/assets/css/gate.css
+++ b/workspace/assets/css/gate.css
@@ -1303,8 +1303,25 @@ body.gate {
/* 1,50 statt 1,59: Bei 1280x800 ist die Tafel 512 px hoch, der
Karteninhalt bei 1,59 aber 536 -- gemessen, nicht geschaetzt.
24 px Ueberstand sieht man an einem Neonrahmen sofort. */
- font-size: clamp(12px, 1.50cqh, 16px);
- padding: 1.75em 1.6em 1.4em;
+ /* NACHGEMESSEN, WEIL EINE ROLLE DAZUKAM (07.09.2026).
+
+ Der Kommentar darueber warnt woertlich davor -- und ich habe
+ genau das gemacht, wovor er warnt: eine fuenfte Rolle eingefuegt
+ und die Zahl daneben nicht angefasst. Gemessen bei fuenf Rollen:
+ Der Inhalt war bei 1280x800 573 px hoch in einer 526 px hohen
+ Tafel, bei 1600x1000 712 in 658 -- die Fusszeile stand jedes Mal
+ unter dem Rahmen.
+
+ Die Schrift kleiner zu machen half nicht: Bei 1280 lag sie mit
+ 12 px schon auf dem unteren Anschlag. Also sind die ABSTAENDE
+ enger geworden -- an neun Stellen ein bis zwei Zehntel, das
+ ergibt zusammen rund 4 em. Die Schrift bleibt lesbar, die Karte
+ passt wieder.
+
+ Wer eine SECHSTE Rolle einbaut, misst hier neu. Die Zahl gehoert
+ zu dem, was sie zaehlt. */
+ font-size: clamp(12px, 1.42cqh, 15.5px);
+ padding: 1.35em 1.5em 1.05em;
/* Die Tafel im Bild ist schon dunkel und hat ihren eigenen Rahmen.
Die Karte legt sich deshalb NUR noch als leichter Schleier
@@ -1326,21 +1343,21 @@ body.gate {
weiterhin am Dokument orientieren und die Abstaende blieben stehen,
waehrend die Schrift schrumpft. Genau daran erkennt man eine
schlecht skalierte Oberflaeche. */
- .tafel .marke { font-size: .74em; margin-bottom: .5em; }
- .tafel .titel { font-size: 1.85em; margin-bottom: .3em; }
- .tafel .unterzeile { font-size: .92em; margin-bottom: 1.15em; }
- .tafel .feldschild { font-size: .76em; margin-bottom: .6em; }
- .tafel .rollen { margin-bottom: 1.1em; }
- .tafel .rolle { padding: .62em .85em; margin-bottom: .42em; gap: .8em; }
+ .tafel .marke { font-size: .78em; margin-bottom: .35em; }
+ .tafel .titel { font-size: 1.78em; margin-bottom: .22em; }
+ .tafel .unterzeile { font-size: .9em; margin-bottom: .85em; }
+ .tafel .feldschild { font-size: .76em; margin-bottom: .45em; }
+ .tafel .rollen { margin-bottom: .8em; }
+ .tafel .rolle { padding: .50em .8em; margin-bottom: .30em; gap: .75em; }
.tafel .rolle__zeichen { width: 1.32em; height: 1.32em; }
.tafel .rolle__name { font-size: .94em; }
.tafel .rolle__zusatz { font-size: .79em; }
.tafel #code { padding: .72em 2.7em .72em .9em; }
.tafel .feld__auge { width: 2.2em; height: 2.2em; padding: .5em; right: .35em; }
- .tafel .knopf { margin-top: .75em; padding: .7em 1.1em; font-size: .88em; width: 100%; }
+ .tafel .knopf { margin-top: .6em; padding: .62em 1.1em; font-size: .88em; width: 100%; }
.tafel .hinweis,
.tafel .fehler { font-size: .82em; margin-top: .5em; }
- .tafel .fuss { margin-top: 1.1em; padding-top: .8em; font-size: .76em; }
+ .tafel .fuss { margin-top: .75em; padding-top: .6em; font-size: .76em; }
/* ---- Der Schleier ------------------------------------------------
Er verlief bisher von links dunkel nach rechts klar -- dort stand
@@ -1774,8 +1791,21 @@ body.gate {
display: flex; align-items: baseline; gap: .42em;
flex-wrap: wrap;
}
-.marke--paar .marke__spicy { color: #e9848c; }
-.marke--paar .marke__dogi { color: #63b8ee; }
+/* KRAEFTIGER (07.09.2026, Wunsch Filipe: "spicy x dogfather sieht man
+ nicht richtig"). Die beiden Namen standen in gedeckten Toenen auf der
+ hellsten Stelle der Karte -- dort, wo der rote Schein von oben links
+ hereinkommt. Gedeckt ist auf dunklem Grund richtig und auf hellem zu
+ wenig. Jetzt heller, etwas groesser und mit einem eigenen Schatten,
+ der sie vom Untergrund abhebt statt sie zu ueberstrahlen. */
+.marke--paar { letter-spacing: .13em; }
+.marke--paar .marke__spicy {
+ color: #ffb3b9;
+ text-shadow: 0 1px 10px rgba(255, 95, 109, .45);
+}
+.marke--paar .marke__dogi {
+ color: #a9dcff;
+ text-shadow: 0 1px 10px rgba(99, 184, 238, .45);
+}
.marke--paar .marke__x {
/* Kleiner, leiser, und mit etwas Luft -- es verbindet, es benennt
nicht. */
@@ -1784,3 +1814,48 @@ body.gate {
color: var(--text-still);
transform: translateY(-.02em);
}
+
+/* =====================================================================
+ DAS GESICHT IN DER PERSONENAUSWAHL (07.09.2026)
+
+ Wunsch Filipe: "jeder soll auch immer das profilbild von denen sehen
+ mit denen sie verbunden sind."
+
+ Ein rundes Bild in einer Auswahlliste ist der billigste und stärkste
+ Zugewinn, den eine solche Liste haben kann: Man trifft den richtigen
+ Namen, ohne ihn zu lesen. Und wer kein Bild hat, bekommt seinen
+ Anfangsbuchstaben -- kein leerer Kreis, kein Platzhaltersymbol.
+
+ Der Buchstabe bleibt beim Laden des Bildes LIEGEN und wird nicht
+ ersetzt. Lädt das Bild nicht (Verbindung weg, Datei gelöscht), steht
+ dort weiterhin etwas Sinnvolles statt eines zerbrochenen Bildsymbols.
+ ===================================================================== */
+.wahl2__bild {
+ position: relative;
+ flex: none;
+ width: 26px; height: 26px;
+ border-radius: 50%;
+ display: inline-flex; align-items: center; justify-content: center;
+ background: linear-gradient(150deg, rgba(63, 189, 245, .22), rgba(138, 118, 255, .18));
+ box-shadow: inset 0 0 0 1px rgba(150, 186, 220, .22);
+ color: #dbe4ef;
+ /* .76rem = 12,16 px. Bei .7rem waren es 11,2 -- unter der Grenze von
+ 11,5, die pruef-css-klassen zieht. Ein einzelner Grossbuchstabe in
+ einem 26-px-Kreis wirkt ohnehin kleiner, als die Zahl sagt. */
+ font-size: .76rem; font-weight: 700; line-height: 1;
+ overflow: hidden;
+}
+.wahl2__bild img {
+ position: absolute; inset: 0;
+ width: 100%; height: 100%;
+ object-fit: cover;
+ border-radius: 50%;
+}
+/* Der Name bekommt den restlichen Platz und wird gekürzt statt zu
+ umbrechen -- eine zweizeilige Zeile in einer Auswahlliste zerreißt
+ den Rhythmus, an dem das Auge entlangläuft. */
+.wahl2__name {
+ min-width: 0;
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+}
+.wahl2__eintrag { display: flex; align-items: center; gap: 10px; }
diff --git a/workspace/assets/css/personen.css b/workspace/assets/css/personen.css
index b8bd334f..038e5c64 100644
--- a/workspace/assets/css/personen.css
+++ b/workspace/assets/css/personen.css
@@ -171,8 +171,24 @@
transition: border-color .16s ease, background .16s ease, color .16s ease;
}
.rollenwahl__knopf:hover { border-color: rgba(255, 255, 255, .22); color: var(--text); }
+/* EIN ECHTER RAHMEN, NICHT NUR EIN SCHATTEN (07.09.2026).
+
+ Hier stand `outline: none` und stattdessen ein box-shadow. Das sieht
+ weicher aus und hat einen Haken: Ein Schatten wird von jedem
+ `overflow: hidden` eines Elternteils abgeschnitten -- und dann ist
+ der Fokus unsichtbar, ohne dass es irgendwo auffaellt. Genau das hat
+ pruef-barrierefrei gemeldet, und zwar NUR beim Manager: Er sieht auf
+ dieser Seite genau einen Rollenknopf, und der stand am Rand seines
+ Behaelters.
+
+ Jetzt beides: der Rahmen sorgt dafuer, dass man ihn IMMER sieht, der
+ Schatten dafuer, dass es gut aussieht. Wer mit der Tastatur arbeitet,
+ ist auf diesen Rahmen angewiesen -- er ist der einzige Hinweis
+ darauf, wo er gerade steht. */
.rollenwahl__knopf:focus-visible {
- outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--r) 32%, transparent);
+ outline: 2px solid color-mix(in srgb, var(--r) 85%, #ffffff);
+ outline-offset: 2px;
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--r) 32%, transparent);
}
/* Eine Farbe je Rolle -- dieselbe Sprache wie überall sonst. */
diff --git a/workspace/assets/js/aufgaben.js b/workspace/assets/js/aufgaben.js
index 1a665020..f3e45da2 100644
--- a/workspace/assets/js/aufgaben.js
+++ b/workspace/assets/js/aufgaben.js
@@ -772,6 +772,14 @@ Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerwe
for (const ziel of ['f-creator', 'f-verant', 'b-creator', 'b-verant']) {
const o = el('option', null, `${p.name} (${p.rolle})`);
o.value = String(p.id);
+ /* Bild ans