Agentur-Events: die Karte als Aushang, Aufgaben zum Abhaken

Filipe, mit einem Bildschirmfoto des Oktober-Events: "ich will das viel
geiler viel perfekter. und auch wenn man die eintraegt und so soll viel
mehr perfektioniert und uebersichtlicher sein."

VORHER GEMESSEN (1280 px, eine einzige Eventkarte):

  Kartenhoehe            1196 px   -- das Fenster ist 900 px
  davon Banner            638 px   -- 53 %, ohne einen Buchstaben
  Titel steht bei        1482 px   -- zweimal scrollen bis zum Namen
  Titelgroesse          15,36 px   -- 1,4 px mehr als der Fliesstext
  Etikett               10,88 px   -- unter der eigenen Grenze (11,5)
  Datum                     3 x    -- in drei Zeilen untereinander
  Knoepfe              4 x gleich  -- "loeschen" wie "zur Aufgabe"

NACHHER, dieselbe Messung: Karte 618 px, Buehne 203 px, Titel 75 px
unter der Kartenkante und 24,8 px gross, Etikett 12 px, das Datum
einmal, "loeschen" abgesetzt.

DIE KARTE
Das Banner ist nicht mehr ein Anhang ueber dem Titel, sondern die
Buehne dahinter -- Hoehe gedeckelt, Titel darauf. Dazu die Frage, die
bei einem Wettbewerb wirklich zaehlt und bisher nirgends beantwortet
wurde: "noch 12 Tage", mit Zeitbalken. Ein Knopf "Banner ganz" holt das
vollstaendige Bild zurueck, das beim Umbau sonst verloren gegangen
waere -- bei Filipes Banner steht die Ansage IM Bild.

DIE AUFGABEN
Aus Fliesstext wird eine Liste, und jeder hakt fuer sich ab (neue
Tabelle event_punkte, Schluessel aus dem Zeilentext). Umsortieren
laesst den Haken, wo er ist; wird die Bedingung selbst umgeschrieben,
faellt er -- beides nachgemessen. Die Leitung sieht, wer wie weit ist,
eine Creatorin sieht diese Liste gar nicht erst.

DAS FORMULAR
Ein Feld je Zeile statt eines leeren Textfeldes. Im echten Event stand
".Jeden Tag Live gehen" neben ". 33k Diamanten erreichen" -- einmal mit
Leerzeichen, einmal ohne. Das ist die zwangslaeufige Folge davon, dass
die Aufzaehlungszeichen von Hand getippt werden; jetzt setzt sie das
Formular. Dazu "Laeuft 14 Tage.", eine Warnung bei verdrehtem Zeitraum
und eine Vorschau, die dieselbe Funktion benutzt wie die echte Karte.

NEBENBEI GEFUNDEN UND BEHOBEN
* Das Banner kam bei Creatorn mit 404 zurueck -- also an genau der
  Karte, die fuer sie gemacht ist. Die Regel "wer das Brett sieht,
  sieht das Bild" galt nur fuer die sieben Community-Bretter. Jetzt
  fragt der Bildweg dieselbe Regel wie das Brett (darfEintragSehen);
  die Gegenprobe zeigt, dass ein fremder Eintrag weiterhin 404 gibt.
* Zwei Stellen setzten das Formular zurueck, die kuerzere liess
  Vorschau und Zeilen-Editor stehen -- beim naechsten "Neuer Eintrag"
  standen die Aufgaben des vorigen Events noch da.
* Der Schriftgrund ragte auf dem Handy 6 px ueber die Karte (feste
  -20 px gegen 14 px Polsterung); jetzt an die Polsterung gekoppelt.

GEPRUEFT
pruef-eventkarte.mjs (neu): 77 Pruefungen, 0 Fehler -- mit Gegenproben
zu jeder Zusage und einer Kontrastmessung am Bildpunkt auf einem
weissen Banner (14,7:1; ohne den Schriftgrund 1,05:1).
pruef-agentur.mjs auf die neuen Bausteine nachgezogen, Zahl unveraendert
bei 62. pruef-eintrag-bild.mjs 24/0.

OFFEN, NICHT AUS DIESEM UMBAU: Das Brett `agentur` ist auf der
Crew-Adresse erreichbar. Zweimal gemessen, mit und ohne diese
Aenderung -- gleiches Ergebnis. Gehoert zur Trennungsregel vom
24.09.2026 und wird getrennt entschieden.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-03 19:02:05 +02:00
co-authored by Claude Opus 5
parent ae2f12c67d
commit 3e1e0810ba
54 changed files with 3773 additions and 798 deletions
+896 -76
View File
@@ -543,85 +543,28 @@
Zustand zusaetzlich als WORT da ("läuft bis 30.09.2026").
===================================================================== */
.event-zeitraum {
display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
margin: 2px 0 10px;
font-size: .82rem;
}
.event-zeitraum__marke {
--f: var(--k-neutral);
display: inline-flex; align-items: center; gap: 6px;
padding: 3px 10px;
border: 1px solid color-mix(in srgb, var(--f) 34%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--f) 12%, transparent);
color: color-mix(in srgb, var(--f) 82%, #fff);
font-weight: 600; letter-spacing: .01em;
font-variant-numeric: tabular-nums;
}
/* Der Punkt davor ist reine Zierde und darf deshalb NICHT vorgelesen
werden -- ein Screenreader saehe sonst ein Aufzaehlungszeichen mitten
im Satz. */
.event-zeitraum__marke::before {
content: ""; width: 7px; height: 7px; border-radius: 50%;
background: var(--f); flex: none;
}
.event-zeitraum[data-stand="laeuft"] .event-zeitraum__marke { --f: var(--k-gut); }
.event-zeitraum[data-stand="kommt"] .event-zeitraum__marke { --f: var(--k-mittel); }
.event-zeitraum[data-stand="vorbei"] .event-zeitraum__marke { --f: var(--k-neutral); }
.event-zeitraum__spanne {
color: var(--text-leise);
font-variant-numeric: tabular-nums;
}
/* ---------------------------------------------------------------------
DIE ALTEN BAUSTEINE SIND AM 03.10.2026 WEGGEFALLEN.
/* Ein vorbeigelaufenes Event bleibt lesbar, tritt aber zurueck. Kein
display:none -- was gelaufen ist, will man nachschlagen koennen.
Gedaempft wird nur die Flaeche, nicht die Schrift: Text unter 4.5:1
waere nicht mehr lesbar, und "alt" ist kein Grund, jemanden
auszuschliessen. */
.eintrag-karte[data-event="vorbei"] { opacity: .82; }
Hier standen `.event-zeitraum*` und `.event-block*` -- die
Zeitraumzeile ueber der Karte und die zwei gleich aussehenden
grauen Kaesten fuer Aufgaben und Regeln. Beide erzeugt bereich.js
nicht mehr: Der Zeitraum steht jetzt auf der Buehne (einmal statt
dreimal), und aus den Kaesten sind Listen geworden, die man abhaken
kann. Die neuen Regeln stehen weiter unten unter "AGENTUR-EVENTS:
DIE KARTE ALS AUSHANG".
.event-block {
margin: 10px 0 0;
padding: 10px 12px;
border: 1px solid var(--rand);
border-radius: var(--radius-klein);
background: rgba(255, 255, 255, .022);
}
.event-block__schild {
margin: 0 0 4px;
/* .74rem = 11.84 px, nicht .7rem. Die Schriftgroessen-Pruefung zieht
die Grenze bei 11.5 px, und sie hat recht: In Grossbuchstaben mit
Sperrung wirkt eine Schrift kleiner, als die Zahl sagt -- und das
hier ist die Beschriftung, an der man die Regeln vom Aufgabenteil
unterscheidet. */
font-size: .74rem; font-weight: 700;
letter-spacing: .09em; text-transform: uppercase;
color: var(--text-still);
}
.event-block__text {
margin: 0;
font-size: .87rem; line-height: 1.6; color: var(--text);
/* pre-line, nicht pre-wrap: Zeilenumbrueche bleiben (die Aufzaehlung
ist der Sinn dieser Felder), aber eine versehentlich eingefuegte
Einrueckung aus einer Chatnachricht reisst die Karte nicht auf. */
white-space: pre-line;
overflow-wrap: anywhere;
}
SIE WURDEN ENTFERNT UND NICHT AUFGEHOBEN: Eine Regel, die auf nichts
mehr zeigt, sieht beim naechsten Lesen aus wie eine, die etwas tut
-- und man sucht den Fehler dann an einer Stelle, an der nichts
passiert. Was sie aussagten, steht in der Begruendung der neuen
Regeln; die Geschichte selbst steht in git.
/* .feld-hinweis steht seit dem 07.09.2026 in aufgaben.css -- bei den
uebrigen Formularstilen. Grund: Die Dashboard-Seite braucht ihn jetzt
auch (Creator anlegen), laedt aber diese Datei hier nicht. Ein
Formularbaustein, der in der Bereichsdatei liegt, ist nur so lange
richtig, wie ihn keine zweite Seite braucht. */
@media (max-width: 560px) {
/* Auf dem Handy untereinander: Marke und Spanne nebeneinander
ergaeben bei "vorbei seit 30.09.2026" einen Umbruch mitten im
Datum. */
.event-zeitraum { gap: 6px; }
.event-zeitraum__spanne { flex-basis: 100%; }
}
Ebenfalls weg: `.eintrag-karte[data-event="vorbei"] { opacity: .82 }`.
Ein abgelaufenes Event war damit kaum von einem laufenden zu
unterscheiden, und der Text wurde schlechter lesbar, ohne dass es
etwas brachte. Jetzt tritt nur das BILD zurueck.
--------------------------------------------------------------------- */
/* =====================================================================
RÜCKMELDUNG: „fürs Team" oder „nur an DogFather" (10.09.2026)
@@ -2195,3 +2138,880 @@ body[data-ton] .kopf-zeile::before {
display: grid; gap: 10px;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
/* =====================================================================
AGENTUR-EVENTS: DIE KARTE ALS AUSHANG (03.10.2026)
Filipe, mit einem Bildschirmfoto des Oktober-Events: "ich will das
viel geiler viel perfekter. und auch wenn man die eintraegt und so
soll viel mehr perfektioniert und uebersichtlicher sein. mach was
hoch profissionelles bitte."
WAS VORHER GEMESSEN WURDE (1280 px, eine einzige Karte):
Kartenhoehe 1196 px -- Fenster ist 900 px
davon Banner 638 px -- 53 %, ohne einen Buchstaben
Titel steht bei 1482 px -- zweimal scrollen bis zum Namen
Titelgroesse 15,36 px -- 1,4 px mehr als der Fliesstext
Etikett 10,88 px -- unter der eigenen Grenze (11,5)
Datum 3 x -- in drei Zeilen untereinander
Knoepfe 4 x gleich -- "loeschen" wie "zur Aufgabe"
WAS SICH DARAUS ERGIBT: Das Bild bleibt gross, traegt aber den Titel.
Der Zeitraum steht einmal, mit der Antwort davor ("noch 13 Tage").
Aufgaben werden zu einer Liste, die man abhaken kann. Und "loeschen"
sieht nicht mehr aus wie "zur Aufgabe".
AUGENSCHONEND BLEIBT ES: keine neuen Signalfarben, kein Leuchten,
kein Blinken. Die Toene kommen aus der vorhandenen Palette
(--k-gut / --k-mittel / --k-neutral / --akzent), die Bewegung ist
dieselbe wie ueberall (--tempo) und faellt bei
prefers-reduced-motion ganz weg.
===================================================================== */
/* Eine Eventkarte traegt ihre eigene Polsterung: Die Buehne geht bis an
den Rand, der Rest nicht. Deshalb hier null -- die Abstaende setzen
die Teile selbst. */
.eintrag-karte[data-eventkarte="ja"] {
padding: 0;
overflow: hidden;
/* Der farbige Streifen links kam von der Dringlichkeit und sagte bei
einem Event nichts: Jedes Event stand auf "mittel", also hatte
jedes denselben blauen Strich. Den Zustand traegt jetzt die Buehne,
und zwar in der Farbe, die ihn bedeutet. */
border-left-width: 1px;
}
/* ---------------------------------------------------------------------
DIE BUEHNE
--------------------------------------------------------------------- */
.ev-buehne {
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-end;
/* DIE HOEHE IST GEDECKELT, UND ZWAR ZWEIFACH.
aspect-ratio haelt die Form bei schmalen Karten (sonst waere die
Buehne auf dem Handy ein Briefschlitz), max-height faengt die
breiten ab: Bei 1230 px Kartenbreite waeren 2.6/1 immer noch
473 px -- mehr als ein Drittel eines Laptopbildschirms fuer ein
Bild, auf dem der Titel steht.
Gemessen: 638 px vorher, 300 px jetzt. */
/* `width: 100%` IST PFLICHT, NICHT ZIERDE (gemessen 03.10.2026).
Ohne sie schrumpft `aspect-ratio` zusammen mit `max-height` auch
die BREITE: In der ersten Fassung war die Buehne 780 px breit in
einer 1230 px breiten Karte -- das Banner stand als Kachel links,
rechts daneben lag ein Drittel Leere. Das sah aus wie ein
Ladefehler und war eine Rechenregel: 300 px Hoehe x 2.6 = 780 px.
Mit fester Breite berechnet aspect-ratio nur noch die Hoehe
(1230 / 2.6 = 473 px), und max-height deckelt sie auf 300. */
width: 100%;
aspect-ratio: 2.6 / 1;
min-height: 170px;
max-height: 300px;
/* DIE SEITLICHE POLSTERUNG HAT EINEN NAMEN, weil der Schriftgrund
darunter sie braucht: Er muss genau bis an den Rand der Buehne
reichen und keinen Schritt weiter. Mit einer zweiten festen Zahl
ging das schief -- auf dem Handy betraegt die Polsterung 14 px,
der Grund rechnete aber weiter mit 20, und die Karte war 6 px
breiter als ihr Fenster (gemessen 03.10.2026, scrollWidth gegen
clientWidth).
Gefunden hat das die Pruefung, nicht das Auge: `overflow: hidden`
an der Karte hat den Ueberhang unsichtbar gemacht. Und mein erster
Suchlauf nach dem Verursacher kam leer zurueck -- ein ::before ist
kein Element, `querySelectorAll("*")` findet es nicht. */
--ev-rand: 20px;
padding: 44px var(--ev-rand) 16px;
background:
radial-gradient(120% 140% at 18% 0%,
color-mix(in srgb, var(--violett, #8a76ff) 26%, transparent), transparent 62%),
linear-gradient(160deg,
color-mix(in srgb, var(--akzent, #3fbdf5) 16%, transparent),
rgba(255, 255, 255, .02));
}
/* OHNE BILD WIRD DIE BUEHNE FLACHER. Ein Farbverlauf braucht keine
300 px, um zu wirken -- und eine halbleere Flaeche ueber drei Zeilen
Text sieht aus, als fehle etwas. */
.ev-buehne:not([data-mitbild="ja"]) {
aspect-ratio: auto;
min-height: 0;
padding-top: 18px;
}
.ev-buehne__bild {
position: absolute; inset: 0;
width: 100%; height: 100%;
object-fit: cover;
/* NICHT MITTIG, SONDERN ETWAS HOEHER: Auf einem Event-Banner steht
die Aussage meistens im oberen Drittel ("Schaffst du es, ab HEUTE
bis zum 16.10.2026"), waehrend unten die Preise stehen. Wird
beschnitten, soll die Aussage uebrig bleiben. */
object-position: center 38%;
}
/* DER SCHLEIER MACHT DIE SCHRIFT LESBAR, UND ZWAR IMMER.
Welches Bild jemand spaeter hochlaedt, weiss heute niemand -- ein
helles Banner ergaebe ohne ihn helle Schrift auf Hell. Deshalb kein
"nur bei hellen Bildern": Der Verlauf liegt grundsaetzlich darunter.
Nach oben laeuft er aus, damit das Bild oben unberuehrt bleibt; das
ist die Haelfte, die man sehen will. */
.ev-buehne__schleier {
position: absolute; inset: 0;
/* DIE KURVE IST NACHGEMESSEN, NICHT GESCHAETZT (03.10.2026).
Die erste Fassung lief schon bei 26 % der Hoehe auf .82 Deckung --
damit war von einem Banner praktisch nichts mehr uebrig, und genau
das Bild, das Filipe hochlaedt, war weg. Jetzt ist der Schleier
dort stark, wo die Schrift steht (unterste 30 %), und laeuft
darueber schnell aus: Ab der Haelfte sieht man das Banner fast
unberuehrt.
DIE UNTERSTE STUFE BLEIBT KRAEFTIG. Sie ist es, die den Titel auch
auf einem weissen Banner lesbar haelt -- und was jemand spaeter
hochlaedt, weiss heute niemand. Nachgemessen wird das Ergebnis am
Bildpunkt (siehe pruef-eventkarte.mjs), nicht an dieser Zahl. */
background: linear-gradient(to top,
rgba(5, 8, 14, .92) 0%,
rgba(5, 8, 14, .78) 18%,
rgba(5, 8, 14, .46) 34%,
rgba(5, 8, 14, .16) 52%,
rgba(5, 8, 14, .02) 74%,
transparent 100%);
pointer-events: none;
}
/* Ohne Bild gibt es nichts abzudunkeln -- der Verlauf wuerde dann nur
den eigenen Farbverlauf zumatschen. */
.ev-buehne:not([data-mitbild="ja"]) .ev-buehne__schleier { display: none; }
/* ==== DIE SCHRIFT BRAUCHT IHREN EIGENEN GRUND (03.10.2026) ==========
NACHGEMESSEN, UND ZWAR AM HAERTEFALL: Mit einem fast weissen Banner
(250,248,252) stand "November Challenge" in heller Schrift auf
hellgrauem Grund -- unlesbar. Der Schleier allein reicht dafuer
nicht: Er ist ein Verlauf ueber die ganze Buehne und muesste, um
auch Weiss zu decken, so kraeftig sein, dass von einem dunklen
Banner nichts uebrig bliebe. Ein Wert, der fuer beide Faelle passt,
existiert nicht.
ALSO ZWEI EBENEN STATT EINER:
der Schleier -- sanft, fuer das Gesamtbild
dieser Grund -- kraeftig, aber nur hinter der Schrift
Er reicht 34 px ueber den Text hinaus und laeuft nach oben aus,
damit keine sichtbare Kante entsteht. Die Deckung unten (.93) ist
nicht geschaetzt: Sie ist der Wert, bei dem der Titel auch auf
reinem Weiss ueber 4,5:1 kommt -- gemessen am Bildpunkt in
pruef-eventkarte.mjs, nicht gerechnet.
WARUM NICHT EINFACH EIN KASTEN: Eine sichtbare Flaeche unter dem
Titel saehe aus wie ein Balken, der ueber dem Bild klebt. Der
Verlauf gehoert zur Buehne und faellt nicht auf -- bis man ihn
braucht. */
/* ---- "Banner ganz" ---------------------------------------------------
Oben rechts, klein, und nur dort, wo es ein Bild gibt. Er liegt UEBER
dem Schleier (der Schleier hat `pointer-events: none`, trotzdem
braucht der Knopf seine eigene Ebene) und bringt seinen eigenen
dunklen Grund mit -- auf einem hellen Banner waere er sonst
unsichtbar, genau wie es dem Titel erging. */
.ev-buehne__ganz {
position: absolute; top: 12px; right: 12px;
display: inline-flex; align-items: center; gap: 7px;
/* 34 px hoch mit 10 px Polsterung: Auf dem Handy wird daraus unten
ein 44-px-Ziel. */
padding: 7px 11px;
border: 1px solid rgba(255, 255, 255, .16);
border-radius: 999px;
background: rgba(8, 11, 18, .72);
backdrop-filter: blur(7px);
color: #dce7f3;
font-size: .74rem; font-weight: 600;
text-decoration: none;
transition: background var(--tempo, .22s), border-color var(--tempo, .22s);
}
.ev-buehne__ganz:hover {
background: rgba(8, 11, 18, .9);
border-color: rgba(255, 255, 255, .3);
}
.ev-buehne__ganz:focus-visible {
outline: 2px solid var(--akzent, #3fbdf5);
outline-offset: 2px;
}
/* Die Lupe ist gezeichnet, kein Schriftzeichen: Ein Symbol aus der
Schrift faellt auf manchen Systemen aus oder kommt als Kaestchen. */
.ev-buehne__ganz-zeichen {
flex: none; position: relative;
width: 13px; height: 13px;
border: 1.5px solid currentColor;
border-radius: 3px;
}
.ev-buehne__ganz-zeichen::before,
.ev-buehne__ganz-zeichen::after {
content: ""; position: absolute;
background: currentColor;
}
.ev-buehne__ganz-zeichen::before { left: 2.5px; top: 5px; width: 6px; height: 1.4px; }
.ev-buehne__ganz-zeichen::after { left: 4.8px; top: 2.7px; width: 1.4px; height: 6px; }
@media (prefers-reduced-motion: reduce) {
.ev-buehne__ganz { transition: none; }
}
@media (max-width: 560px) {
.ev-buehne__ganz { top: 9px; right: 9px; padding: 9px 12px; }
/* Auf schmalen Schirmen nur die Lupe -- "Banner ganz" neben einem
Titel, der ohnehin umbricht, kostet eine Zeile. Die Beschriftung
bleibt fuer Vorleseprogramme erhalten (aria-label), sie wird nur
nicht gezeigt. */
.ev-buehne__ganz-wort {
position: absolute; width: 1px; height: 1px;
overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
}
.ev-buehne__inhalt { position: relative; }
.ev-buehne[data-mitbild="ja"] .ev-buehne__inhalt::before {
content: "";
position: absolute;
left: calc(-1 * var(--ev-rand)); right: calc(-1 * var(--ev-rand));
bottom: -16px; top: -34px;
background: linear-gradient(to top,
rgba(4, 7, 12, .93) 0%,
rgba(4, 7, 12, .90) 52%,
rgba(4, 7, 12, .62) 82%,
rgba(4, 7, 12, 0) 100%);
pointer-events: none;
}
/* Die Kinder muessen UEBER diesen Grund -- ein absolut gesetztes
::before liegt sonst ueber allem, was selbst nicht positioniert ist,
und der Titel waere verschwunden. Genau so ist es beim ersten
Versuch passiert. */
.ev-buehne__kopf, .ev-zeit, .ev-zeitbalken { position: relative; }
/* ---- Titelzeile auf der Buehne -------------------------------------- */
.ev-buehne__kopf { align-items: center; gap: 8px 10px; }
/* DER TITEL IST DER TITEL (gemessen: vorher 15,36 px, also 1,4 px mehr
als der Text darunter -- das liest niemand als Ueberschrift).
clamp statt fester Zahl: Eine feste Groesse ist eine Rechnung von
gestern, sobald die Karte breiter oder schmaler wird. Hier skaliert
sie mit der Breite und bleibt zwischen 1,15 rem (18,4 px) und
1,55 rem (24,8 px). */
.eintrag-karte[data-eventkarte="ja"] .eintrag-titel {
flex: 1 1 100%;
font-size: clamp(1.15rem, 2.4vw, 1.55rem);
font-weight: 700;
line-height: 1.22;
letter-spacing: -.01em;
color: #f2f6fb;
/* Der Schatten ist keine Zierde: Er haelt den Titel auch dann
lesbar, wenn genau an dieser Stelle ein heller Fleck im Banner
liegt -- und das sieht man einem Bild vorher nicht an. */
text-shadow: 0 2px 10px rgba(0, 0, 0, .75);
}
/* DAS ETIKETT: 10,88 px waren unter der projekteigenen Grenze von
11,5 px -- angehoben wurde es bisher nur auf dem Handy. Hier gilt
dieselbe Groesse auf jedem Geraet; eine Grenze, die nur bei schmalen
Fenstern gilt, ist keine Grenze. */
.eintrag-karte[data-eventkarte="ja"] .marke-art {
font-size: .75rem;
padding: 2px 10px;
background: rgba(10, 14, 22, .62);
border: 1px solid rgba(255, 255, 255, .14);
backdrop-filter: blur(6px);
text-shadow: none;
}
/* ---- Die Zeitzeile -------------------------------------------------- */
.ev-zeit {
display: flex; align-items: center; flex-wrap: wrap;
gap: 6px 10px;
margin: 10px 0 0;
font-size: .84rem;
}
/* "noch 13 Tage" -- die Antwort, nicht der Beleg. Deshalb die kraeftige
Marke, waehrend das Datum daneben leise bleibt. */
.ev-noch {
--f: var(--k-neutral, #8e9cb0);
display: inline-flex; align-items: center; gap: 7px;
padding: 4px 12px;
border: 1px solid color-mix(in srgb, var(--f) 40%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--f) 16%, rgba(8, 11, 18, .72));
color: color-mix(in srgb, var(--f) 72%, #ffffff);
font-weight: 650;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.ev-noch::before {
content: ""; width: 7px; height: 7px; border-radius: 50%;
background: var(--f); flex: none;
}
.ev-noch[data-stand="laeuft"] { --f: var(--k-gut, #5fc99a); }
.ev-noch[data-stand="kommt"] { --f: var(--k-mittel, #e8b877); }
.ev-noch[data-stand="vorbei"] { --f: var(--k-neutral, #8e9cb0); }
/* DIE LETZTEN TAGE SIND NICHT ROT.
Signalrot waere hier falsch: Ein auslaufendes Event ist kein Fehler,
sondern der Normalfall -- jedes endet. Es wird nur waermer und
kraeftiger, damit man es im Vorbeischauen sieht, ohne erschreckt zu
werden. */
.ev-noch[data-dringend="ja"] {
--f: var(--k-mittel, #e8b877);
font-weight: 700;
}
.ev-spanne {
color: #c3d2e2;
font-variant-numeric: tabular-nums;
text-shadow: 0 1px 6px rgba(0, 0, 0, .7);
}
/* ---- Der Zeitbalken -------------------------------------------------- */
.ev-zeitbalken {
position: relative;
height: 4px;
margin-top: 12px;
border-radius: 999px;
background: rgba(255, 255, 255, .14);
overflow: hidden;
}
.ev-zeitbalken__fuellung {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg,
color-mix(in srgb, var(--akzent, #3fbdf5) 70%, transparent),
color-mix(in srgb, var(--violett, #8a76ff) 80%, transparent));
transition: width var(--tempo, .22s) ease;
}
.ev-buehne[data-stand="vorbei"] .ev-zeitbalken__fuellung {
background: rgba(255, 255, 255, .26);
}
@media (prefers-reduced-motion: reduce) {
.ev-zeitbalken__fuellung { transition: none; }
}
/* ---------------------------------------------------------------------
DER KOERPER: Text, Aufgaben, Regeln
--------------------------------------------------------------------- */
.eintrag-karte[data-eventkarte="ja"] .eintrag-text {
margin: 14px 18px 0;
color: var(--text-leise);
/* EINE ZEILE HAT EINE LESBARE LAENGE. Auf 1280 px lief der
Erklaertext ueber 1120 px -- rund 150 Zeichen je Zeile. Das Auge
findet den Anfang der naechsten Zeile dann nicht mehr zuverlaessig;
die gaengige Empfehlung liegt bei 45 bis 75 Zeichen. 72ch ist das
obere Ende davon und passt zu dieser Schriftgroesse. */
max-width: 72ch;
}
/* ZWEI SPALTEN, WENN PLATZ IST -- GEMESSEN, NICHT GERECHNET.
`auto-fit` mit einer Mindestbreite bricht genau dann um, wenn eine
Spalte unter 300 px fiele. Das ist der Unterschied zu einer
Fensterbreite in einer Zahl: Kommt irgendwann ein dritter Block
dazu oder wird die Karte in einer schmaleren Spalte gezeigt, stimmt
es weiterhin, ohne dass jemand die Zahl nachzieht.
(Die Lehre dazu steht im Projektgedaechtnis unter dem 06.09.2026:
eine feste Schwelle, die beim Hinzufuegen des naechsten Elements
nicht nachgezogen wurde, hat zwoelf Seiten lang den falschen Knopf
getroffen.) */
.ev-bloecke {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 14px;
align-items: start;
margin: 14px 18px 0;
}
.ev-bloecke .ev-block { margin: 0; }
.ev-block {
margin: 14px 18px 0;
padding: 13px 15px;
border: 1px solid var(--rand);
border-radius: var(--radius-klein);
background: rgba(255, 255, 255, .022);
}
/* AUFGABEN UND REGELN SEHEN NICHT MEHR GLEICH AUS.
Vorher waren es zwei identische graue Kaesten untereinander -- "was
ich tun soll" und "was dabei gilt" wirkten damit gleich wichtig.
Die Aufgaben sind das, weswegen jemand hier ist; sie bekommen einen
Rand in der Hausfarbe. Die Regeln treten zurueck, ohne zu
verschwinden. */
.ev-block--aufgaben {
border-color: color-mix(in srgb, var(--akzent, #3fbdf5) 24%, var(--rand));
background:
linear-gradient(180deg,
color-mix(in srgb, var(--akzent, #3fbdf5) 7%, transparent),
transparent 58%),
rgba(255, 255, 255, .022);
}
.ev-block--regeln { background: rgba(255, 255, 255, .012); }
/* Alles geschafft: ein ruhiger gruener Rand, kein Konfetti. */
.ev-block--aufgaben[data-fertig="ja"] {
border-color: color-mix(in srgb, var(--k-gut, #5fc99a) 34%, var(--rand));
}
.ev-block__kopf {
display: flex; align-items: baseline;
gap: 10px; margin-bottom: 9px;
}
/* DIE ZAHL GEHOERT ZUR UEBERSCHRIFT, NICHT AN DEN RAND.
Mit `justify-content: space-between` stand "0 von 3" auf einer
1160 px breiten Karte rund einen Meter von dem Wort entfernt, auf
das es sich bezieht -- man liest es dann als zwei verschiedene
Angaben. Jetzt steht es direkt daneben, abgesetzt durch einen
Trennpunkt statt durch Leere. */
.ev-zaehler::before {
content: "·";
margin-right: 8px;
color: var(--text-still);
opacity: .7;
}
.ev-block__schild {
margin: 0;
/* .75rem = 12 px. Nicht kleiner: In Grossbuchstaben mit Sperrung
wirkt eine Schrift kleiner, als die Zahl sagt -- die Begruendung
steht so schon beim alten .event-block__schild. */
font-size: .75rem; font-weight: 700;
letter-spacing: .09em; text-transform: uppercase;
color: var(--text-leise);
}
/* "1 von 3" -- die Zahl gehoert neben die Ueberschrift und nicht
darunter: Sie ist die Zusammenfassung der Liste, nicht ihr erster
Eintrag. tabular-nums, damit sie beim Hochzaehlen nicht springt. */
.ev-zaehler {
font-size: .78rem; font-weight: 650;
font-variant-numeric: tabular-nums;
color: var(--text-still);
white-space: nowrap;
}
.ev-zaehler[data-voll="ja"] { color: var(--k-gut, #5fc99a); }
/* ---- Die Liste ------------------------------------------------------- */
.ev-liste {
list-style: none;
margin: 0; padding: 0;
display: flex; flex-direction: column; gap: 2px;
}
.ev-punkt { margin: 0; }
/* EIN VORBEIGELAUFENES EVENT HAT KEINE KNOEPFE -- und ohne den Knopf
fehlte der Zeile seine Polsterung: Beim ersten Lauf klebte
"20 Live-Stunden" am linken Rand des Kastens, waehrend dieselbe
Liste eine Karte darueber ordentlich eingerueckt stand. Zwei Listen
nebeneinander, die verschieden aussehen, liest man als Fehler.
Deshalb bekommt die reine Anzeigezeile dasselbe Raster wie der
Knopf -- nur eben ohne Hintergrund und ohne Zeiger. */
.ev-punkt:not(:has(.ev-punkt__knopf)):not(.ev-punkt--regel) {
display: flex; align-items: flex-start; gap: 10px;
padding: 9px 10px;
font-size: .9rem; line-height: 1.45;
color: var(--text);
}
/* DER GANZE PUNKT IST DER KNOPF, nicht nur ein Kaestchen daneben.
Ein 16-px-Kaestchen ist auf dem Handy ein Ziel von 16 x 16 px -- die
gaengige Mindestgroesse liegt bei 44 x 44. Wer danebentippt, denkt,
es sei kaputt. Die ganze Zeile zu nehmen loest beides: Das Ziel ist
so breit wie die Karte, und man muss nicht zielen. */
.ev-punkt__knopf {
display: flex; align-items: flex-start; gap: 10px;
width: 100%;
padding: 9px 10px;
margin: 0;
border: 1px solid transparent;
border-radius: 9px;
background: transparent;
color: var(--text);
font: inherit;
font-size: .9rem;
line-height: 1.45;
text-align: left;
cursor: pointer;
transition: background var(--tempo, .22s), border-color var(--tempo, .22s);
}
.ev-punkt__knopf:hover {
background: rgba(255, 255, 255, .04);
border-color: var(--rand);
}
.ev-punkt__knopf:focus-visible {
outline: 2px solid var(--akzent, #3fbdf5);
outline-offset: 1px;
}
.ev-punkt__knopf[disabled] { opacity: .6; cursor: progress; }
@media (prefers-reduced-motion: reduce) {
.ev-punkt__knopf { transition: none; }
}
/* Das Kaestchen. Leer ein Rahmen, voll ein Haken -- und der Haken ist
gezeichnet (clip-path), kein Schriftzeichen: Ein Haken aus der
Schrift sieht auf jedem System anders aus und faellt auf manchen
ganz aus. */
.ev-punkt__kaestchen {
flex: none;
width: 19px; height: 19px;
margin-top: 1px;
/* 62 % waren im Bildschirmfoto kaum auszumachen -- ein Kaestchen,
das man suchen muss, laedt nicht zum Anfassen ein. Auf 92 %
angehoben und mit etwas mehr Fuellung dahinter. */
border: 1.5px solid color-mix(in srgb, var(--text-still) 92%, transparent);
border-radius: 6px;
background: rgba(255, 255, 255, .055);
position: relative;
transition: background var(--tempo, .22s), border-color var(--tempo, .22s);
}
.ev-punkt__kaestchen::after {
content: "";
position: absolute; left: 4px; top: 4px;
width: 9px; height: 5px;
border-left: 2px solid #07101a;
border-bottom: 2px solid #07101a;
transform: rotate(-45deg) scale(.6);
opacity: 0;
transition: opacity var(--tempo, .22s), transform var(--tempo, .22s);
}
.ev-punkt[data-abgehakt="ja"] .ev-punkt__kaestchen {
background: var(--k-gut, #5fc99a);
border-color: var(--k-gut, #5fc99a);
}
.ev-punkt[data-abgehakt="ja"] .ev-punkt__kaestchen::after {
opacity: 1; transform: rotate(-45deg) scale(1);
}
@media (prefers-reduced-motion: reduce) {
.ev-punkt__kaestchen, .ev-punkt__kaestchen::after { transition: none; }
}
/* ABGEHAKT WIRD NICHT DURCHGESTRICHEN.
Durchgestrichener Text ist schlecht lesbar, und man will ihn
weiterhin lesen koennen -- "22 Live-Stunden" bleibt die Bedingung,
auch wenn sie erfuellt ist. Der Haken sagt es bereits; die Schrift
tritt nur leicht zurueck. */
.ev-punkt[data-abgehakt="ja"] .ev-punkt__text { color: var(--text-leise); }
.ev-punkt__text { flex: 1 1 auto; overflow-wrap: anywhere; }
/* Die Regelliste: dasselbe Raster, aber ein Strich statt eines
Kaestchens -- man sieht auf einen Blick, dass hier nichts zu tun
ist. */
.ev-punkt--regel {
display: flex; align-items: flex-start; gap: 10px;
padding: 6px 10px;
font-size: .87rem; line-height: 1.5;
color: var(--text-leise);
}
.ev-punkt__zeichen {
flex: none;
width: 19px; height: 19px;
margin-top: 1px;
position: relative;
}
.ev-punkt__zeichen::before {
content: "";
position: absolute; left: 3px; top: 9px;
width: 11px; height: 1.5px;
border-radius: 2px;
background: color-mix(in srgb, var(--text-still) 52%, transparent);
}
/* Der Rueckfall, wenn der Server keine Punkteliste schickt. */
.ev-rohtext {
margin: 0;
font-size: .87rem; line-height: 1.6; color: var(--text);
white-space: pre-line;
overflow-wrap: anywhere;
}
/* ---- Wer macht mit (nur fuer die Leitung) ---------------------------- */
.ev-mitmacher {
display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
margin-top: 11px; padding-top: 10px;
border-top: 1px solid var(--rand);
}
.ev-mitmacher__schild {
font-size: .7rem; font-weight: 700;
letter-spacing: .08em; text-transform: uppercase;
color: var(--text-still);
margin-right: 2px;
}
.ev-mitmacher__person {
padding: 2px 9px;
border: 1px solid var(--rand);
border-radius: 999px;
background: rgba(255, 255, 255, .03);
font-size: .76rem;
color: var(--text-leise);
font-variant-numeric: tabular-nums;
}
.ev-mitmacher__person[data-voll="ja"] {
border-color: color-mix(in srgb, var(--k-gut, #5fc99a) 40%, transparent);
color: color-mix(in srgb, var(--k-gut, #5fc99a) 80%, #ffffff);
}
/* ---------------------------------------------------------------------
DER FUSS
--------------------------------------------------------------------- */
.eintrag-karte[data-eventkarte="ja"] .eintrag-fuss {
margin: 16px 0 0;
padding: 12px 18px 15px;
border-top: 1px solid var(--rand);
background: rgba(255, 255, 255, .012);
}
.eintrag-karte[data-eventkarte="ja"] .ampel {
margin: 0; padding: 11px 18px 14px;
border-top: 1px solid var(--rand);
}
/* LOESCHEN SIEHT NICHT MEHR AUS WIE "ZUR AUFGABE".
Gemessen waren es vier gleich grosse, gleich graue Pillen
nebeneinander -- darunter die eine, die man nicht rueckgaengig
machen kann. Sie rutscht jetzt mit einem Abstand nach rechts weg und
bekommt einen warmen Ton. KEIN Signalrot und keine Fuellung: Der
Knopf soll sich unterscheiden, nicht drohen; erst beim Ueberfahren
wird er deutlicher. */
.eintrag-karte[data-eventkarte="ja"] .schritt--gefahr {
margin-left: 10px;
color: color-mix(in srgb, var(--warn, #ff9aa2) 72%, var(--text-still));
border-color: color-mix(in srgb, var(--warn, #ff9aa2) 26%, transparent);
}
.eintrag-karte[data-eventkarte="ja"] .schritt--gefahr:hover {
color: var(--warn, #ff9aa2);
border-color: color-mix(in srgb, var(--warn, #ff9aa2) 52%, transparent);
background: color-mix(in srgb, var(--warn, #ff9aa2) 10%, transparent);
}
/* ---------------------------------------------------------------------
EIN VORBEIGELAUFENES EVENT
--------------------------------------------------------------------- */
/* Die alte Regel dimmte die ganze Karte auf .82 -- damit war ein
abgelaufenes Event kaum von einem laufenden zu unterscheiden, und der
Text wurde schlechter lesbar, ohne dass es etwas brachte.
Jetzt tritt nur das BILD zurueck (entsaettigt und abgedunkelt). Der
Text behaelt seinen vollen Kontrast: "alt" ist kein Grund, jemanden
vom Lesen auszuschliessen. */
.eintrag-karte[data-eventkarte="ja"][data-event="vorbei"] { opacity: 1; }
.ev-buehne[data-stand="vorbei"] .ev-buehne__bild {
filter: saturate(.35) brightness(.62);
}
.ev-buehne[data-stand="vorbei"] { aspect-ratio: 3.6 / 1; max-height: 190px; }
/* ---------------------------------------------------------------------
AUF DEM HANDY
--------------------------------------------------------------------- */
@media (max-width: 560px) {
.ev-buehne {
aspect-ratio: 1.9 / 1;
min-height: 150px; max-height: 230px;
--ev-rand: 14px;
padding: 38px var(--ev-rand) 13px;
}
.ev-buehne[data-stand="vorbei"] { aspect-ratio: 2.6 / 1; max-height: 160px; }
.eintrag-karte[data-eventkarte="ja"] .eintrag-text { margin: 12px 14px 0; }
.ev-bloecke { margin: 12px 14px 0; gap: 12px; }
.ev-block { margin: 12px 14px 0; padding: 12px 12px; }
.ev-bloecke .ev-block { margin: 0; }
.eintrag-karte[data-eventkarte="ja"] .eintrag-fuss { padding: 12px 14px 14px; }
.eintrag-karte[data-eventkarte="ja"] .ampel { padding: 11px 14px 13px; }
/* Das Beruehrziel: 9 px Polsterung plus 19 px Kaestchen ergaeben
37 px. Auf dem Handy wird daraus 44 -- dieselbe Zahl wie bei allen
anderen Knoepfen des Hauses. */
.ev-punkt__knopf { min-height: 44px; align-items: center; }
/* Der Loeschknopf steht auf dem Handy in einer eigenen Zeile; ein
Abstand nach links wuerde ihn dort nur einruecken. */
.eintrag-karte[data-eventkarte="ja"] .schritt--gefahr { margin-left: 0; }
}
/* Hoher Kontrast: Die Zustandsfarben verschwinden dort, also muss die
Form sprechen. */
@media (forced-colors: active) {
.ev-buehne__schleier { display: none; }
.ev-punkt__kaestchen { border: 2px solid CanvasText; }
.ev-punkt[data-abgehakt="ja"] .ev-punkt__kaestchen { background: Highlight; }
.ev-zeitbalken { border: 1px solid CanvasText; }
}
/* =====================================================================
DER ZEILEN-EDITOR UND DIE VORSCHAU (03.10.2026)
Die Begruendung, warum es ihn gibt, steht bei zeilenEditor() in
bereich.js: Ein leeres Textfeld zwingt den Menschen, die
Aufzaehlungszeichen selbst zu tippen -- und dann sehen drei Zeilen
auf drei Arten aus. Hier steht nur, wie er aussieht.
===================================================================== */
.zeileneditor {
display: flex; flex-direction: column;
gap: 6px;
}
.zeileneditor__liste {
display: flex; flex-direction: column;
gap: 6px;
}
.zeileneditor__zeile {
display: flex; align-items: center; gap: 9px;
}
/* Das Aufzaehlungszeichen steht VOR dem Feld und nicht darin: So sieht
man schon beim Tippen, dass es gesetzt wird -- und man kommt gar
nicht erst auf die Idee, selbst eines zu tippen. */
.zeileneditor__zeichen {
flex: none;
width: 16px; height: 16px;
position: relative;
}
.zeileneditor__zeichen::before {
content: "";
position: absolute; left: 4px; top: 6px;
width: 6px; height: 6px; border-radius: 50%;
background: color-mix(in srgb, var(--akzent, #3fbdf5) 58%, transparent);
}
.zeileneditor__feld {
flex: 1 1 auto;
min-width: 0;
/* Dieselben Masse wie alle Felder des Hauses (--feld-ziel usw. in
start.css) -- ein Eingabefeld, das sich von den anderen
unterscheidet, sieht nach einem Fremdkoerper aus. */
min-height: 40px;
padding: 8px 11px;
border: 1px solid var(--rand);
border-radius: 9px;
background: rgba(255, 255, 255, .035);
color: var(--text);
font: inherit;
font-size: .9rem;
}
.zeileneditor__feld:focus-visible {
outline: 2px solid var(--akzent, #3fbdf5);
outline-offset: 1px;
border-color: transparent;
}
/* Der Wegnehmen-Knopf: ein Kreuz, gezeichnet statt getippt -- ein
Mal-Zeichen aus der Schrift sitzt je nach System anders. */
.zeileneditor__weg {
flex: none;
width: 34px; height: 34px;
border: 1px solid transparent;
border-radius: 8px;
background: transparent;
color: var(--text-still);
cursor: pointer;
position: relative;
transition: color var(--tempo, .22s), background var(--tempo, .22s),
border-color var(--tempo, .22s);
}
.zeileneditor__weg::before,
.zeileneditor__weg::after {
content: "";
position: absolute; left: 10px; top: 16px;
width: 12px; height: 1.6px; border-radius: 2px;
background: currentColor;
}
.zeileneditor__weg::before { transform: rotate(45deg); }
.zeileneditor__weg::after { transform: rotate(-45deg); }
.zeileneditor__weg:hover {
color: var(--warn, #ff9aa2);
background: color-mix(in srgb, var(--warn, #ff9aa2) 9%, transparent);
border-color: color-mix(in srgb, var(--warn, #ff9aa2) 24%, transparent);
}
.zeileneditor__weg:focus-visible {
outline: 2px solid var(--akzent, #3fbdf5);
outline-offset: 1px;
}
@media (prefers-reduced-motion: reduce) {
.zeileneditor__weg { transition: none; }
}
.zeileneditor__fuss {
display: flex; align-items: center; gap: 10px;
margin-top: 7px;
flex-wrap: wrap;
}
.zeileneditor__mehr {
padding: 7px 14px;
min-height: 36px;
border: 1px dashed color-mix(in srgb, var(--akzent, #3fbdf5) 34%, var(--rand));
border-radius: 9px;
background: transparent;
color: color-mix(in srgb, var(--akzent, #3fbdf5) 72%, #cfe0f0);
font: inherit; font-size: .84rem; font-weight: 600;
cursor: pointer;
transition: background var(--tempo, .22s), border-color var(--tempo, .22s);
}
.zeileneditor__mehr:hover {
background: color-mix(in srgb, var(--akzent, #3fbdf5) 10%, transparent);
border-style: solid;
}
.zeileneditor__mehr:focus-visible {
outline: 2px solid var(--akzent, #3fbdf5);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.zeileneditor__mehr { transition: none; }
}
/* Der Weg zurueck zum Textfeld steht LEISE daneben: Der Normalfall ist
die Liste, das Textfeld ist der Ausweg fuer den Sonderfall. */
.zeileneditor__wechsel {
padding: 6px 2px;
min-height: 36px;
border: 0; background: transparent;
color: var(--text-still);
font: inherit; font-size: .79rem;
text-decoration: underline;
text-underline-offset: 3px;
cursor: pointer;
}
.zeileneditor__wechsel:hover { color: var(--text-leise); }
.zeileneditor__wechsel:focus-visible {
outline: 2px solid var(--akzent, #3fbdf5);
outline-offset: 2px;
border-radius: 6px;
}
/* Der Hinweis "Läuft 14 Tage." -- und die Warnung, wenn das Ende vor
dem Anfang liegt. Die Warnung ist ein WORT und nicht nur eine Farbe:
Wer Farben schlecht unterscheidet, saehe sonst gar nichts. */
#hinweis-dauer[data-warnung="ja"] {
color: var(--warn, #ff9aa2);
font-weight: 600;
}
/* ---------------------------------------------------------------------
DIE VORSCHAU
--------------------------------------------------------------------- */
.ev-vorschau {
margin: 18px 0 4px;
padding: 14px;
border: 1px dashed var(--rand);
border-radius: var(--radius-klein);
background: rgba(255, 255, 255, .012);
}
.ev-vorschau__schild {
margin: 0 0 10px;
font-size: .72rem; font-weight: 700;
letter-spacing: .09em; text-transform: uppercase;
color: var(--text-still);
}
/* Die Vorschau zeigt nur die Buehne -- der Rest der Karte haengt an
Daten, die es vor dem Speichern noch nicht gibt (Knoepfe, Ampel,
Rueckmeldungen). Eine Vorschau, die etwas zeigt, das hinterher
fehlt, ist schlimmer als eine kurze. */
.ev-vorschau .eintrag-karte { margin-bottom: 0; }
+817 -30
View File
@@ -374,6 +374,79 @@
return { schluessel: 'laeuft', text: von === bis ? 'heute' : 'läuft bis ' + datum(bis) };
}
/* =====================================================================
WIE LANGE NOCH? (03.10.2026)
Die eine Frage, die bei einem Wettbewerb wirklich zaehlt -- und
die Karte hat sie bis heute nicht beantwortet. Dort stand dreimal
dasselbe Datum ("laeuft bis 16.10.2026", "03.10.2026 - 16.10.2026",
"Im Kalender am 03.10.2026") und nirgends die Zahl, die man
eigentlich wissen will.
IN TAGEN, NICHT IN STUNDEN. Ein Event laeuft ueber Wochen; "noch
312 Stunden" muss man erst umrechnen. Am LETZTEN Tag wechselt der
Ton: Dann ist "heute ist der letzte Tag" die Auskunft, und sie
gehoert hervorgehoben.
GERECHNET WIRD AUF TAGEN, nicht auf Zeitpunkten. Ein Event endet
am 16.10. -- also mit dem Ablauf dieses Tages, nicht um 00:00 Uhr.
Wer mit Date.now() rechnet, meldet am Morgen des letzten Tages
"vorbei".
*/
const TAG_MS = 86400000;
const alsTag = (iso) => {
const t = String(iso || '').slice(0, 10);
return /^\d{4}-\d{2}-\d{2}$/.test(t) ? new Date(t + 'T00:00:00').getTime() : null;
};
const tageZwischen = (a, b) => Math.round((b - a) / TAG_MS);
/** Der Satz unter dem Titel: "noch 13 Tage", "letzter Tag",
* "startet morgen", "vorbei seit 12 Tagen". */
function eventNoch(e) {
const heuteIso = window.heuteLokal ? window.heuteLokal()
: new Date().toISOString().slice(0, 10);
const heute = alsTag(heuteIso);
const von = alsTag(e.datum);
const bis = alsTag(e.event_ende || e.datum);
if (heute === null || von === null || bis === null) return null;
if (heute < von) {
const n = tageZwischen(heute, von);
return { wort: n === 1 ? 'startet morgen' : `startet in ${n} Tagen`, dringend: n <= 1 };
}
if (heute > bis) {
const n = tageZwischen(bis, heute);
return { wort: n === 1 ? 'seit gestern vorbei' : `vorbei seit ${n} Tagen`, dringend: false };
}
const n = tageZwischen(heute, bis);
if (n === 0) return { wort: 'heute ist der letzte Tag', dringend: true };
if (n === 1) return { wort: 'noch 1 Tag', dringend: true };
/* "Noch 2 Tage" ist knapp, "noch 3" ist es nicht mehr -- die Grenze
ist gesetzt und nicht gemessen, aber sie steht an EINER Stelle. */
return { wort: `noch ${n} Tage`, dringend: n <= 2 };
}
/** Wie viel des Zeitraums ist herum? 0 bis 1, oder null.
*
* FUER EINEN BALKEN, nicht fuer eine Zahl. "Wie viel Zeit ist noch"
* sieht man an einer Laenge sofort und muss sie nicht lesen -- genau
* wie beim Rangbalken der Wunschliste darueber.
*
* EIN EINTAEGIGES EVENT hat keinen Verlauf (von === bis, Division
* durch null). Dann gibt es keinen Balken: Ein Balken, der bei einem
* Tagesevent immer voll oder immer leer stuende, sagt nichts. */
function eventFortschritt(e) {
const heuteIso = window.heuteLokal ? window.heuteLokal()
: new Date().toISOString().slice(0, 10);
const heute = alsTag(heuteIso);
const von = alsTag(e.datum);
const bis = alsTag(e.event_ende || e.datum);
if (heute === null || von === null || bis === null || bis <= von) return null;
const anteil = (heute - von) / (bis - von);
return Math.max(0, Math.min(1, anteil));
}
/* Der Abruf steht seit dem 22.09.2026 in assets/js/holen.js.
Er stand ACHTMAL da, Zeichen fuer Zeichen gleich -- und als
die Regel darin sich als falsch herausstellte (jeder 404
@@ -428,8 +501,14 @@
feld.click();
}
function tuKnopf(text, tu) {
const b = el('button', 'schritt', text);
/* `klasse` kam am 03.10.2026 dazu, fuer den Loeschknopf an der
Eventkarte. Vorher sahen dort vier Knoepfe gleich aus -- darunter
der eine, den man nicht zuruecknehmen kann. Die Unterscheidung
gehoert an den Knopf und nicht in eine Regel, die ihn ueber seine
Stellung in der Reihe sucht: Kommt ein fuenfter dazu, trifft so
eine Regel den Falschen. */
function tuKnopf(text, tu, klasse) {
const b = el('button', 'schritt' + (klasse ? ' ' + klasse : ''), text);
b.type = 'button';
b.addEventListener('click', async () => {
b.disabled = true; melde('');
@@ -509,10 +588,319 @@
if (ton) document.body.dataset.ton = String(ton);
}
/* =====================================================================
DIE BUEHNE EINES EVENTS (03.10.2026)
Filipe: "ich will das viel geiler viel perfekter."
GEMESSEN, WARUM: Auf 1280 px war eine Eventkarte 1196 px hoch --
hoeher als das Fenster. 638 px davon, also 53 %, waren das Banner,
und zwar leeres Banner: Der Titel stand DARUNTER, bei 1482 px auf
der Seite. Man musste zweimal scrollen, um zu erfahren, wie das
Event ueberhaupt heisst. Dazu war der Titel 15,36 px gross -- 1,4 px
mehr als der Fliesstext, der darunter stand.
ALSO STEHT DER TITEL JETZT AUF DEM BILD. Das Banner ist kein Anhang
mehr, sondern der Hintergrund des Kopfes: Es behaelt seine Wirkung,
verliert aber die Hoehe, die es ohne Inhalt verbraucht hat. Die
Hoehe ist gedeckelt (CSS), damit ein hochkantes Bild die Karte
nicht erneut aufreisst.
OHNE BANNER GIBT ES TROTZDEM EINE BUEHNE -- dann aus Farbe. Eine
Karte, die je nach Bild zwei voellig verschiedene Formen hat, liest
man als Unordnung; und ein Event ohne Bild ist kein Event zweiter
Klasse.
DIE LESBARKEIT KOMMT NICHT VOM ZUFALL: Ueber dem Bild liegt ein
Verlauf von unten (siehe .ev-buehne__schleier im CSS). Ein heller
Banner wuerde sonst weisse Schrift auf Weiss ergeben -- und genau
das kann man einem Bild, das jemand spaeter hochlaedt, nicht
ansehen. Der Schleier gilt deshalb immer, nicht nur bei dunklen
Bildern. */
function eventBuehne(e, stand, kopf) {
const b = el('header', 'ev-buehne');
b.dataset.stand = stand.schluessel;
const bild = (e.bilder || [])[0];
if (bild) {
b.dataset.mitbild = 'ja';
const img = el('img', 'ev-buehne__bild');
img.src = '/workspace/api/dateien/' + bild.id + '/bild';
/* LEERES alt IST HIER RICHTIG: Das Banner ist Schmuck hinter dem
Titel, der direkt darauf steht. Ein zweites Mal "Bild zu
Oktober Event" vorgelesen zu bekommen, nachdem der Titel schon
kam, ist Laerm, keine Auskunft. */
img.alt = '';
img.loading = 'lazy';
img.decoding = 'async';
/* Faellt das Bild aus, bleibt die Buehne stehen -- nur eben als
Farbe. Ein kaputtes Bildsymbol mitten im Kopf saehe aus, als
sei die Seite defekt. */
img.addEventListener('error', () => { img.remove(); delete b.dataset.mitbild; });
b.append(img);
/* ==== DAS BANNER IN VOLLER GROESSE (03.10.2026) ==============
EINE VERSCHLECHTERUNG, DIE BEIM NACHSEHEN AUFFIEL: Vorher war
das Bild ein Verweis -- ein Klick zeigte es ganz. Als es zur
Buehne wurde, fiel dieser Weg weg, und das ist bei genau
diesen Bildern der falsche Verlust: Auf Filipes Oktober-Banner
steht die eigentliche Ansage IM Bild ("Schaffst du es, ab
HEUTE bis zum 16.10.2026") samt der drei Bedingungen. Die
Buehne zeigt davon einen Ausschnitt -- sie muss, sonst waere
sie wieder 638 px hoch.
DESHALB EIN EIGENER KNOPF und nicht die ganze Flaeche: Auf der
Buehne stehen Titel und Zeitangabe, und Text, den man nicht
markieren kann, ohne dass sich ein Fenster oeffnet, ist
aergerlich. Ein kleiner Knopf oben rechts ist eindeutig --
und er sagt mit Worten, was er tut, nicht nur mit einem
Symbol. */
const auf = el('a', 'ev-buehne__ganz');
auf.href = '/workspace/api/dateien/' + bild.id + '/bild';
auf.target = '_blank';
auf.rel = 'noopener';
auf.append(el('span', 'ev-buehne__ganz-zeichen'));
auf.append(el('span', 'ev-buehne__ganz-wort', 'Banner ganz'));
auf.title = 'Zeigt das Banner in voller Größe in einem neuen Fenster';
b.append(auf);
}
b.append(el('div', 'ev-buehne__schleier'));
const inhalt = el('div', 'ev-buehne__inhalt');
kopf.classList.add('ev-buehne__kopf');
inhalt.append(kopf);
/* DIE ZEITZEILE -- einmal, nicht dreimal.
"noch 13 Tage" steht VOR dem Datum: Es ist die Antwort, das
Datum ist der Beleg dazu. Und ein Zeitraum gehoert als Spanne
gelesen, nicht als zwei Einzeldaten -- deshalb ein Gedankenstrich
und kein zweites Feld. */
const zeit = el('p', 'ev-zeit');
const noch = eventNoch(e);
if (noch) {
const m = el('span', 'ev-noch', noch.wort);
m.dataset.stand = stand.schluessel;
if (noch.dringend) m.dataset.dringend = 'ja';
zeit.append(m);
}
const von = datum(e.datum);
const bis = e.event_ende && e.event_ende !== e.datum ? datum(e.event_ende) : null;
zeit.append(el('span', 'ev-spanne', bis ? von + ' – ' + bis : von));
inhalt.append(zeit);
/* DER ZEITBALKEN. "Wie viel ist herum" sieht man an einer Laenge
sofort; als Zahl muesste man sie lesen und umrechnen. Fuer
Vorleseprogramme steht derselbe Inhalt als Satz da -- ein Balken
ohne Beschriftung ist fuer sie gar nicht vorhanden. */
const anteil = eventFortschritt(e);
if (anteil !== null && stand.schluessel !== 'kommt') {
const bal = el('div', 'ev-zeitbalken');
const f = el('div', 'ev-zeitbalken__fuellung');
f.style.width = Math.round(anteil * 100) + '%';
bal.append(f);
bal.setAttribute('role', 'img');
bal.setAttribute('aria-label', noch
? 'Zeitraum: ' + noch.wort
: Math.round(anteil * 100) + ' Prozent des Zeitraums vorbei');
inhalt.append(bal);
}
b.append(inhalt);
return b;
}
/* =====================================================================
DIE AUFGABEN ZUM ABHAKEN (03.10.2026)
Bisher stand hier ein Absatz mit white-space: pre-line -- also
genau das, was DogFather getippt hatte, Zeichen fuer Zeichen. Im
echten Oktober-Event sah das so aus:
.Jeden Tag Live gehen
.22-Live-Stunden erreichen
. 33k Diamanten erreichen
Einmal mit Leerzeichen, zweimal ohne. Das ist keine Schlamperei,
sondern die unvermeidliche Folge davon, dass die Aufzaehlungs-
zeichen von Hand getippt werden. Jetzt setzt die Liste sie selbst
(siehe punkteAusText im Server), und die Einrueckung stimmt
zwangslaeufig.
JEDER HAKT FUER SICH AB. Der Haken ist eine Selbstauskunft: "diese
Bedingung habe ich erfuellt". Niemand kann ihn bei jemand anderem
setzen, auch die Leitung nicht -- sonst waere es eine Bewertung.
DIE ZAHL KOMMT VOM SERVER, auch nach dem Klicken. Im Browser
weiterzuzaehlen ("war 1, jetzt 2") geht schief, sobald zwei
Fenster offen sind oder eine Aufgabe inzwischen umgeschrieben
wurde. Angezeigt wird sie trotzdem sofort -- bei einem Haken ist
eine halbe Sekunde Wartezeit der Unterschied zwischen "hat
geklappt" und "hab ich das jetzt gedrueckt?". Antwortet der Server
anders, gilt seine Zahl. Dieselbe Ueberlegung steht schon beim
Knopf "Will ich auch".
EIN VORBEIGELAUFENES EVENT WIRD NICHT MEHR ABGEHAKT. Der Haken
waere eine Behauptung ueber etwas, das nicht mehr stattfindet --
und die Liste bleibt trotzdem lesbar, damit man nachschlagen kann,
worum es ging. */
function eventAufgaben(e, stand) {
const punkte = Array.isArray(e.event_punkte) ? e.event_punkte : [];
if (!punkte.length) return null;
const block = el('section', 'ev-block ev-block--aufgaben');
const kopf = el('div', 'ev-block__kopf');
kopf.append(el('h4', 'ev-block__schild', 'Aufgaben'));
const offen = stand.schluessel !== 'vorbei';
let geschafft = punkte.filter((p) => p.meiner).length;
const zaehler = el('span', 'ev-zaehler');
const zahlMalen = () => {
zaehler.textContent = geschafft + ' von ' + punkte.length;
const voll = punkte.length > 0 && geschafft === punkte.length;
zaehler.dataset.voll = voll ? 'ja' : '';
block.dataset.fertig = voll ? 'ja' : '';
};
zahlMalen();
kopf.append(zaehler);
block.append(kopf);
const liste = el('ul', 'ev-liste');
for (const p of punkte) {
const zeile = el('li', 'ev-punkt');
if (!offen) {
/* Ohne Knopf, aber mit demselben Zeichen davor: Die Liste soll
gleich aussehen, nur eben nicht mehr bedienbar. */
zeile.dataset.abgehakt = p.meiner ? 'ja' : '';
zeile.append(el('span', 'ev-punkt__kaestchen'));
zeile.append(el('span', 'ev-punkt__text', p.text));
liste.append(zeile);
continue;
}
const knopf = el('button', 'ev-punkt__knopf');
knopf.type = 'button';
let meiner = !!p.meiner;
const malen = () => {
knopf.setAttribute('aria-pressed', String(meiner));
zeile.dataset.abgehakt = meiner ? 'ja' : '';
};
knopf.append(el('span', 'ev-punkt__kaestchen'));
knopf.append(el('span', 'ev-punkt__text', p.text));
malen();
knopf.addEventListener('click', async () => {
knopf.disabled = true;
meiner = !meiner;
geschafft += meiner ? 1 : -1;
malen(); zahlMalen();
try {
const a = await hole('/workspace/api/eintrag/' + e.id + '/punkt/' + p.schluessel,
meiner
? { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}' }
: { method: 'DELETE' });
if (a.ok) {
const j = await a.json();
geschafft = j.geschafft;
const frisch = (j.punkte || []).find((x) => x.schluessel === p.schluessel);
if (frisch) meiner = !!frisch.meiner;
malen(); zahlMalen();
} else {
/* ZURUECKDREHEN UND SAGEN, WORAN ES LAG. Ein Haken, der
aussieht wie gesetzt und keiner ist, ist schlimmer als
ein Klick, der sichtbar nicht geklappt hat. */
meiner = !meiner;
geschafft += meiner ? 1 : -1;
malen(); zahlMalen();
melde(sagWas((await a.json().catch(() => ({}))).fehler,
'Das Abhaken hat nicht geklappt.'));
}
} catch {
/* umgeleitet -- holen.js schickt bei 401 zur Anmeldung */
} finally { knopf.disabled = false; }
});
zeile.append(knopf);
liste.append(zeile);
}
block.append(liste);
/* WER MACHT MIT? Nur fuer die Leitung (der Server schickt das Feld
sonst gar nicht), und nur, wenn ueberhaupt jemand angefangen hat.
Eine Ueberschrift "Dabei" ueber einer leeren Flaeche sieht aus,
als sei die Abfrage kaputt. */
if (Array.isArray(e.event_stand) && e.event_stand.length) {
const wer = el('div', 'ev-mitmacher');
wer.append(el('span', 'ev-mitmacher__schild', 'Dabei'));
for (const m of e.event_stand) {
const x = el('span', 'ev-mitmacher__person',
(m.name || 'jemand') + ' · ' + m.geschafft + '/' + punkte.length);
if (m.geschafft >= punkte.length) x.dataset.voll = 'ja';
wer.append(x);
}
block.append(wer);
}
return block;
}
/** Die Regeln -- dieselbe Liste, aber ohne Haken.
*
* BEWUSST ANDERS ALS DIE AUFGABEN. Vorher waren beide derselbe graue
* Kasten mit derselben Ueberschriftzeile; nebeneinander sahen "was
* ich tun soll" und "was dabei gilt" gleich wichtig aus. Eine Regel
* ist aber kein Schritt: Man hakt sie nicht ab, man haelt sie ein. */
function eventRegeln(e) {
const punkte = Array.isArray(e.event_regelpunkte) ? e.event_regelpunkte : [];
if (!punkte.length) return null;
const block = el('section', 'ev-block ev-block--regeln');
const kopf = el('div', 'ev-block__kopf');
kopf.append(el('h4', 'ev-block__schild', 'Regeln'));
block.append(kopf);
const liste = el('ul', 'ev-liste ev-liste--still');
for (const p of punkte) {
const zeile = el('li', 'ev-punkt ev-punkt--regel');
zeile.append(el('span', 'ev-punkt__zeichen'));
zeile.append(el('span', 'ev-punkt__text', p.text));
liste.append(zeile);
}
block.append(liste);
return block;
}
/** Der dritte Ausgang fuer die Event-Bloecke.
*
* Kommt vom Server keine Punkteliste (alte Fassung im Netz, Feld
* leer geblieben, Abfrage gescheitert), stand hier sonst GAR NICHTS
* -- die Aufgaben eines laufenden Wettbewerbs waeren wortlos
* verschwunden. Das ist der schlimmere Ausgang: Eine schlichte
* Liste ist ein kleiner Schoenheitsfehler, eine fehlende Bedingung
* ein inhaltlicher Verlust.
*
* Es ist bewusst dieselbe Form wie frueher (Absatz mit erhaltenen
* Zeilenumbruechen), nur im neuen Rahmen. */
function eventRueckfall(name, text) {
const block = el('section', 'ev-block ev-block--roh');
const kopf = el('div', 'ev-block__kopf');
kopf.append(el('h4', 'ev-block__schild', name));
block.append(kopf);
block.append(el('p', 'ev-rohtext', text));
return block;
}
function karte(e) {
const k = el('article', 'eintrag-karte');
k.dataset.dringlich = e.dringlichkeit;
k.dataset.status = e.status;
/* IST DAS EIN EVENT? Dann sieht diese Karte anders aus -- Buehne
statt Bildstreifen, Listen statt Textbloecke, der Zeitraum einmal
statt dreimal. Die Frage wird HIER gestellt und nicht erst unten
bei den Bloecken: Davon haengt auch ab, ob das Bild oben als
Streifen erscheint (bei einem Event ist es der Hintergrund des
Kopfes) -- und diese Entscheidung faellt weiter oben als die
bisherige Zeile `const stand = ...`. */
const evStand = istEventArt(e.art) ? eventStand(e) : null;
if (evStand) k.dataset.eventkarte = 'ja';
/* DIE ART GEHOERT AN DIE KARTE (22.09.2026).
Am Anschlagbrett traegt sie die Farbe: Ansage, Neue Regel,
@@ -701,7 +1089,11 @@
nicht-anspringbaren Kacheln ist keine Galerie. Deshalb ein
echter Verweis mit Beschriftung -- das kann die Tastatur, ohne
eine Zeile JavaScript. */
if (Array.isArray(e.bilder) && e.bilder.length) {
/* BEI EINEM EVENT NICHT HIER: Das erste Bild wird zur Buehne
hinter dem Titel (siehe eventBuehne). Ein Streifen DARUEBER und
dasselbe Bild nochmal dahinter waere die Doppelung, die diese
Karte gerade verloren hat. */
if (!evStand && Array.isArray(e.bilder) && e.bilder.length) {
const bg = el('div', 'bildstreifen');
if (e.bilder.length > 1) bg.dataset.mehrere = 'ja';
for (const b of e.bilder) {
@@ -745,7 +1137,12 @@
k.append(bg);
}
k.append(kopf);
/* Beim Event wandert derselbe Kopf auf die Buehne -- mit allem,
was darin steckt (Etikett, Titel, "nur DogFather", Bewertung).
Ihn dort neu zu bauen hiesse, jede dieser Sonderregeln ein
zweites Mal zu pflegen. */
if (evStand) k.append(eventBuehne(e, evStand, kopf));
else k.append(kopf);
/* ---- DAS VIDEO (17.09.2026), Stufe A des Video-Plans ----------
Der Knopf ist die Antwort auf "und dann liken": Er fuehrt
@@ -874,18 +1271,12 @@
-- da, wo bei allen anderen Karten das Datum steht --, liest man
erst die Aufgaben und danach, dass es seit einer Woche vorbei
ist. */
const stand = istEventArt(e.art) ? eventStand(e) : null;
if (stand) {
k.dataset.event = stand.schluessel;
const zeile = el('p', 'event-zeitraum');
zeile.dataset.stand = stand.schluessel;
zeile.append(el('span', 'event-zeitraum__marke', stand.text));
zeile.append(el('span', 'event-zeitraum__spanne',
e.event_ende && e.event_ende !== e.datum
? `${datum(e.datum)} – ${datum(e.event_ende)}`
: datum(e.datum)));
k.append(zeile);
}
const stand = evStand;
/* Der Zustand bleibt als Merkmal an der Karte -- daran haengt im
CSS, wie ein vorbeigelaufenes Event zuruecktritt. Die ZEILE, die
hier frueher stand ("laeuft bis 16.10.2026" plus Spanne), ist
weg: Sie steht jetzt in der Buehne, und zwar einmal. */
if (stand) k.dataset.event = stand.schluessel;
if (e.text) {
const t = el('p', 'eintrag-text');
@@ -894,15 +1285,35 @@
}
if (stand) {
for (const [feld, name] of [['event_aufgaben', 'Aufgaben'], ['event_regeln', 'Regeln']]) {
if (!e[feld]) continue;
const block = el('div', 'event-block');
block.append(el('h4', 'event-block__schild', name));
/* Zeilenumbrueche bleiben stehen (white-space: pre-line im CSS).
Eine Aufzaehlung, die zu einem Absatz zusammenlaeuft, ist
genau das, was diese beiden Felder verhindern sollen. */
block.append(el('p', 'event-block__text', e[feld]));
k.append(block);
/* DIE ZERLEGUNG MACHT DER SERVER (event_punkte /
event_regelpunkte). Sie hier zu wiederholen hiesse, die
Rechenvorschrift fuer den Schluessel eines Punktes an zwei
Stellen zu haben -- und ein Haken, der nach einem Umbau am
falschen Punkt haengt, faellt niemandem auf.
FAELLT DIE AUSKUNFT AUS (alte Serverfassung, leeres Feld),
bleibt der Textblock als Rueckfall stehen. Lieber eine
schlichte Liste als eine Karte, auf der die Aufgaben fehlen. */
const auf = eventAufgaben(e, stand)
|| (e.event_aufgaben ? eventRueckfall('Aufgaben', e.event_aufgaben) : null);
const reg = eventRegeln(e)
|| (e.event_regeln ? eventRueckfall('Regeln', e.event_regeln) : null);
/* BEIDE IN EINE REIHE, nicht untereinander an die Karte.
Auf 1280 px ist eine Karte rund 1160 px breit. "30
Live-Stunden" ueber diese Breite zu ziehen und darunter noch
einmal "Nur eigene Konten" verschenkt zwei Drittel der Flaeche
und macht die Karte unnoetig lang.
Wie viele Spalten daraus werden, entscheidet das CSS -- und
zwar durch MESSEN (auto-fit mit Mindestbreite), nicht durch
eine Fensterbreite in einer Zahl. Eine feste Schwelle waere
beim naechsten zusaetzlichen Block wieder falsch. */
if (auf || reg) {
const reihe = el('div', 'ev-bloecke');
if (auf) reihe.append(auf);
if (reg) reihe.append(reg);
k.append(reihe);
}
}
@@ -922,7 +1333,12 @@
ES IST EIN LINK, keine Marke. Wer liest „steht im Kalender",
will als Naechstes nachsehen, was noch an dem Tag ist. */
if (e.termin) {
/* BEIM EVENT NICHT: Dort steht der Zeitraum schon in der Buehne.
"Im Kalender am 03.10.2026" waere die DRITTE Zeile mit demselben
Datum gewesen -- gemessen am echten Oktober-Event standen dort
"laeuft bis 16.10.", "03.10. - 16.10." und "Im Kalender am
03.10." untereinander. */
if (e.termin && !evStand) {
/* LINK NUR, WENN DER KALENDER FUER MICH OFFEN IST (19.09.2026).
Fuer die Community ist er es nicht -- `kalender.html` steht in
@@ -1459,7 +1875,7 @@
return;
}
await laden();
}));
}, 'schritt--gefahr'));
}
fuss.append(knoepfe);
k.append(fuss);
@@ -2775,6 +3191,333 @@
Oeffnen zu schalten waere der bequeme Fehler: Man oeffnet mit
"Schulung", waehlt "Agentur-Events" und fuellt ein Formular aus,
das noch nach der alten Art fragt. */
/* =====================================================================
DAS EVENT-FORMULAR (03.10.2026)
Filipe: "und auch wenn man die eintraegt und so soll viel mehr
perfektioniert und uebersichtlicher sein."
WORAN ES LAG, SIEHT MAN AM ERGEBNIS. Im echten Oktober-Event stand
auf der Karte:
.Jeden Tag Live gehen
.22-Live-Stunden erreichen
. 33k Diamanten erreichen
Einmal mit Leerzeichen hinter dem Punkt, zweimal ohne. Das ist
keine Unachtsamkeit, sondern die zwangslaeufige Folge eines leeren
Textfeldes: Wer eine Aufzaehlung tippt, setzt die Zeichen selbst --
und dann sehen drei Zeilen auf drei Arten aus.
DIE LOESUNG IST NICHT, DAS ERGEBNIS ZU PUTZEN, sondern die Eingabe
zu aendern: EINE ZEILE, EIN FELD. Das Aufzaehlungszeichen setzt
das Formular, nicht der Mensch.
DAS TEXTFELD BLEIBT TROTZDEM DIE WAHRHEIT. Abgeschickt wird
weiterhin `event_aufgaben` als Text -- der Server bleibt
unveraendert, und wer lieber tippt (oder etwas aus einer Nachricht
einfuegt), schaltet mit einem Knopf zurueck. Ein Editor, der den
einzigen Weg darstellt, ist eine Sackgasse, sobald er einen Fall
nicht kann.
EINGEFUEGTER MEHRZEILIGER TEXT WIRD AUFGETEILT. Der haeufigste
Weg, wie so eine Liste entsteht, ist Kopieren aus einer Nachricht.
Landete das alles in einem Feld, waere der Editor genau dort im
Weg, wo er helfen soll.
===================================================================== */
/** Blendet die Textgestaltungs-Leiste (F K U S) mit ihrem Feld um.
*
* SIE GEHOERT ZUM TEXTFELD, NICHT ZUM FORMULAR. Solange der
* Zeilen-Editor dran ist, ist das Feld verborgen -- die Leiste
* stand aber weiter da und schwebte unter einer Liste, zu der sie
* nicht gehoert. Beim ersten Lauf sah man unter "+ Punkt" ein
* "F K U S", das nichts bewirkte.
*
* SIE WIRD JEDES MAL NEU GESUCHT und nicht beim Aufbau gemerkt:
* textform.js haengt sie NACH dem Formularaufbau an (sie haengt an
* `laden()`), und etwas zu merken, was es noch nicht gibt, ergibt
* null. */
function textformUmblenden(feld, zeigen) {
const l = feld?.parentElement?.querySelector('.textform');
if (l) l.hidden = !zeigen;
}
/** Baut den Zeilen-Editor vor ein Textfeld.
* feldId: die Nummer des <textarea>, das die Wahrheit bleibt. */
function zeilenEditor(feldId, wort, beispiel) {
const feld = $(feldId);
if (!feld || feld.dataset.editor === 'ja') return;
feld.dataset.editor = 'ja';
const rahmen = el('div', 'zeileneditor');
const liste = el('div', 'zeileneditor__liste');
rahmen.append(liste);
/* Aus dem Feld in den Editor -- beim Oeffnen und beim Zurueck-
schalten. Zerlegt wird nach derselben Regel wie im Server
(Zeilenumbruch, fuehrendes Aufzaehlungszeichen weg); dass beide
dasselbe tun, weist pruef-eventkarte.mjs nach. */
const auseinander = (text) => String(text || '')
.split(/\r?\n/)
.map((z) => z.replace(/^[\s]*[-–—•·*▪◦‣>+]+[\s]*/u, '').trim())
.filter(Boolean);
/* ... und zurueck. Das Zeichen setzt das Formular, einheitlich. */
const zusammen = () => [...liste.querySelectorAll('.zeileneditor__feld')]
.map((f) => f.value.trim())
.filter(Boolean)
.map((t) => '· ' + t)
.join('\n');
const schreiben = () => {
feld.value = zusammen();
/* Das Ereignis von Hand ausloesen: Die Vorschau haengt daran,
und ein Wert, der per Skript gesetzt wird, loest `input` nicht
von selbst aus. */
feld.dispatchEvent(new Event('input', { bubbles: true }));
};
function zeileBauen(wert, fokus) {
const z = el('div', 'zeileneditor__zeile');
const punkt = el('span', 'zeileneditor__zeichen');
punkt.setAttribute('aria-hidden', 'true');
const f = el('input', 'zeileneditor__feld');
f.type = 'text';
f.value = wert || '';
f.maxLength = 300;
f.setAttribute('aria-label', wort + ': ein Punkt');
/* DER PLATZHALTER GEHOERT ZUM FELD. "z. B. 22 Live-Stunden
erreichen" stand im ersten Lauf auch unter REGELN -- und ein
Beispiel, das zur Ueberschrift darueber nicht passt, ist eine
Fehlanleitung, keine Hilfe. */
f.placeholder = beispiel;
f.addEventListener('input', schreiben);
/* EINFUEGEN AUS EINER NACHRICHT. Der haeufigste Weg, wie so eine
Liste entsteht. Ohne das landete der ganze Block in einem
Feld -- und der Editor waere genau dort hinderlich, wo er
helfen soll. */
f.addEventListener('paste', (ev) => {
const roh = ev.clipboardData?.getData('text') || '';
if (!/\r?\n/.test(roh)) return;
ev.preventDefault();
const teile = auseinander(roh);
if (!teile.length) return;
f.value = teile[0];
let davor = z;
for (const t of teile.slice(1)) {
const neu = zeileBauen(t);
davor.insertAdjacentElement('afterend', neu);
davor = neu;
}
schreiben();
});
f.addEventListener('keydown', (ev) => {
/* ENTER MACHT EINE NEUE ZEILE -- so wie in jeder Liste, die
man kennt. Ohne das muesste man nach jedem Punkt zur Maus
greifen, und bei fuenf Punkten ist das fuenfmal. */
if (ev.key === 'Enter') {
ev.preventDefault();
const neu = zeileBauen('', true);
z.insertAdjacentElement('afterend', neu);
schreiben();
return;
}
/* RUECKTASTE IM LEEREN FELD nimmt die Zeile weg und setzt den
Schreibzeiger ans Ende der vorigen -- dieselbe Erwartung wie
in einem Textfeld. Die letzte Zeile bleibt stehen: ein
Editor ohne ein einziges Feld hat keinen Weg zurueck. */
if (ev.key === 'Backspace' && !f.value) {
const alle = [...liste.querySelectorAll('.zeileneditor__zeile')];
if (alle.length <= 1) return;
ev.preventDefault();
const stelle = alle.indexOf(z);
const ziel = alle[stelle - 1] || alle[stelle + 1];
z.remove();
schreiben();
const zf = ziel?.querySelector('.zeileneditor__feld');
if (zf) { zf.focus(); zf.setSelectionRange(zf.value.length, zf.value.length); }
}
});
const weg = el('button', 'zeileneditor__weg');
weg.type = 'button';
weg.setAttribute('aria-label', 'Diesen Punkt entfernen');
weg.title = 'Diesen Punkt entfernen';
weg.addEventListener('click', () => {
const alle = [...liste.querySelectorAll('.zeileneditor__zeile')];
if (alle.length <= 1) { f.value = ''; schreiben(); f.focus(); return; }
z.remove();
schreiben();
});
z.append(punkt, f, weg);
if (fokus) setTimeout(() => f.focus(), 0);
return z;
}
const fuellen = () => {
liste.textContent = '';
const teile = auseinander(feld.value);
if (!teile.length) liste.append(zeileBauen(''));
else for (const t of teile) liste.append(zeileBauen(t));
};
fuellen();
const fuss = el('div', 'zeileneditor__fuss');
const mehr = el('button', 'zeileneditor__mehr', '+ Punkt');
mehr.type = 'button';
mehr.addEventListener('click', () => {
liste.append(zeileBauen('', true));
schreiben();
});
fuss.append(mehr);
/* DER WEG ZURUECK ZUM TEXTFELD. Er steht leise daneben, nicht als
zweiter Hauptknopf: Der Normalfall ist die Liste. */
const umschalten = el('button', 'zeileneditor__wechsel', 'als Text bearbeiten');
umschalten.type = 'button';
umschalten.addEventListener('click', () => {
const alsText = rahmen.hidden !== true;
if (alsText) {
rahmen.hidden = true;
feld.hidden = false;
textformUmblenden(feld, true);
umschalten.textContent = 'als Liste bearbeiten';
feld.focus();
} else {
/* Zurueck in die Liste: neu einlesen, damit auch das ankommt,
was im Textfeld getippt wurde. */
fuellen();
rahmen.hidden = false;
feld.hidden = true;
textformUmblenden(feld, false);
umschalten.textContent = 'als Text bearbeiten';
}
});
fuss.append(umschalten);
rahmen.append(fuss);
feld.hidden = true;
textformUmblenden(feld, false);
feld.insertAdjacentElement('beforebegin', rahmen);
/* Der Umschalter gehoert NEBEN beides und nicht in den Rahmen --
sonst verschwindet er mit, sobald man zum Textfeld wechselt. */
rahmen.insertAdjacentElement('afterend', fuss);
/* Wird das Feld von aussen gesetzt (Vorlage uebernehmen, Formular
zuruecksetzen), muss die Liste nachziehen. */
feld.addEventListener('frischfuellen', fuellen);
}
/* =====================================================================
DIE VORSCHAU (03.10.2026)
Sie zeigt die BUEHNE, und zwar mit genau der Funktion, die sie
spaeter auch auf dem Brett baut (eventBuehne). Eine nachgebaute
Vorschau waere eine zweite Fassung derselben Sache -- und die
zeigt irgendwann etwas, das hinterher anders aussieht. Dann ist
sie schlimmer als keine.
SIE ERSCHEINT ERST, WENN ES ETWAS ZU ZEIGEN GIBT. Eine leere
Vorschau ueber einem leeren Formular ist nur ein weiterer Kasten.
===================================================================== */
function vorschauZeichnen() {
const ziel = $('ev-vorschau');
if (!ziel) return;
const event = istEventArt($('f-art').value);
const titel = ($('f-titel')?.value || '').trim();
if (!event || !titel) { ziel.hidden = true; ziel.textContent = ''; return; }
const heute = window.heuteLokal ? window.heuteLokal()
: new Date().toISOString().slice(0, 10);
const e = {
id: 0,
titel,
art: EVENT_ART,
datum: $('f-datum')?.value || heute,
event_ende: $('f-ende')?.value || null,
bilder: [],
};
const stand = eventStand(e);
if (!stand) { ziel.hidden = true; ziel.textContent = ''; return; }
ziel.hidden = false;
ziel.textContent = '';
ziel.append(el('p', 'ev-vorschau__schild', 'So sieht es nachher aus'));
const karte = el('article', 'eintrag-karte');
karte.dataset.eventkarte = 'ja';
karte.dataset.event = stand.schluessel;
karte.dataset.status = 'offen';
/* Derselbe Kopf wie auf dem Brett: Etikett und Titel. */
const kopf = el('div', 'eintrag-kopf');
kopf.append(el('span', 'marke-art', einstellung?.arten?.[EVENT_ART] || 'Agentur-Events'));
kopf.append(el('h3', 'eintrag-titel', titel));
karte.append(eventBuehne(e, stand, kopf));
ziel.append(karte);
}
/** Wie lange laeuft das Event? Steht unter dem Zeitraum.
*
* NICHT NUR ZIERDE: "02.10. bis 15.10." liest man als Spanne, aber
* ob das zwei Wochen sind, rechnet man im Kopf nach. Und ein Ende
* VOR dem Anfang ist ein Tippfehler, den man sonst erst auf der
* Karte sieht -- dort steht dann "vorbei seit 3 Tagen" an einem
* Event, das gerade erst angelegt wurde. */
function dauerZeigen() {
const ziel = $('hinweis-dauer');
if (!ziel) return;
const event = istEventArt($('f-art').value);
const von = $('f-datum')?.value || '';
const bis = $('f-ende')?.value || '';
if (!event || !von) { ziel.textContent = ''; delete ziel.dataset.warnung; return; }
if (!bis) {
ziel.textContent = 'Ohne Ende gilt der eine Tag.';
delete ziel.dataset.warnung;
return;
}
const a = new Date(von + 'T00:00:00').getTime();
const b = new Date(bis + 'T00:00:00').getTime();
if (Number.isNaN(a) || Number.isNaN(b)) { ziel.textContent = ''; return; }
if (b < a) {
ziel.textContent = 'Das Ende liegt vor dem Anfang.';
ziel.dataset.warnung = 'ja';
return;
}
delete ziel.dataset.warnung;
/* EINSCHLIESSLICH BEIDER TAGE: Ein Event vom 2. bis zum 15. laeuft
14 Tage, nicht 13 -- der letzte Tag zaehlt mit, und genau so
rechnet auch eventNoch auf der Karte. */
const tage = Math.round((b - a) / 86400000) + 1;
ziel.textContent = tage === 1 ? 'Ein Tag.' : `Läuft ${tage} Tage.`;
}
/** Das Formular in den Zustand "frisch" bringen.
*
* AN EINER STELLE, weil es zwei Anlaesse gibt (Abbrechen und
* erfolgreiches Speichern) und beide dasselbe meinen. Vorher gab es
* zwei Fassungen, und die kuerzere liess Vorschau und Zeilen-Editor
* stehen -- der Unterschied faellt erst auf, wenn jemand zwei
* Events nacheinander anlegt. */
function formularLeeren() {
$('neu').reset();
$('neu-fehler').textContent = '';
/* `form.reset()` leert die Textfelder, aber die Zeilenfelder davor
stehen noch voll da -- und beim naechsten Tippen schrieben sie
den alten Stand zurueck. */
for (const f of ['f-aufgaben', 'f-regeln']) {
$(f)?.dispatchEvent(new Event('frischfuellen'));
}
/* Und das raeumt Dauerhinweis und Vorschau mit ab. */
formularAnpassen();
}
function formularAnpassen() {
const event = istEventArt($('f-art').value);
@@ -2791,6 +3534,18 @@
$('feld-ende').hidden = !event;
$('feld-aufgaben').hidden = !event;
$('feld-regeln').hidden = !event;
/* DER ZEILEN-EDITOR WIRD ERST GEBAUT, WENN ER GEBRAUCHT WIRD --
und nur einmal (zeilenEditor merkt sich das am Feld). Ihn beim
Laden der Seite fuer jedes Brett aufzubauen hiesse, zwei
Eingabehilfen vorzuhalten, die auf fuenf von sechs Brettern nie
sichtbar werden. */
if (event) {
zeilenEditor('f-aufgaben', 'Aufgaben', 'z. B. 22 Live-Stunden erreichen');
zeilenEditor('f-regeln', 'Regeln', 'z. B. Nur Streams auf dem eigenen Konto');
}
dauerZeigen();
vorschauZeichnen();
if (einstellung?.dringlichkeit) $('feld-dringlich').hidden = event;
if (einstellung?.vertraulich) $('feld-vertraulich').hidden = event;
@@ -2845,13 +3600,19 @@
umschalten(true);
});
$('neu-abbrechen').addEventListener('click', () => {
$('neu').reset(); $('neu-fehler').textContent = ''; formularAnpassen(); umschalten(false);
formularLeeren(); umschalten(false);
});
/* "change" und nicht "input": Die Auswahl wird ueber wahl.js zu einer
Knopfliste umgebaut, und die meldet ihre Wahl als change-Ereignis
am urspruenglichen <select> -- ohne dieses Ereignis merkte das
Formular vom Artwechsel nichts. */
$('f-art').addEventListener('change', formularAnpassen);
/* Dauer und Vorschau haengen an vier Feldern. Ein einziger Zuhoerer
am Formular statt vier einzelnen: Kommt ein fuenftes Feld dazu,
ist es automatisch dabei -- eine Liste von Feldnamen waere die,
die beim naechsten Umbau nicht nachgezogen wird. */
$('neu')?.addEventListener('input', () => { dauerZeigen(); vorschauZeichnen(); });
$('neu')?.addEventListener('change', () => { dauerZeigen(); vorschauZeichnen(); });
/* DER HINWEIS ZIEHT MIT. Ohne diese beiden Zeilen stuende er auf dem
Stand vom Oeffnen des Formulars -- also dauerhaft auf „Heute", auch
nachdem jemand den 24. eingetragen hat. Ein Hinweis, der beim Tippen
@@ -2915,7 +3676,22 @@
$('neu-fehler').textContent = sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.');
return;
}
$('neu').reset(); umschalten(false);
/* DIESELBE FUNKTION WIE BEIM ABBRECHEN (03.10.2026).
Hier stand `$('neu').reset()` allein -- also eine ZWEITE
Fassung des Zuruecksetzens, die weniger tat als die beim
Abbrechen. Gemessen hat das die Pruefung: Nach dem Speichern
blieb die Vorschau mit dem gerade angelegten Event stehen
(verborgen: false, 81 Zeichen Inhalt), und beim naechsten
"Neuer Eintrag" standen die Aufgaben des vorigen Events noch
im Editor -- man haette sie versehentlich ein zweites Mal
eingetragen.
`form.reset()` allein kann das gar nicht leisten: Es setzt
Formularfelder auf ihre Vorgabe zurueck und weiss nichts von
Zeilen-Editoren, Hinweisen oder einer Vorschau, die daneben
stehen. */
formularLeeren(); umschalten(false);
await laden();
} catch { /* umgeleitet */ } finally { knopf.disabled = false; }
});
@@ -3233,7 +4009,18 @@
if (window.Textform) {
for (const id of ['f-text', 'f-aufgaben', 'f-regeln']) {
const feld = document.getElementById(id);
if (feld) window.Textform.leiste(feld);
if (!feld) continue;
window.Textform.leiste(feld);
/* ERST DANACH VERSTECKEN. Der Zeilen-Editor steht bei den
Event-Feldern schon (er entsteht in formularAnpassen, und
das laeuft innerhalb von laden()) -- die Leiste kommt also
NACH ihm und waere sonst als einziges Stueck des Textfeldes
sichtbar geblieben. Das war im ersten Bildschirmfoto das
"F K U S" unter der Punkteliste. */
if (feld.dataset.editor === 'ja' && feld.hidden) {
const l = feld.parentElement?.querySelector('.textform');
if (l) l.hidden = true;
}
}
}
})();