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 = + '' + + '' + + ''; + k.append(svg); + + const text = document.createElement('span'); + text.className = 'teilen__text'; + text.textContent = 'Teilen'; + k.append(text); + + const ZIEL = location.origin + '/workspace/'; + let laeuft = false; + k.addEventListener('click', async () => { + if (laeuft) return; + laeuft = true; + try { + if (kannTeilen) { + await navigator.share({ + title: 'Creator Workspace', + text: 'Der Creator Workspace von DogFather Universe', + url: ZIEL, + }); + } else { + await navigator.clipboard.writeText(ZIEL); + const vorher = text.textContent; + text.textContent = 'Kopiert'; + k.dataset.fertig = 'ja'; + setTimeout(() => { text.textContent = vorher; delete k.dataset.fertig; }, 2200); + } + } catch (f) { + /* Ein Abbruch durch die Nutzerin ist KEIN Fehler und darf nicht + wie einer aussehen -- sie hat den Dialog absichtlich + geschlossen. Alles andere wird gemeldet. */ + if (f?.name !== 'AbortError') { + text.textContent = 'Ging nicht'; + setTimeout(() => { text.textContent = 'Teilen'; }, 2200); + } + } finally { laeuft = false; } + }); + + /* Ganz links in der rechten Gruppe: Teilen ist eine Nebenhandlung + und darf dem Abmelden nicht den Platz nehmen. */ + leiste.prepend(k); + } + /* ---------- Auf- und Zuklappen langer Listen ------------------------- Lange Protokoll-Listen sind Nachschlagewerk, kein Startbild: Sie @@ -69,14 +166,23 @@ Kinder die Zeilen sind), den Knopf und das Textfeld darin. Das Ausblenden macht CSS ueber [data-klapp="zu"] -- ohne Neuaufbau und ohne zweite Abfrage. */ + /* Zwei Sichtbarkeitsstufen, beide mit einer Regel im CSS: + 4 der Normalfall -- eine Liste zum Nachschlagen + 7 wo die Woche die Einheit ist (die Tagesliste der Zahlen) + Die Zahl ist der Schluessel zum passenden `data-klapp`-Wert; eine + unbekannte Zahl faellt auf 4 zurueck, statt still gar nichts zu + verbergen. */ + const KLAPP_STUFEN = { 4: 'zu', 7: 'zu7' }; const KLAPP_ZU = 4; /* dieselbe Zahl steht im CSS als nth-child(n+5) */ - window.klappe = function (liste, knopf, textfeld) { + window.klappe = function (liste, knopf, textfeld, wieviele = KLAPP_ZU) { if (!liste || !knopf) return; const anzahl = liste.children.length; + const stufe = KLAPP_STUFEN[wieviele] || 'zu'; + const zeigt = KLAPP_STUFEN[wieviele] ? wieviele : KLAPP_ZU; /* Ein Knopf, der nichts zu verbergen hat, verwirrt nur. */ - knopf.hidden = anzahl <= KLAPP_ZU; + knopf.hidden = anzahl <= zeigt; if (knopf.hidden) { liste.removeAttribute('data-klapp'); return; } /* Liest die Anzahl bei jedem Aufruf frisch aus der Liste, statt sie @@ -86,13 +192,13 @@ an einer Liste mit dreissig Zeilen. */ const stand = (offen) => { if (offen) liste.removeAttribute('data-klapp'); - else liste.dataset.klapp = 'zu'; + else liste.dataset.klapp = stufe; knopf.setAttribute('aria-expanded', offen ? 'true' : 'false'); /* Der Knopf sagt, was er TUT, nicht wo man gerade ist -- und nennt die Zahl, damit man weiss, was dahintersteckt. */ if (textfeld) { textfeld.textContent = offen - ? `Nur die letzten ${KLAPP_ZU}` + ? `Nur die letzten ${zeigt}` : `Alle ${liste.children.length} zeigen`; } }; diff --git a/workspace/assets/js/leistung.js b/workspace/assets/js/leistung.js index 0274a014..f8c8b1e4 100644 --- a/workspace/assets/js/leistung.js +++ b/workspace/assets/js/leistung.js @@ -271,6 +271,22 @@ } ziel.append(zeile); } + + /* ZUGEKLAPPT AUF SIEBEN TAGE (07.09.2026, Wunsch Filipe). + + Vierzehn Zeilen sind zu viel fuers Hinsehen und zu wenig fuers + Nachschlagen -- man scrollt an ihnen vorbei, statt sie zu lesen. + Sieben ist die Woche: der kuerzeste Zeitraum, ueber den man + ueberhaupt etwas sagen kann, und genau der, mit dem der + Wochenbericht rechnet. Die restlichen sieben Tage sind einen + Klick entfernt und verschwinden nicht. + + Die Sieben steht nicht hier, sondern als eigene Regel im CSS + (`[data-klapp="zu7"]`) -- der Schalter kennt beide Stufen und + benennt sie im Knopftext, damit niemand raten muss, wie viel + gerade verborgen ist. */ + window.klappe?.(ziel, document.getElementById('tage-schalter'), + document.getElementById('tage-schalter-text'), 7); } function wert(name, text) { diff --git a/workspace/assets/js/start.js b/workspace/assets/js/start.js index 6a0fc903..295f218f 100644 --- a/workspace/assets/js/start.js +++ b/workspace/assets/js/start.js @@ -278,6 +278,46 @@ 0: 'Wochenende', }; + /* ---------- Die Uhr in der Begrüßungskachel ------------------------- + Zwei Ringe um dieselbe Mitte: der äußere zeigt die Minute in der + Stunde, der innere die Sekunde. Beides als SVG-Kreis mit + `stroke-dasharray` -- ein Kegelverlauf (conic-gradient) hätte an + der Naht eine sichtbare Kante, ein Kreisbogen nicht. + + UMFANG = 2 * pi * 44 = 276.46. Die Zahl steht hier ausgerechnet und + nicht als Formel im Stil: Ändert jemand den Radius im HTML, fällt + der Ring sofort auf, statt sich stillschweigend zu verrechnen. + + KEIN setInterval(…, 1000). Ein Takt von genau einer Sekunde läuft + mit der Zeit aus dem Tritt (jeder Durchlauf kostet ein paar + Millisekunden), und die Anzeige springt dann gelegentlich zwei + Sekunden weiter. Stattdessen wird jedes Mal bis zur NÄCHSTEN vollen + Sekunde gewartet -- die Anzeige bleibt für immer im Takt der + Systemuhr. */ + const UHR_UMFANG = 276.46; + function uhrStarten() { + const zeit = $('uhr-zeit'), sek = $('uhr-sek'); + const zeiger = $('uhr-zeiger'), minute = $('uhr-minute'); + if (!zeit || !zeiger) return; + const p = (n) => String(n).padStart(2, '0'); + + const takt = () => { + const j = new Date(); + zeit.textContent = `${p(j.getHours())}:${p(j.getMinutes())}`; + sek.textContent = p(j.getSeconds()); + /* Voll = kein Versatz. Der Ring wächst im Uhrzeigersinn, weil das + SVG im Stil um 90 Grad zurückgedreht ist. */ + zeiger.style.strokeDashoffset = String(UHR_UMFANG * (1 - j.getSeconds() / 60)); + if (minute) minute.style.strokeDashoffset = String(UHR_UMFANG * (1 - j.getMinutes() / 60)); + /* Der volle Zeitpunkt für Vorleseprogramme -- sichtbar steht dort + nur die Uhrzeit, vorgelesen wird der ganze Satz. */ + const uhr = $('uhr'); + if (uhr) uhr.setAttribute('aria-label', `${p(j.getHours())}:${p(j.getMinutes())} Uhr`); + setTimeout(takt, 1000 - (Date.now() % 1000) + 8); + }; + takt(); + } + function grussZeigen() { const jetzt = new Date(); const stunde = jetzt.getHours(); @@ -391,6 +431,7 @@ window.werZeigen?.(ich); grussZeigen(); + uhrStarten(); /* Die Hinweise zuerst -- die Kacheln brauchen ihre Zahlen. Die Tagestermine laufen daneben her: Sie sind die rechte Hälfte diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 2a4fff8b..ad663fa4 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -9,9 +9,9 @@ - - - + + + @@ -211,10 +211,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 4777c355..a90558bd 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -134,10 +134,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 6889fbd5..2dcd8d81 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -143,14 +143,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 5a2b38ab..6ccbae68 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -76,11 +76,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index c995870d..875f4432 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -9,16 +9,16 @@ - - + + - - + + @@ -154,9 +154,9 @@ - - - - + + + + diff --git a/workspace/content.html b/workspace/content.html index 18215a9f..3db4f087 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -166,12 +166,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index 5a21e1e0..ebb1ef41 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -95,10 +95,10 @@ - - - - - + + + + + diff --git a/workspace/index.html b/workspace/index.html index 25422b80..465048ab 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -27,7 +27,7 @@ - + @@ -173,6 +173,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index 2eb42fea..2385e3f6 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -257,11 +257,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index e73258f9..fe7a3501 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -77,7 +77,19 @@ @@ -199,10 +211,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index e9304afe..f3446405 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -123,10 +123,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 00148e22..af5cb926 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -201,11 +201,11 @@ - - - - - - + + + + + + diff --git a/workspace/report.html b/workspace/report.html index 3c76c7a0..0960292f 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -93,11 +93,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 1ba30d96..229b594d 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -126,10 +126,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index e856a71f..a725c6dd 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -9,8 +9,8 @@ - - + + @@ -51,6 +51,33 @@

+ + +
+
+ + +
+ + + +
+ --:-- + -- +
+
+
@@ -160,10 +187,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index a9297dac..4e4b067d 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -72,11 +72,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 2cb92182..ca7ea8fa 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -113,10 +113,10 @@ - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index c6a1e8bd..9e338d26 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -9,9 +9,9 @@ - - - + + + @@ -82,10 +82,10 @@ - + - - - + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index ed158d93..d2f20127 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -177,10 +177,10 @@ - - - - - + + + + +