diff --git a/server/pruef-struktur.mjs b/server/pruef-struktur.mjs
index af248ed8..c2839fe7 100644
--- a/server/pruef-struktur.mjs
+++ b/server/pruef-struktur.mjs
@@ -266,9 +266,33 @@ for (const unter of ["assets/js", "assets/css", "assets/img"]) {
ausdruecklich "das ist die grosse Stufe". Wer eine Datei so nennt,
ohne dass es Stufen gibt, umgeht die Grenze -- deshalb prueft die
Zeile darunter, dass es die kleineren Stufen auch wirklich gibt. */
- const gestuft = /-uhd\.webp$|^gate-(1920|2560)\.webp$|^gate-hoch-900\.webp$/.test(n);
+ /* `-breit` GEHOERT DAZU (07.09.2026, Nachtrag am selben Tag).
+
+ Die Ausnahme kannte nur die groesste Stufe. Dann wurden die Bilder
+ nachgeschaerft -- und Schaerfe ist genau das, was Bytes kostet:
+ Sie fuegt hohe Frequenzen hinzu, und die kann kein Verfahren
+ wegrechnen. Die MITTLERE Stufe wuchs damit von rund 170 auf 230
+ bis 300 KB und lief in eine Grenze, die fuer EINZELDATEIEN gedacht
+ ist, nicht fuer eine von drei Stufen.
+
+ Die Regel bleibt inhaltlich unveraendert: Eine Datei darf gross
+ sein, WENN es nachweislich eine kleinere Stufe daneben gibt. Fuer
+ `-breit` ist das `-schmal` (85 bis 130 KB) -- ein Handy holt
+ weiterhin die kleine. Was wirklich zaehlt, misst ohnehin
+ pruef-tempo-workspace an der fertigen Seite: Dort liegt die
+ Grenze bei 1,2 MB, und start.html kommt auf 290 KB. */
+ const gestuft = /-(uhd|breit)\.webp$|^gate-(1920|2560)\.webp$|^gate-hoch-900\.webp$/.test(n);
if (gestuft) {
- const kleiner = n.replace("-uhd.webp", "-breit.webp")
+ /* JE STUFE GENAU EINE naechstkleinere -- nicht verkettet.
+ `replace(uhd->breit).replace(breit->schmal)` haette bei einer
+ uhd-Datei BEIDE Ersetzungen ausgefuehrt und am Ende nach
+ "-schmal" gesucht. Das faellt nicht auf, solange alle drei
+ Stufen da sind, und genau deshalb waere es ein stiller Fehler:
+ Fehlte eines Tages `-breit`, meldete die Pruefung trotzdem
+ "in Ordnung". */
+ const kleiner = n.endsWith("-uhd.webp") ? n.replace("-uhd.webp", "-breit.webp")
+ : n.endsWith("-breit.webp") ? n.replace("-breit.webp", "-schmal.webp")
+ : n
.replace(/^gate-(1920|2560)\.webp$/, "gate-1280.webp")
.replace("gate-hoch-900.webp", "gate-hoch-440.webp");
if (!existsSync(join(ordner, kleiner))) {
diff --git a/tools/ansicht.mjs b/tools/ansicht.mjs
new file mode 100644
index 00000000..1ba18a0e
--- /dev/null
+++ b/tools/ansicht.mjs
@@ -0,0 +1,72 @@
+/* ANSEHEN — startet die Anwendung mit frischer Datenbank, meldet sich an
+ und legt Bildschirmfotos ab. Kein Prueflauf, nur ein Blick.
+ Aufruf: node tools/ansicht.mjs [seite ...] */
+import { mkdtempSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join, dirname } from "node:path";
+import { fileURLToPath } from "node:url";
+import { spawn } from "node:child_process";
+import { DatabaseSync } from "node:sqlite";
+import { scryptSync, randomBytes } from "node:crypto";
+
+const HIER = join(dirname(fileURLToPath(import.meta.url)), "..", "server");
+const PORT = 4362;
+const BASIS = `http://127.0.0.1:${PORT}`;
+const ordner = mkdtempSync(join(tmpdir(), "ws-ans-"));
+const DB = join(ordner, "workspace.db");
+const SEITEN = process.argv.slice(2).length ? process.argv.slice(2) : ["start"];
+
+let kind = null;
+async function start() {
+ kind = spawn(process.execPath, [join(HIER, "index.js")], {
+ env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT),
+ SITE_ACCESS_SECRET: "lokaler-test",
+ SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" },
+ stdio: ["ignore", "pipe", "pipe"] });
+ let aus = "";
+ kind.stdout.on("data", (d) => { aus += d; });
+ kind.stderr.on("data", (d) => { aus += d; });
+ for (let i = 0; i < 80; i++) {
+ await new Promise((r) => setTimeout(r, 250));
+ if (kind.exitCode !== null) break;
+ try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" });
+ if (a.status < 500) return; } catch { /* noch nicht oben */ }
+ }
+ throw new Error("Server startete nicht:\n" + aus.slice(-1500));
+}
+await start();
+kind.kill();
+await new Promise((r) => setTimeout(r, 600));
+{
+ const d = new DatabaseSync(DB);
+ const salz = randomBytes(16).toString("hex");
+ const hash = scryptSync("CODE-DOGI-0001", salz, 64,
+ { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
+ d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
+ .run("Filipe", "admin", hash, salz, 32768, new Date().toISOString());
+ d.close();
+}
+await start();
+
+const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
+const browser = await pw.chromium.launch();
+const ctx = await browser.newContext({ viewport: { width: 1600, height: 1000 }, deviceScaleFactor: 1 });
+const seite = await ctx.newPage();
+
+await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
+await seite.click('.rolle[data-rolle="admin"]');
+await seite.fill("#code", "CODE-DOGI-0001");
+await seite.click("button[type=submit]");
+await seite.waitForURL("**/start.html", { timeout: 25000 });
+
+for (const s of SEITEN) {
+ await seite.goto(`${BASIS}/workspace/${s}.html`, { waitUntil: "networkidle" });
+ await seite.waitForTimeout(1600);
+ await seite.screenshot({ path: `/tmp/a-${s}.png`, fullPage: false });
+ console.log(`/tmp/a-${s}.png`);
+}
+
+await browser.close();
+kind.kill();
+await new Promise((r) => setTimeout(r, 400));
+try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ }
diff --git a/workspace/assets/css/aufgaben.css b/workspace/assets/css/aufgaben.css
index 797f01c5..b26bbadf 100644
--- a/workspace/assets/css/aufgaben.css
+++ b/workspace/assets/css/aufgaben.css
@@ -214,19 +214,116 @@
/* ---------- Bearbeiten-Dialog ------------------------------------------ */
+/* =====================================================================
+ DIALOGE (07.09.2026 überarbeitet)
+
+ Filipe zu zwei davon: "lass alles in dieser kachel viel spezieller und
+ geiler aussehen" und "dass soll auch viel besser aussehen".
+
+ Er hatte recht, und der Grund war nicht Geschmack: Ein Dialog war
+ bisher ein grauer Kasten (`background: #0b1018`, ein Rand, fertig) --
+ während die Seite dahinter aus beleuchteten Platten mit Schienen,
+ Körnung und Glanz besteht. Beim Öffnen fiel man aus dem einen
+ Material ins andere.
+
+ Jetzt trägt er dieselbe Sprache wie alles übrige:
+ - Leuchtschiene links, im Hausakzent
+ - Kopfzeile abgesetzt mit einer feinen Fuge
+ - Körnung und ein fester Glanz von links oben
+ - Materialstärke: Licht oben, Schatten unten
+ Und er ist deutlich TIEFER als vorher (#0b1018 war ein helles Grau
+ gegen das, was jetzt hinter ihm liegt) -- ein Dialog muss sich vom
+ Untergrund abheben, sonst schwimmt er darin.
+
+ Der Hintergrund dahinter wird STÄRKER abgedunkelt und weicher
+ gezeichnet: Ein Dialog ist der einzige Ort, an dem ein Weichzeichner
+ richtig ist -- er soll das Dahinter ausdrücklich zurücktreten
+ lassen. Genau dafür ist er gemacht, und genau deshalb war er unter
+ den Kacheln falsch.
+ ===================================================================== */
.dialog {
- width: min(560px, calc(100vw - 32px));
- padding: 0; border: 1px solid var(--rand-hell);
- border-radius: var(--radius);
- background: #0b1018;
+ position: relative;
+ width: min(580px, calc(100vw - 32px));
+ padding: 0;
+ border: 1px solid rgba(150, 186, 220, .16);
+ border-radius: 4px 20px 20px 4px;
+ background:
+ linear-gradient(90deg,
+ color-mix(in srgb, var(--akzent) 16%, transparent) 0%,
+ color-mix(in srgb, var(--akzent) 5%, transparent) 14%,
+ transparent 40%),
+ linear-gradient(163deg, #141c29 0%, #0a0e17 58%, #070a11 100%);
color: var(--text);
- box-shadow: var(--schatten);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, .10),
+ inset 0 -1px 0 rgba(0, 0, 0, .55),
+ inset 2px 0 0 -1px color-mix(in srgb, var(--akzent) 34%, transparent),
+ 0 30px 70px -24px rgba(0, 0, 0, .92),
+ 0 4px 12px -6px rgba(0, 0, 0, .7);
+ overflow: hidden;
}
-.dialog::backdrop { background: rgba(2, 4, 8, .68); backdrop-filter: blur(3px); }
+/* Die Leuchtschiene -- dieselbe wie an jeder Kachel. */
+.dialog::before {
+ content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
+ border-radius: 4px 0 0 4px;
+ background: linear-gradient(180deg,
+ color-mix(in srgb, var(--akzent) 55%, #fff) 0%,
+ var(--akzent) 26%,
+ color-mix(in srgb, var(--akzent) 48%, transparent) 100%);
+ box-shadow:
+ 0 0 9px 0 color-mix(in srgb, var(--akzent) 55%, transparent),
+ 3px 0 20px -3px color-mix(in srgb, var(--akzent) 42%, transparent);
+ z-index: 2;
+}
+/* Glanz und Körnung. `pointer-events: none` ist Pflicht: Ohne das läge
+ eine unsichtbare Fläche über dem ganzen Formular, und kein einziges
+ Feld wäre anklickbar. */
+.dialog::after {
+ content: ""; position: absolute; inset: 0; pointer-events: none;
+ border-radius: inherit;
+ background:
+ linear-gradient(122deg,
+ rgba(255, 255, 255, .075) 0%,
+ rgba(255, 255, 255, .020) 15%,
+ transparent 34%);
+ /* Ohne Körnung -- siehe die Begründung in automation.css: Auf einer
+ Ebene mit der Spiegelung erbt sie deren Deckkraft und wird zum
+ grauen Schleier. */
+ opacity: .55;
+ z-index: 1;
+}
+.dialog > * { position: relative; z-index: 2; }
-.neu--blank { margin: 0; border: 0; background: none; padding: 22px; }
+.dialog::backdrop {
+ background: rgba(2, 4, 8, .78);
+ backdrop-filter: blur(6px) saturate(80%);
+}
-.dialog__titel { margin: 0 0 18px; font-size: 1.12rem; font-weight: 700; }
+.neu--blank { margin: 0; border: 0; background: none; padding: 22px 24px 20px; }
+
+/* Der Kopf steht auf einer eigenen Stufe: Überschrift, darunter der
+ erklärende Satz, darunter eine feine Fuge. Ohne diese Trennung
+ verschwimmt die Ansage mit dem ersten Feld -- und genau die Ansage
+ ist das, was den Dialog erklärt. */
+.dialog__titel {
+ margin: 0 0 4px;
+ font-size: 1.24rem; font-weight: 700; letter-spacing: -.015em;
+ color: #f2f7fd;
+}
+.dialog__unter {
+ margin: 0 0 16px; padding-bottom: 16px;
+ border-bottom: 1px solid rgba(150, 186, 220, .12);
+ font-size: .87rem; line-height: 1.55;
+ color: var(--text-leise);
+}
+/* Hat ein Dialog keine Erklärung, muss die Fuge trotzdem da sein --
+ sonst hängt die Überschrift am ersten Feld. */
+.dialog__titel:last-of-type,
+.dialog__unter:empty { border-bottom: 0; }
+.dialog__unter:empty {
+ margin: 0 0 16px; padding-bottom: 16px;
+ border-bottom: 1px solid rgba(150, 186, 220, .12);
+}
.dialog__knoepfe { display: flex; gap: 10px; align-items: center; margin-top: 16px; flex-wrap: wrap; }
.dialog__knoepfe #b-loeschen { margin-left: auto; }
diff --git a/workspace/assets/css/automation.css b/workspace/assets/css/automation.css
index 9a8f043b..40256266 100644
--- a/workspace/assets/css/automation.css
+++ b/workspace/assets/css/automation.css
@@ -145,12 +145,293 @@
font-size: .82rem;
}
.verlauf__zeile:nth-child(odd) { background: var(--flaeche); }
-.verlauf__zeit { color: var(--text-still); font-variant-numeric: tabular-nums; }
-.verlauf__was { color: var(--text); }
-.verlauf__wer { color: var(--text-still); overflow: hidden; text-overflow: ellipsis; }
+/* ÜBERLAUF WIRD GEKÜRZT, NICHT ÜBERGESTELLT.
+
+ Die erste Spalte ist 96 px breit und hatte keine Bremse -- ein zu
+ langer Text lief einfach weiter und legte sich über den Nachbarn.
+ Am 07.09.2026 ist genau das passiert (ein falsch gebautes Datum,
+ siehe automation.js). Der Auslöser ist behoben; diese drei Zeilen
+ sorgen dafür, dass der NÄCHSTE zu lange Text nur abgeschnitten wird,
+ statt etwas unlesbar zu machen. Eine Spalte mit fester Breite ohne
+ Kürzung ist immer eine Zeitbombe. */
+.verlauf__zeit {
+ color: var(--text-still); font-variant-numeric: tabular-nums;
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+}
+.verlauf__was {
+ color: var(--text);
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+}
+.verlauf__wer { color: var(--text-still); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 700px) {
.verlauf__zeile { grid-template-columns: 1fr; gap: 2px; }
}
@media (prefers-reduced-motion: reduce) { .hinweis { transition: none; } }
+
+/* =====================================================================
+ AUTOMATIONEN — dasselbe Material wie überall (07.09.2026)
+
+ Filipe zu den Regelkarten: "diese kacheln sollen alle viel viel
+ krasser sein bitte." Und zum Sicherungsblock: "das soll auch viel
+ viel spezieller aussehen und die texte auch nicht übereinander."
+
+ Der zweite Satz ist der wichtigere -- die Überlappung war ein echter
+ Fehler und ist in automation.js behoben (ein falsch gebautes Datum)
+ und in .verlauf__zeit abgesichert (feste Spaltenbreite ohne Kürzung).
+
+ Optisch war der Befund derselbe wie überall: Diese Seite bestand aus
+ flachen Rechtecken mit `var(--flaeche)` und einem Rand -- gebaut,
+ bevor es die Leuchtschienen gab. Sie sah dadurch aus wie aus einer
+ anderen Anwendung.
+
+ ZWEI STUFEN, NICHT EINE. Die Regelkarten bekommen eine RUHIGE
+ Schiene: Es sind neun gleichrangige Karten, und neun leuchtende
+ Schienen nebeneinander ergeben einen Zaun. Der Sicherungsblock
+ dagegen ist EINE Karte mit einer Aussage ("zuletzt gesichert vor 23
+ Stunden") und bekommt die volle Behandlung -- er ist das Erste, was
+ man auf dieser Seite wissen will.
+
+ UND DIE SCHIENE SAGT ETWAS. Am Sicherungsblock nimmt sie die Farbe
+ des Zustands an: grün, wenn frisch, bernstein, wenn alt, rot bei
+ Fehler. Damit ist der Zustand aus dem Augenwinkel erkennbar, und der
+ Text daneben sagt dasselbe noch einmal in Worten -- Farbe allein
+ reicht nie.
+ ===================================================================== */
+
+/* ---------- Der Sicherungsblock -------------------------------------- */
+.zustand {
+ --z: var(--k-gut);
+ position: relative;
+ overflow: hidden;
+ border-radius: 4px 20px 20px 4px;
+ background:
+ linear-gradient(90deg,
+ color-mix(in srgb, var(--z) 26%, transparent) 0%,
+ color-mix(in srgb, var(--z) 9%, transparent) 12%,
+ color-mix(in srgb, var(--z) 3%, transparent) 26%,
+ transparent 46%),
+ linear-gradient(163deg,
+ rgba(23, 31, 45, .95) 0%,
+ rgba(11, 15, 24, .95) 58%,
+ rgba(8, 11, 18, .95) 100%);
+ border-color: color-mix(in srgb, var(--z) 20%, rgba(150, 186, 220, .13));
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, .10),
+ inset 0 -1px 0 rgba(0, 0, 0, .50),
+ 0 2px 5px -2px rgba(0, 0, 0, .55),
+ 0 16px 34px -20px rgba(0, 0, 0, .88);
+}
+.zustand::before {
+ content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
+ border-radius: 4px 0 0 4px;
+ background: linear-gradient(180deg,
+ color-mix(in srgb, var(--z) 55%, #fff) 0%,
+ var(--z) 26%,
+ color-mix(in srgb, var(--z) 48%, transparent) 100%);
+ box-shadow:
+ 0 0 9px 0 color-mix(in srgb, var(--z) 55%, transparent),
+ 3px 0 20px -3px color-mix(in srgb, var(--z) 42%, transparent);
+ z-index: 2;
+}
+.zustand::after {
+ content: ""; position: absolute; inset: 0; pointer-events: none;
+ border-radius: inherit;
+ background:
+ linear-gradient(112deg,
+ transparent 24%,
+ rgba(255, 255, 255, .045) 40%,
+ rgba(255, 255, 255, .08) 47%,
+ rgba(255, 255, 255, .025) 55%,
+ transparent 70%);
+ /* KEINE Körnung auf DIESER Ebene (07.09.2026, gemessen).
+
+ Sie stand hier als zweite Schicht und hat die Deckkraft der
+ Spiegelung geerbt -- 50 % eines mittelgrauen Rauschens heben jeden
+ Bildpunkt um rund 30 Stufen an. Gemessen: rgb(56,44,58) statt der
+ berechneten rgb(20,26,38), und über einer hellen Stelle des
+ Motivs rgb(85,83,81). Die Karte sah dadurch durchsichtig aus,
+ obwohl sie zu 95 % deckt.
+
+ Derselbe Fehler wie an der Anmeldekarte heute Nachmittag, und
+ genau derselbe Grund: Körnung gehört auf eine EIGENE Ebene mit
+ `mix-blend-mode: overlay` und 3 bis 4 Prozent -- so wie bei den
+ Kacheln der Startseite. Auf einer geteilten Ebene ist sie kein
+ Material, sondern ein grauer Schleier. */
+ opacity: .5;
+}
+.zustand > * { position: relative; z-index: 1; }
+
+/* Der Zustand steckt in EINER Größe. Die beiden Regeln darunter setzen
+ nur noch sie -- vorher standen Rand und Fläche zweimal getrennt da
+ und konnten auseinanderlaufen. */
+.zustand[data-zustand="alt"] { --z: var(--k-mittel); }
+.zustand[data-zustand="fehler"] { --z: var(--k-warn); }
+
+/* Die Kennzahlen darunter als eingelassene Felder statt als bloße
+ Aufzählung -- so liest man sie als Anzeige und nicht als Fließtext. */
+.zustand__daten {
+ gap: 10px;
+}
+.zustand__daten li {
+ padding: 9px 12px;
+ border-radius: var(--radius-klein);
+ background: rgba(0, 0, 0, .28);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, .04);
+ align-items: baseline;
+}
+
+/* ---------- Die Regelkarten ------------------------------------------ */
+.regeln { gap: 12px; }
+.regel {
+ position: relative;
+ overflow: hidden;
+ padding: 16px 18px 16px 20px;
+ border-radius: 3px 16px 16px 3px;
+ border-color: rgba(150, 186, 220, .12);
+ background:
+ linear-gradient(90deg,
+ color-mix(in srgb, var(--akzent) 13%, transparent) 0%,
+ color-mix(in srgb, var(--akzent) 4%, transparent) 16%,
+ transparent 40%),
+ linear-gradient(163deg,
+ rgba(21, 28, 41, .95) 0%,
+ rgba(11, 15, 24, .95) 62%,
+ rgba(8, 11, 18, .95) 100%);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, .075),
+ inset 0 -1px 0 rgba(0, 0, 0, .45),
+ 0 2px 4px -2px rgba(0, 0, 0, .5),
+ 0 12px 26px -18px rgba(0, 0, 0, .85);
+ transition: border-color var(--tempo), transform var(--tempo), box-shadow var(--tempo);
+}
+/* Die ruhige Schiene: 3 px, kein Leuchten. Neun leuchtende Schienen
+ nebeneinander ergäben einen Zaun -- hier zählt, dass die Karten eine
+ Reihe bilden, nicht dass jede für sich strahlt. */
+.regel::before {
+ content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
+ border-radius: 3px 0 0 3px;
+ background: linear-gradient(180deg,
+ color-mix(in srgb, var(--akzent) 78%, transparent) 0%,
+ color-mix(in srgb, var(--akzent) 40%, transparent) 100%);
+ opacity: .6;
+ transition: opacity var(--tempo);
+}
+.regel::after {
+ content: ""; position: absolute; inset: 0; pointer-events: none;
+ border-radius: inherit;
+ background-image: var(--koernung);
+ background-size: 140px 140px;
+ opacity: .03;
+ mix-blend-mode: overlay;
+}
+.regel > * { position: relative; z-index: 1; }
+/* Beim Überfahren hebt sich die Karte an und ihre Schiene brennt --
+ dieselbe Bewegung wie bei den Kacheln der Startseite, damit man nicht
+ zwei Oberflächen lernt. Angeklickt wird hier nichts; das Anheben sagt
+ nur "das gehört zusammen". */
+.regel:hover {
+ transform: translateY(-2px);
+ border-color: color-mix(in srgb, var(--akzent) 30%, rgba(150, 186, 220, .16));
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, .10),
+ inset 0 -1px 0 rgba(0, 0, 0, .45),
+ 0 4px 9px -4px rgba(0, 0, 0, .55),
+ 0 18px 36px -20px rgba(0, 0, 0, .9);
+}
+.regel:hover::before { opacity: 1; }
+.regel__name { margin: 0 0 6px; font-size: .96rem; font-weight: 700; color: #eaf2fb; }
+.regel__text { color: var(--text-leise); }
+
+@media (prefers-reduced-motion: reduce) {
+ .regel, .regel:hover { transform: none; }
+}
+@media (forced-colors: active) {
+ .zustand, .regel { background: Canvas; box-shadow: none; border: 1px solid CanvasText; }
+ .zustand::after, .regel::after { display: none; }
+}
+
+/* =====================================================================
+ DIE KI-KACHEL bekommt dasselbe Material wie der Sicherungsblock.
+
+ Sie stand als einzige Fläche dieser Seite noch flach da. Zwei
+ Materialien auf einer Seite wirken zusammengesucht -- und ausgerechnet
+ die oberste Kachel hätte den ersten Eindruck gemacht.
+ ===================================================================== */
+.ki-kasten {
+ --z: var(--violett);
+ position: relative;
+ overflow: hidden;
+ border-radius: 4px 20px 20px 4px;
+ background:
+ linear-gradient(90deg,
+ color-mix(in srgb, var(--z) 24%, transparent) 0%,
+ color-mix(in srgb, var(--z) 8%, transparent) 12%,
+ color-mix(in srgb, var(--z) 3%, transparent) 26%,
+ transparent 46%),
+ linear-gradient(163deg,
+ rgba(23, 31, 45, .95) 0%,
+ rgba(11, 15, 24, .95) 58%,
+ rgba(8, 11, 18, .95) 100%);
+ border-color: color-mix(in srgb, var(--z) 20%, rgba(150, 186, 220, .13));
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, .10),
+ inset 0 -1px 0 rgba(0, 0, 0, .50),
+ 0 2px 5px -2px rgba(0, 0, 0, .55),
+ 0 16px 34px -20px rgba(0, 0, 0, .88);
+}
+.ki-kasten::before {
+ content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
+ border-radius: 4px 0 0 4px;
+ background: linear-gradient(180deg,
+ color-mix(in srgb, var(--z) 55%, #fff) 0%,
+ var(--z) 26%,
+ color-mix(in srgb, var(--z) 48%, transparent) 100%);
+ box-shadow:
+ 0 0 9px 0 color-mix(in srgb, var(--z) 55%, transparent),
+ 3px 0 20px -3px color-mix(in srgb, var(--z) 42%, transparent);
+ z-index: 2;
+}
+.ki-kasten::after {
+ content: ""; position: absolute; inset: 0; pointer-events: none;
+ border-radius: inherit;
+ background:
+ linear-gradient(112deg,
+ transparent 24%,
+ rgba(255, 255, 255, .045) 40%,
+ rgba(255, 255, 255, .08) 47%,
+ rgba(255, 255, 255, .025) 55%,
+ transparent 70%);
+ /* KEINE Körnung auf DIESER Ebene (07.09.2026, gemessen).
+
+ Sie stand hier als zweite Schicht und hat die Deckkraft der
+ Spiegelung geerbt -- 50 % eines mittelgrauen Rauschens heben jeden
+ Bildpunkt um rund 30 Stufen an. Gemessen: rgb(56,44,58) statt der
+ berechneten rgb(20,26,38), und über einer hellen Stelle des
+ Motivs rgb(85,83,81). Die Karte sah dadurch durchsichtig aus,
+ obwohl sie zu 95 % deckt.
+
+ Derselbe Fehler wie an der Anmeldekarte heute Nachmittag, und
+ genau derselbe Grund: Körnung gehört auf eine EIGENE Ebene mit
+ `mix-blend-mode: overlay` und 3 bis 4 Prozent -- so wie bei den
+ Kacheln der Startseite. Auf einer geteilten Ebene ist sie kein
+ Material, sondern ein grauer Schleier. */
+ opacity: .5;
+}
+.ki-kasten > * { position: relative; z-index: 1; }
+/* Der Zustand steckt wieder in EINER Größe. */
+.ki-kasten[data-zustand="laeuft"] { --z: var(--violett); }
+.ki-kasten[data-zustand="weg"] { --z: var(--k-warn); }
+.ki-kasten__daten { gap: 10px; }
+.ki-kasten__daten li {
+ padding: 9px 12px;
+ border-radius: var(--radius-klein);
+ background: rgba(0, 0, 0, .28);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, .04);
+ align-items: baseline;
+}
+@media (forced-colors: active) {
+ .ki-kasten { background: Canvas; box-shadow: none; border: 1px solid CanvasText; }
+ .ki-kasten::after { display: none; }
+}
diff --git a/workspace/assets/css/calls.css b/workspace/assets/css/calls.css
index f22c9abd..be3d7f4b 100644
--- a/workspace/assets/css/calls.css
+++ b/workspace/assets/css/calls.css
@@ -224,3 +224,123 @@
.zeitblock { border: 1px solid CanvasText; }
.zeitblock__zahl { border: 1px solid CanvasText; }
}
+
+
+/* =====================================================================
+ CALLS & PROTOKOLLE — übersichtlicher (07.09.2026)
+
+ Wunsch Filipe: "ich will dass du diese komplette seite viel
+ übersichtlicher machst ... total übersichtlich und hoch profissionell."
+
+ ---------------------------------------------------------------------
+ WAS AN DER SEITE UNÜBERSICHTLICH WAR
+
+ Nicht die Menge -- 27 anstehende Gespräche sind viele, aber sie waren
+ schon sauber gruppiert. Unübersichtlich waren DREI Dinge, und alle
+ drei sind Abstände, keine Inhalte:
+
+ 1. ALLES STAND GLEICH WEIT AUSEINANDER. Zwischen zwei Karten
+ derselben Woche lag derselbe Abstand wie zwischen zwei Gruppen.
+ Damit sieht man die Gliederung nicht mehr -- man liest eine
+ lange Reihe statt drei Blöcke. Jetzt: 10 px innerhalb einer
+ Gruppe, 32 px zwischen Gruppen, und die Gruppenüberschrift
+ bekommt eine eigene Linie.
+
+ 2. DIE KARTEN WAREN FAST DURCHSICHTIG. Auf einem Bild mit einem
+ großen hellen Logo bedeutet das: Der Untergrund kämpft mit dem
+ Text. Sie bekommen jetzt dasselbe Material wie überall --
+ beleuchtete Platte, Leuchtschiene in der Farbe ihres Zustands.
+
+ 3. DER ZUSTAND STAND NUR IN DER FARBE. "Protokoll fehlt" war ein
+ bernsteinfarbener Strich am Rand. Farbe allein ist nie genug --
+ die Schiene bleibt, aber die Karte trägt den Zustand ohnehin
+ schon als Wort in ihrer Marke.
+
+ Die Farben sind dieselben wie im ganzen Haus: Bernstein = wartet,
+ Blau = steht an, Grün = erledigt.
+ ===================================================================== */
+
+.gruppe { margin-bottom: 34px; }
+/* Die Gruppenüberschrift bekommt eine Grundlinie über die ganze Breite
+ -- das ist der billigste und stärkste Trick für Gliederung: Das Auge
+ sieht einen Block, bevor es ein Wort gelesen hat. */
+.gruppe > .stufe {
+ padding-bottom: 10px;
+ border-bottom: 1px solid rgba(150, 186, 220, .12);
+}
+.gruppe__karten { gap: 10px; margin-top: 14px; }
+
+.call {
+ --c: var(--akzent);
+ padding: 16px 18px 16px 22px;
+ border-radius: 3px 16px 16px 3px;
+ border-color: color-mix(in srgb, var(--c) 16%, rgba(150, 186, 220, .12));
+ background:
+ linear-gradient(90deg,
+ color-mix(in srgb, var(--c) 20%, transparent) 0%,
+ color-mix(in srgb, var(--c) 6%, transparent) 14%,
+ color-mix(in srgb, var(--c) 2%, transparent) 30%,
+ transparent 50%),
+ linear-gradient(163deg,
+ rgba(21, 28, 41, .95) 0%,
+ rgba(11, 15, 24, .95) 62%,
+ rgba(8, 11, 18, .95) 100%);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, .075),
+ inset 0 -1px 0 rgba(0, 0, 0, .45),
+ 0 2px 4px -2px rgba(0, 0, 0, .5),
+ 0 12px 26px -18px rgba(0, 0, 0, .85);
+ transition: border-color var(--tempo), transform var(--tempo), box-shadow var(--tempo);
+}
+/* Die Schiene läuft über die ganze Höhe, nicht mit 10 px Luft oben und
+ unten. Ein Strich, der vor der Ecke aufhört, sieht aus wie ein
+ Fehler; einer, der durchgeht, sieht aus wie Bauweise. */
+.call::before {
+ top: 0; bottom: 0; width: 3px;
+ border-radius: 3px 0 0 3px;
+ background: linear-gradient(180deg,
+ color-mix(in srgb, var(--c) 80%, transparent) 0%,
+ color-mix(in srgb, var(--c) 42%, transparent) 100%);
+ opacity: .75;
+ transition: opacity var(--tempo);
+}
+.call[data-gruppe="offen"] { --c: #f0c48a; }
+.call[data-gruppe="anstehend"] { --c: var(--akzent); }
+.call[data-gruppe="erledigt"] { --c: #79d1a2; }
+/* Die alten Sonderregeln setzten Fläche und Strichfarbe getrennt --
+ jetzt steckt beides in --c. Diese eine Zeile ersetzt vier. */
+.call[data-gruppe="offen"]::before,
+.call[data-gruppe="anstehend"]::before,
+.call[data-gruppe="erledigt"]::before { background: linear-gradient(180deg,
+ color-mix(in srgb, var(--c) 80%, transparent) 0%,
+ color-mix(in srgb, var(--c) 42%, transparent) 100%); }
+/* Erledigtes tritt zurück -- aber über die Fläche, nicht über die
+ Schriftdeckkraft. `opacity` auf der ganzen Karte hätte auch den Text
+ erfasst, und genau dieser Fehler hat am 07.09. an anderer Stelle
+ einen Kontrast unter die Schwelle gedrückt. */
+.call[data-gruppe="erledigt"] {
+ opacity: 1;
+ border-color: rgba(150, 186, 220, .10);
+}
+.call[data-gruppe="erledigt"]::before { opacity: .5; }
+
+.call:hover {
+ transform: translateY(-2px);
+ border-color: color-mix(in srgb, var(--c) 34%, rgba(150, 186, 220, .16));
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, .10),
+ inset 0 -1px 0 rgba(0, 0, 0, .45),
+ 0 4px 9px -4px rgba(0, 0, 0, .55),
+ 0 18px 36px -20px rgba(0, 0, 0, .9);
+}
+.call:hover::before { opacity: 1; }
+
+.call__titel { font-size: 1.05rem; font-weight: 700; color: #eaf2fb; }
+.call__zeit { color: var(--text-leise); font-variant-numeric: tabular-nums; }
+
+@media (prefers-reduced-motion: reduce) {
+ .call, .call:hover { transform: none; }
+}
+@media (forced-colors: active) {
+ .call { background: Canvas; box-shadow: none; border: 1px solid CanvasText; }
+}
diff --git a/workspace/assets/css/gate.css b/workspace/assets/css/gate.css
index efc79605..c843f20e 100644
--- a/workspace/assets/css/gate.css
+++ b/workspace/assets/css/gate.css
@@ -1679,3 +1679,74 @@ body.gate {
inset 0 0 18px -8px color-mix(in srgb, var(--akzent) 85%, transparent);
}
}
+
+/* =====================================================================
+ DER TEILEN-KNOPF (07.09.2026)
+
+ Er sitzt links in der rechten Knopfgruppe und sieht aus wie ihre
+ Nachbarn -- dieselbe Höhe, derselbe geschliffene Rand, dieselbe
+ Schrift. Ein Knopf, der aus der Reihe tanzt, sagt "nachträglich
+ eingebaut", und genau das soll er nicht sagen.
+
+ Der einzige Unterschied ist die Farbe: --f wird auf Violett gesetzt.
+ Alle anderen Knöpfe der Leiste tragen den Hausakzent; ein zweiter
+ Ton macht diesen einen auffindbar, ohne ihn lauter zu machen.
+
+ BEIM GELINGEN WECHSELT DER TEXT, NICHT DIE FARBE ALLEIN. "Kopiert"
+ steht als Wort da -- eine Rückmeldung, die nur aus einem Farbwechsel
+ besteht, kommt bei niemandem an, der Farben schlecht unterscheidet.
+ ===================================================================== */
+.teilen {
+ --f: var(--violett);
+ position: relative;
+ display: inline-flex; align-items: center; justify-content: center; gap: 8px;
+ min-height: 38px; padding: 8px 14px;
+ border: 1px solid transparent;
+ border-radius: 11px;
+ background:
+ linear-gradient(#121926, #0d1420) padding-box,
+ linear-gradient(150deg,
+ color-mix(in srgb, var(--f) 70%, transparent),
+ rgba(255, 255, 255, .07) 52%,
+ color-mix(in srgb, var(--f) 28%, transparent)) border-box;
+ color: color-mix(in srgb, var(--f) 45%, #ffffff);
+ font: inherit; font-size: .85rem; font-weight: 600;
+ cursor: pointer;
+ transition: transform .16s ease, box-shadow .2s ease, color .16s ease;
+}
+.teilen:hover {
+ color: #fff;
+ transform: translateY(-1px);
+ box-shadow: 0 8px 20px -12px color-mix(in srgb, var(--f) 85%, transparent);
+}
+.teilen:active { transform: translateY(0); }
+.teilen:focus-visible { outline: 2px solid var(--f); outline-offset: 2px; }
+/* Geschafft: der Rand wird grün, das Wort sagt es zusätzlich. */
+.teilen[data-fertig="ja"] {
+ --f: var(--k-gut);
+ color: color-mix(in srgb, var(--k-gut) 40%, #ffffff);
+}
+.teilen__zeichen {
+ width: 16px; height: 16px; flex: none;
+ fill: none; stroke: currentColor;
+ stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;
+}
+
+@media (max-width: 720px) {
+ /* Auf schmalen Geräten nur das Zeichen. Die Kopfleiste ist am
+ 06.09.2026 schon einmal an einem Element zu viel zerbrochen --
+ dieser Knopf kommt deshalb von vornherein mit 38 px aus, statt
+ eine feste Umbruchschwelle noch einmal zu verschieben. */
+ .teilen { padding: 8px 10px; }
+ .teilen__text {
+ position: absolute;
+ width: 1px; height: 1px; margin: -1px; padding: 0;
+ overflow: hidden; clip-path: inset(50%); white-space: nowrap;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ .teilen, .teilen:hover, .teilen:active { transform: none; }
+}
+@media (forced-colors: active) {
+ .teilen { border: 1px solid ButtonText; background: ButtonFace; color: ButtonText; }
+}
diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css
index d903f42d..f3389ea4 100644
--- a/workspace/assets/css/start.css
+++ b/workspace/assets/css/start.css
@@ -646,6 +646,15 @@ body.start > main { position: relative; z-index: 1; }
gap: 12px; flex-wrap: wrap; margin-bottom: 8px;
}
.klapp-kopf > .feldschild, .klapp-kopf > .block__titel { margin: 0; }
+/* Zwei Knöpfe rechts im Kopf einer Liste: der stille Schalter und die
+ Hauptaktion. Eigener Behälter, damit sie beim Umbruch ZUSAMMEN
+ umbrechen -- sonst steht auf schmalen Geräten der Schalter allein in
+ einer Zeile und die Hauptaktion in der nächsten. */
+.klapp-kopf__knoepfe {
+ display: flex; align-items: center; gap: 10px;
+ flex-wrap: wrap; justify-content: flex-end;
+ margin-left: auto;
+}
.klapp-pfeil {
width: 14px; height: 14px; flex: 0 0 auto;
@@ -659,6 +668,15 @@ body.start > main { position: relative; z-index: 1; }
statt ueber das Entfernen von Zeilen: Das Aufklappen braucht dann
weder einen neuen Aufbau noch eine zweite Abfrage. */
[data-klapp="zu"] > *:nth-child(n+5) { display: none; }
+/* SIEBEN STATT VIER -- fuer Listen, bei denen die Woche die Einheit ist
+ (07.09.2026, Wunsch Filipe zur Tagesliste: "man soll immer noch die
+ letzten 7 tage sehen"). Vier Tage sind kein Zeitraum, ueber den man
+ etwas sagen kann; sieben sind eine Woche und damit vergleichbar.
+
+ Eine zweite Regel und keine gerechnete Zahl: `nth-child` nimmt keine
+ CSS-Variable. Zwei feste Werte, die beide im Quelltext stehen, sind
+ ehrlicher als eine Zahl, die man im Stil sucht und im Skript findet. */
+[data-klapp="zu7"] > *:nth-child(n+8) { display: none; }
.willkommen { margin-bottom: 38px; }
.willkommen .titel { margin-bottom: 6px; }
@@ -1231,7 +1249,12 @@ body.start > main { position: relative; z-index: 1; }
/* Die Seite ist breit, damit die Kacheln Platz haben. Eine Zeile Text
ueber 1240 px zu ziehen macht sie aber nicht lesbarer, sondern
anstrengender -- Hinweise und Begruessung behalten deshalb ihr Mass. */
-.willkommen, .dran__liste, .inhalt .fuss { max-width: 940px; }
+/* .willkommen ist seit dem 07.09.2026 eine KACHEL und keine Textzeile
+ mehr. Eine Textzeile begrenzt man auf 940 px, weil lange Zeilen
+ schlecht zu lesen sind; eine Kachel, die 200 px vor allen anderen
+ endet, sieht dagegen aus, als sei sie danebengerutscht. Der Text
+ darin bleibt kurz -- die Begrenzung hat er nie gebraucht. */
+.dran__liste, .inhalt .fuss { max-width: 940px; }
.dran { margin-bottom: 30px; }
.dran__liste { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
@@ -4240,3 +4263,259 @@ body[data-fremde-sicht="ja"]::after {
.kachel > .kachel__link::before,
.kachel__zeichen::after { display: none; }
}
+
+/* =====================================================================
+ DIE BEGRÜSSUNGSKACHEL — mit Uhr und Glocke (07.09.2026)
+
+ Wunsch Filipe: "ich will dass diese kacheln da auch übertrieben geil
+ gemacht wird, wirklich seeeeeehr hochwertig mit einer runden
+ digitalen uhr rechts in der kachel ... und der benachrichtigungs
+ button soll auch von der leiste in diese kachel gesetzt werden."
+
+ ---------------------------------------------------------------------
+ WARUM DIE UHR HIER RICHTIG IST UND NICHT NUR SCHÖN
+
+ Eine Uhr ist die einzige Angabe auf dieser Seite, die sich ändert,
+ während man hinsieht. Genau das macht sie wertvoll: Sie sagt ohne ein
+ Wort, dass die Seite LEBT -- dass die Zahlen daneben von jetzt sind
+ und nicht von gestern Nacht. Eine Oberfläche, bei der man das nicht
+ weiß, prüft man ständig nach.
+
+ Und sie steht neben dem Datum, das ohnehin dort steht. Zwei Angaben
+ zur selben Frage gehören zusammen; das Datum oben und die Uhrzeit in
+ der Fußzeile wären zwei halbe Antworten.
+
+ ---------------------------------------------------------------------
+ ZWEI RINGE, NICHT EINER
+
+ Der innere Ring füllt sich in einer Minute, der äußere in einer
+ Stunde. Das ist der Unterschied zwischen "eine Uhr, die tickt" und
+ "eine Uhr, an der man den Stand der Stunde sieht" -- man erkennt aus
+ dem Augenwinkel, ob die Stunde jung ist oder gleich vorbei.
+
+ Der Sekundenring läuft NICHT weich. Ein weicher Zeiger sieht teurer
+ aus, kostet aber eine Bildberechnung pro Bild statt einer pro
+ Sekunde, und auf einer Seite, die stundenlang offen steht, ist das
+ der Unterschied zwischen einem warmen und einem kalten Rechner.
+ Der Sprung ist außerdem ehrlicher: Die Anzeige ist digital.
+ ===================================================================== */
+
+/* Die Kachel selbst: dasselbe Material wie die Bereichskacheln, damit
+ nicht zwei Oberflächen nebeneinanderliegen. Sie hat KEINE eigene
+ Farbschiene -- sie gehört zu keiner Kategorie, sie ist der Anfang. */
+.willkommen {
+ position: relative;
+ padding: 22px 24px;
+ border: 1px solid var(--rand);
+ border-radius: 4px 20px 20px 4px;
+ background:
+ linear-gradient(90deg,
+ rgba(63, 189, 245, .10) 0%,
+ rgba(63, 189, 245, .03) 14%,
+ transparent 40%),
+ linear-gradient(163deg,
+ rgba(23, 31, 45, .95) 0%,
+ rgba(11, 15, 24, .95) 58%,
+ rgba(8, 11, 18, .95) 100%);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, .10),
+ inset 0 -1px 0 rgba(0, 0, 0, .50),
+ inset 2px 0 0 -1px color-mix(in srgb, var(--akzent) 34%, transparent),
+ 0 2px 5px -2px rgba(0, 0, 0, .55),
+ 0 16px 34px -20px rgba(0, 0, 0, .88);
+ overflow: hidden;
+}
+/* Die Leuchtschiene links -- dieselbe Sprache wie überall, hier im
+ Hausakzent statt in einer Kategoriefarbe. */
+.willkommen::before {
+ content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
+ border-radius: 4px 0 0 4px;
+ background: linear-gradient(180deg,
+ color-mix(in srgb, var(--akzent) 55%, #fff) 0%,
+ var(--akzent) 26%,
+ color-mix(in srgb, var(--akzent) 50%, transparent) 100%);
+ box-shadow:
+ 0 0 9px 0 color-mix(in srgb, var(--akzent) 55%, transparent),
+ 3px 0 20px -3px color-mix(in srgb, var(--akzent) 42%, transparent);
+ opacity: .9;
+ z-index: 2;
+}
+/* Körnung und ein fester Glanz. Kein wandernder wie bei den Kacheln:
+ Diese Fläche wird nicht angeklickt, also gibt es nichts zu quittieren. */
+.willkommen::after {
+ content: ""; position: absolute; inset: 0; pointer-events: none;
+ border-radius: inherit;
+ background:
+ linear-gradient(104deg,
+ transparent 26%,
+ rgba(255, 255, 255, .05) 40%,
+ rgba(255, 255, 255, .085) 47%,
+ rgba(255, 255, 255, .03) 55%,
+ transparent 70%);
+ /* KEINE Körnung auf DIESER Ebene (07.09.2026, gemessen).
+
+ Sie stand hier als zweite Schicht und hat die Deckkraft der
+ Spiegelung geerbt -- 50 % eines mittelgrauen Rauschens heben jeden
+ Bildpunkt um rund 30 Stufen an. Gemessen: rgb(56,44,58) statt der
+ berechneten rgb(20,26,38), und über einer hellen Stelle des
+ Motivs rgb(85,83,81). Die Karte sah dadurch durchsichtig aus,
+ obwohl sie zu 95 % deckt.
+
+ Derselbe Fehler wie an der Anmeldekarte heute Nachmittag, und
+ genau derselbe Grund: Körnung gehört auf eine EIGENE Ebene mit
+ `mix-blend-mode: overlay` und 3 bis 4 Prozent -- so wie bei den
+ Kacheln der Startseite. Auf einer geteilten Ebene ist sie kein
+ Material, sondern ein grauer Schleier. */
+ opacity: .5;
+}
+.willkommen > * { position: relative; z-index: 1; }
+
+.willkommen__reihe {
+ display: flex; align-items: center; gap: 16px;
+ flex-wrap: wrap;
+}
+.willkommen__text { min-width: 0; flex: 1 1 260px; }
+
+/* Rechts: Glocke neben der Uhr, beide rechtsbündig. `margin-left: auto`
+ statt `justify-content: space-between` am Elternteil -- so bleibt die
+ Gruppe auch dann rechts, wenn der Text davor kurz ist. */
+.willkommen__rechts {
+ display: flex; align-items: center; gap: 14px;
+ margin-left: auto; flex: 0 0 auto;
+}
+/* DER PLATZ WIRD RESERVIERT, NICHT NACHTRÄGLICH GENOMMEN.
+
+ Hier stand `:empty { display: none }` -- und das ist genau der Fehler,
+ den die Kachel darunter schon einmal gemacht hat: Die Glocke wird
+ erst nach der ersten Abfrage eingehängt, und bis dahin war der Platz
+ 0 px breit. In dem Moment, in dem sie erscheint, wächst die Zeile,
+ und alles darunter rutscht.
+
+ Ein Sprung ist kein Schönheitsfehler, sondern der Grund, warum man
+ auf den falschen Knopf drückt: Man zielt auf etwas, und im Moment des
+ Klicks rutscht es weg. Gemessen von pruef-tempo-workspace.
+
+ Also 46 px, immer -- ob die Glocke kommt oder nicht. Auf Geräten, auf
+ denen es sie gar nicht gibt (iPhone im Browser), bleibt ein leerer
+ Streifen neben der Uhr; das ist der Preis, und er ist niedriger als
+ ein springendes Bild. */
+.glocke-platz { width: 46px; height: 46px; flex: none; }
+
+/* ---------- Die Uhr -------------------------------------------------- */
+.uhr {
+ position: relative;
+ width: 108px; height: 108px; flex: none;
+ display: grid; place-items: center;
+ border-radius: 50%;
+ /* Das Gehäuse: außen ein Lichtsaum, innen eine dunkle Scheibe. */
+ background:
+ radial-gradient(circle at 50% 34%, rgba(255, 255, 255, .05), transparent 62%),
+ radial-gradient(circle at 50% 50%, #0d131f 62%, #070a11 100%);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, .12),
+ inset 0 -2px 6px -3px rgba(0, 0, 0, .9),
+ 0 0 0 1px rgba(150, 186, 220, .10),
+ 0 10px 24px -14px rgba(0, 0, 0, .9);
+}
+/* Die Skala: sechzig feine Striche, aus EINEM sich wiederholenden
+ Kegelverlauf. Sechzig Elemente im HTML wären dieselbe Optik mit
+ sechzig Elementen mehr im Baum. */
+.uhr__skala {
+ position: absolute; inset: 7px; border-radius: 50%;
+ background: repeating-conic-gradient(from 0deg,
+ rgba(150, 186, 220, .30) 0deg 0.5deg,
+ transparent 0.5deg 6deg);
+ /* Nur ein schmaler Ring davon ist zu sehen -- der Rest wird
+ ausgestanzt. */
+ -webkit-mask: radial-gradient(circle, transparent 0 76%, #000 76% 84%, transparent 84%);
+ mask: radial-gradient(circle, transparent 0 76%, #000 76% 84%, transparent 84%);
+ opacity: .5;
+ pointer-events: none;
+}
+/* Die Glasscheibe: eine Spiegelung von oben links über der Kuppe. */
+.uhr__glas {
+ position: absolute; inset: 0; border-radius: 50%;
+ background: linear-gradient(148deg,
+ rgba(255, 255, 255, .10) 0%,
+ rgba(255, 255, 255, .025) 22%,
+ transparent 46%);
+ pointer-events: none;
+}
+.uhr__ring {
+ position: absolute; inset: 0; width: 100%; height: 100%;
+ /* Bei Null soll der Ring OBEN beginnen, nicht rechts. */
+ transform: rotate(-90deg);
+}
+.uhr__bahn, .uhr__minute, .uhr__zeiger {
+ fill: none; stroke-linecap: round;
+ stroke-dasharray: 276.46;
+}
+.uhr__bahn { stroke: rgba(150, 186, 220, .11); stroke-width: 3.5; }
+/* Der Stundenstand: gedeckt, breit, ruhig -- er ändert sich einmal pro
+ Minute und darf nicht um Aufmerksamkeit bitten. */
+.uhr__minute {
+ stroke: color-mix(in srgb, var(--violett) 62%, transparent);
+ stroke-width: 3.5;
+ stroke-dashoffset: 276.46;
+}
+/* Die Sekunde: schmaler, heller, mit einem Hauch Leuchten. */
+.uhr__zeiger {
+ stroke: var(--akzent); stroke-width: 2.2;
+ stroke-dashoffset: 276.46;
+ filter: drop-shadow(0 0 3px color-mix(in srgb, var(--akzent) 70%, transparent));
+}
+.uhr__mitte { display: grid; justify-items: center; gap: 1px; line-height: 1; }
+.uhr__zeit {
+ font-size: 1.42rem; font-weight: 700; letter-spacing: -.01em;
+ /* Gleiche Zifferbreite: Ohne sie zappelt die Anzeige bei jedem
+ Minutenwechsel, weil die 1 schmaler ist als die 8. */
+ font-variant-numeric: tabular-nums;
+ color: #eaf2fb;
+ text-shadow: 0 0 18px rgba(63, 189, 245, .28);
+}
+.uhr__sek {
+ font-size: .72rem; font-weight: 600;
+ font-variant-numeric: tabular-nums;
+ letter-spacing: .16em; text-indent: .16em;
+ color: color-mix(in srgb, var(--akzent) 55%, #ffffff);
+}
+
+/* ---------- Die Glocke, wenn sie in der Kachel sitzt ------------------ */
+/* In der Leiste ist sie ein Knopf unter Knöpfen. Hier steht sie allein
+ neben der Uhr und wird deshalb rund -- sie nimmt die Form ihrer
+ Nachbarin auf, statt als Rechteck danebenzustehen. */
+.glocke--kachel {
+ width: 46px; height: 46px; padding: 0;
+ border-radius: 50%;
+ justify-content: center;
+}
+.glocke--kachel .glocke__text {
+ /* Der Text bleibt für Vorleseprogramme da, verschwindet aber vom
+ Bildschirm: Neben einer Uhr ist Platz für ein Zeichen, nicht für
+ eine Beschriftung. */
+ position: absolute;
+ width: 1px; height: 1px; margin: -1px; padding: 0;
+ overflow: hidden; clip-path: inset(50%); white-space: nowrap;
+}
+.glocke--kachel .glocke__zeichen { width: 19px; height: 19px; }
+
+@media (max-width: 620px) {
+ /* Auf dem Handy rückt die Gruppe unter den Gruß und die Uhr wird
+ kleiner -- 108 px neben einer Überschrift lassen für den Namen
+ keine Zeile mehr übrig. */
+ .willkommen { padding: 18px 16px; }
+ .willkommen__rechts { margin-left: 0; width: 100%; justify-content: flex-start; }
+ .uhr { width: 88px; height: 88px; }
+ .uhr__zeit { font-size: 1.18rem; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* Die Uhr springt ohnehin -- hier gibt es nichts abzuschalten. Die
+ Zeile steht trotzdem da, damit der nächste, der einen weichen
+ Zeiger einbaut, sie findet. */
+ .uhr__zeiger, .uhr__minute { transition: none; }
+}
+@media (forced-colors: active) {
+ .uhr { background: Canvas; box-shadow: none; border: 1px solid CanvasText; }
+ .uhr__skala, .uhr__glas { display: none; }
+}
diff --git a/workspace/assets/js/automation.js b/workspace/assets/js/automation.js
index fd75eb5a..1ce418b0 100644
--- a/workspace/assets/js/automation.js
+++ b/workspace/assets/js/automation.js
@@ -267,11 +267,31 @@
$('sicherungen-schalter').hidden = true;
return;
}
+ /* DAS DATUM WIRD GESUCHT, NICHT ZUSAMMENGESETZT (07.09.2026).
+
+ Hier stand `name.split('-').slice(1).reverse().join('.')`. Bei
+ "woechentlich-2026-09-06.db" ging das gut. Die Sicherungen vor
+ einem Umbau heissen aber "vorher-2026-09-02-160012438.db" -- mit
+ einem Zeitstempel hinten dran. Aus derselben Rechnung wurde dann
+ "160012438.02.09.2026": eine Zeichenkette, die dreimal so lang
+ ist wie die Spalte und ueber den Nachbartext lief. Genau das hat
+ Filipe im Bildschirmfoto gesehen.
+
+ Ein Muster, das die Bestandteile ZAEHLT statt sie zu SUCHEN,
+ bricht beim ersten Namen, der einen Teil mehr hat. Deshalb jetzt
+ ein Suchmuster nach vier-zwei-zwei Ziffern -- egal, was davor
+ oder dahinter steht. */
for (const s of z.sicherungen) {
+ const roh = s.name.replace(/\.db$/, '');
+ const t = roh.match(/(\d{4})-(\d{2})-(\d{2})/);
const zeile = el('div', 'verlauf__zeile');
- zeile.append(el('span', 'verlauf__zeit', s.name.replace(/\.db$/, '').split('-').slice(1).reverse().join('.')));
- zeile.append(el('span', 'verlauf__was', s.name.split('-')[0]));
+ /* Findet sich kein Datum, wird das GESAGT und nicht geraten --
+ ein erfundenes Datum an einer Sicherung waere schlimmer als
+ ein Strich. */
+ zeile.append(el('span', 'verlauf__zeit', t ? `${t[3]}.${t[2]}.${t[1]}` : '—'));
+ zeile.append(el('span', 'verlauf__was', roh.split('-')[0]));
zeile.append(el('span', 'verlauf__wer', bytes(s.groesse)));
+ zeile.title = s.name; // der volle Name bleibt erreichbar
liste.append(zeile);
}
window.klappe?.(liste, $('sicherungen-schalter'), $('sicherungen-schalter-text'));
diff --git a/workspace/assets/js/bereiche.js b/workspace/assets/js/bereiche.js
index 30d09b73..53f1985d 100644
--- a/workspace/assets/js/bereiche.js
+++ b/workspace/assets/js/bereiche.js
@@ -364,14 +364,25 @@
(Orangerot) darunter. Wer diese Gruppe umsortiert, lässt
tools/kachel-farben.mjs neu laufen -- die Nachbarschaften
haben sich durch diese Kachel geändert. */
- { name: 'Zahlen', unter: 'Diamanten, LIVE-Tage, Verweildauer', zeichen: 'zahlen', ton: 2,
- rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'leistung.html', szene: 'arena' },
+ /* REIHENFOLGE: STECKBRIEF, PROFILE, ZAHLEN (Wunsch Filipe,
+ 07.09.2026: "zuerst steckbrief, dan, profile, und dan erst
+ zahlen"). Das ist auch die Reihenfolge, in der man arbeitet:
+ erst wer jemand IST, dann was er vorhat, dann was dabei
+ herauskommt. Die Zahlen zuerst zu zeigen dreht das um. */
{ name: 'Mein Steckbrief', unter: 'Dein Bild und deine Kanäle', zeichen: 'steckbrief', ton: 10,
rollen: ['admin', 'manager', 'scout'], ziel: 'steckbrief.html', szene: 'halle' },
{ name: 'Creator-Profile', unter: 'Ziele und 90-Tage-Plan', zeichen: 'profil', ton: 20,
rollen: ['admin', 'manager', 'scout'], ziel: 'profil.html', szene: 'halle' },
- { name: 'Mein Profil', unter: 'Dein Steckbrief, Ziele und Pläne', zeichen: 'profil', ton: 20,
+ /* KEINE eigene Steckbrief-Kachel fuer Creator, und das ist
+ Absicht: Ihr Steckbrief samt Profilbild steht bereits auf
+ `profil.html` (der Block mit `data-nur-creator="ja"`). Eine
+ zweite Kachel fuehrte auf dieselbe Sache unter anderem Namen
+ -- und wer zwei Wege zu einer Einstellung anbietet, bekommt
+ die Frage, welcher der richtige ist. */
+ { name: 'Mein Profil', unter: 'Dein Bild, deine Ziele und Pläne', zeichen: 'profil', ton: 20,
rollen: ['creator'], ziel: 'profil.html', szene: 'halle' },
+ { name: 'Zahlen', unter: 'Diamanten, LIVE-Tage, Verweildauer', zeichen: 'zahlen', ton: 2,
+ rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'leistung.html', szene: 'arena' },
{ name: 'Start-Check', unter: 'Erstanalyse, 16 Punkte', zeichen: 'startcheck', ton: 16,
rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'startcheck.html', szene: 'wald' },
{ name: 'LIVE-Analyse', unter: 'Vorher, während, danach', zeichen: 'live', ton: 3,
diff --git a/workspace/assets/js/glocke.js b/workspace/assets/js/glocke.js
index 92c372c9..2628778c 100644
--- a/workspace/assets/js/glocke.js
+++ b/workspace/assets/js/glocke.js
@@ -174,7 +174,23 @@
}
async function bauen() {
- const kopf = document.querySelector('.kopfleiste__rechts');
+ /* WO DIE GLOCKE HINGEHÖRT (07.09.2026, Wunsch Filipe: "der
+ benachrichtigungs button soll auch von der leiste in diese kachel
+ gesetzt werden").
+
+ Gibt es auf der Seite einen ausgewiesenen Platz, kommt sie
+ dorthin -- auf der Startseite in die Begrüßungskachel, neben die
+ Uhr. Das ist der richtige Ort: Benachrichtigungen sind eine
+ Einstellung zum eigenen Tag, keine Navigation.
+
+ Auf allen anderen Seiten gibt es diesen Platz nicht, und dort
+ bleibt die Leiste. Deshalb ein Rückfall und keine Umstellung:
+ Eine Glocke, die auf zwölf Seiten verschwindet, weil sie auf
+ einer umgezogen ist, wäre keine Verbesserung. Und weil die
+ Kopfleiste am 06.09. schon einmal an einem sechsten Element
+ zerbrochen ist, wird sie hier nebenbei um eines leichter. */
+ const platz = document.getElementById('glocke-platz');
+ const kopf = platz || document.querySelector('.kopfleiste__rechts');
if (!kopf || document.getElementById('glocke')) return;
knopf = el('button', 'glocke');
@@ -196,10 +212,12 @@
knopf.append(el('span', 'glocke__text', '…'));
/* VOR dem Abmelden-Knopf, nicht dahinter: Abmelden ist die letzte
- Handlung auf einer Seite und bleibt ganz rechts. */
- const abmeldenKnopf = kopf.querySelector('.abmelden');
+ Handlung auf einer Seite und bleibt ganz rechts. Im eigenen Platz
+ gibt es nichts, wovor sie stehen müsste. */
+ const abmeldenKnopf = platz ? null : kopf.querySelector('.abmelden');
if (abmeldenKnopf) kopf.insertBefore(knopf, abmeldenKnopf);
else kopf.append(knopf);
+ if (platz) knopf.classList.add('glocke--kachel');
knopf.addEventListener('click', async () => {
const l = lage();
diff --git a/workspace/assets/js/kopf.js b/workspace/assets/js/kopf.js
index f1288342..0f01ada2 100644
--- a/workspace/assets/js/kopf.js
+++ b/workspace/assets/js/kopf.js
@@ -54,8 +54,105 @@
/* Vorleseprogramme bekommen einen Satz statt drei Bruchstücke. */
ziel.setAttribute('aria-label',
`Angemeldet als ${ich.name} in der Rolle ${ich.rolle_name || ich.rolle}`);
+
+ teilenKnopf(ich);
};
+ /* ---------- Teilen ---------------------------------------------------
+
+ Wunsch Filipe (07.09.2026): "einen button oben in der leiste ... zum
+ teilen der seite, auch nur die scouts, manager und dogfather sollen
+ diesen button sehen und benutzen können."
+
+ WAS GETEILT WIRD, IST DIE ADRESSE DES WORKSPACE -- nicht die Seite,
+ auf der man gerade steht. Das ist kein Detail: Wer einen Link auf
+ `bereich.html?b=schutz` weitergibt, schickt jemanden auf eine Seite,
+ die er entweder gar nicht sehen darf oder die ohne Zusammenhang
+ verwirrt. Geteilt wird der Eingang.
+
+ WARUM NUR DIE LEITUNG. Ein Creator hat niemanden, dem er den
+ Workspace geben würde -- er ist der Eingeladene, nicht der
+ Einladende. Ein Knopf, den man hat und nie braucht, ist Ballast;
+ einer, den man drückt und der nichts Sinnvolles tut, ist schlimmer.
+ Durchgesetzt wird das an der Rolle aus /api/ich, also an derselben
+ Angabe, mit der der Server auch alles andere entscheidet.
+
+ ZWEI WEGE, EIN KNOPF. Wo es die Teilen-Funktion des Geräts gibt
+ (Handy, neuere Rechner), wird sie benutzt -- das ist der Weg, den
+ Filipe kennt. Wo nicht, landet die Adresse in der Zwischenablage
+ und der Knopf sagt "Kopiert". Kein Dialog, kein zweiter Klick.
+ Und wo auch das nicht geht (alte Browser, kein sicherer Kontext),
+ erscheint der Knopf gar nicht erst -- ein dritter Ausgang statt
+ einer Schaltfläche, die nichts tut. */
+ const DARF_TEILEN = new Set(['admin', 'manager', 'scout']);
+
+ function teilenKnopf(ich) {
+ const leiste = document.querySelector('.kopfleiste__rechts');
+ if (!leiste || document.getElementById('teilen')) return;
+ if (!DARF_TEILEN.has(ich.rolle)) return;
+ const kannTeilen = typeof navigator.share === 'function';
+ const kannKopieren = !!navigator.clipboard?.writeText;
+ if (!kannTeilen && !kannKopieren) return;
+
+ const k = document.createElement('button');
+ k.type = 'button';
+ k.id = 'teilen';
+ k.className = 'teilen';
+ k.title = 'Adresse des Workspace weitergeben';
+
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ svg.setAttribute('class', 'teilen__zeichen');
+ svg.setAttribute('viewBox', '0 0 24 24');
+ svg.setAttribute('aria-hidden', 'true');
+ /* Drei Knoten, zwei Linien -- das Zeichen für "weitergeben". Als
+ Pfade und nicht als Schriftzeichen: Ein Emoji sieht auf jedem
+ Gerät anders aus und lässt sich nicht einfärben. */
+ svg.innerHTML =
+ '