Kampagnen-Kacheln: gegliederte Texte, Geschenke mit Preis, aufklappbar

Filipe, mit einem Bildschirmfoto des Regelblocks: "man soll die titel
in den texten besser erkennen und sehen was zu was passt. ich will
auch dass man sieht welche geschenke bei dem event zaehlen und so.
ich will alle informationen die moeglich sind wenn man den link
hochlaedt. ... auch wenn man teilt soll es einfach perfekt sein. man
soll alle infos sehen die man braucht. die kacheln von den events
soll auch nicht so lang sein, die soll man aufklappen koennen. genau
wie eine trennung fuer heute, letzte woche letzten monat und so."

DER FEHLER LAG NICHT IM TEXT, SONDERN IN SEINER ANZEIGE. Im Feld
stand die ganze Zeit eine Gliederung -- Hinweise mit "* ", eine
Ueberschrift mit Untertitel hinter einem Gedankenstrich, Absaetze
ohne Zeichen, Aufzaehlungen mit "- ". Die Kachel hat daraus mit
punkteAusText() EINE flache Liste gemacht: jede Zeile derselbe
Strich, alle gleich gross.

  gliederung()      liest die Gliederung, die schon dasteht, statt
                    den Text umzuschreiben. Die beiden Kampagnen, die
                    heute in der Datenbank liegen, sehen dadurch
                    sofort richtig aus -- ohne Umstellung.
  Geschenke         kommen aus der gelesenen LISTE, nicht aus dem
                    Text, und stehen OHNE Aufklappen auf der Kachel.
  Aufklappen        alles Uebrige hinter einem Druck: gemessen 555 px
                    weniger je Kachel bei Gipfelstuermer.
  Gruppen           je Spalte laeuft / kommt / vorbei, Vergangenes
                    nach diese Woche, dieser Monat, aelter.

NEU GELESEN (stand in allen vier gespeicherten Seiten und wurde nie
angesehen): die Rangliste mit ihren Gruppen ("Superstars" bis
"5. Liga", bei Glow Up "Team Glow"/"Team Shine"), die Beschreibung je
Missionsgruppe, der Steckbrief (Kurznummer, Markt, Sprache, Zone),
der zweite Bildbaustein, und Ueberschriften INNERHALB der Klapptexte
-- "WELCHE LIGA BIN ICH?" hat jetzt sechs eingerueckte Unterpunkte
statt zwoelf gleichrangiger Zeilen.

EVENT_TEXT_MAX 6000 -> 20000. Der Regeltext von Gipfelstuermer ist
7679 Zeichen lang; es fielen bisher jedes Mal rund 1600 Zeichen
echter Inhalt weg. Die Zahl steht jetzt an EINER Stelle.

TEILEN: Was die KAMPAGNE sagt, geht hinaus (Aufgaben, Geschenke,
Regeln) -- wer WIR sind, nicht (Name, Rolle, Haus, Kampagnennummer,
Steckbrief). Und keine Hinweiszeile: "Die Preise konnten nicht
gelesen werden -- bitte nachtragen" ist eine Notiz an DogFather und
hat auf einer Seite, die er in Discord postet, nichts zu suchen.

BERICHTIGT, WEIL NACHGEMESSEN:
  - Number(null) ist 0 und 0 ist endlich -- "Feuerwerk" bekam an drei
    Stellen "0 Coins" angeschrieben, also eine falsche Preisangabe.
  - Ein Kommentar im Leser behauptete, der Wertungszeitraum weiche
    vom Kampagnenzeitraum ab. Nachgemessen ist er in allen drei
    Seiten mit Rangliste auf die Millisekunde gleich; ich hatte zwei
    verschiedene Kampagnen verglichen.
  - bild-kampagne.mjs verlangte "jede Kachel traegt den Ton ihrer
    Seite" und war damit seit dem 07.10. rot, weil an dem Tag bewusst
    das Gegenteil entschieden wurde (Zustandston, Seitenkante). Nicht
    aufgefallen, weil nach dem Lauf nur die Zahl der MESSUNGEN notiert
    wurde, nicht die der FEHLER.
  - .kv-seite > .eintrag-karte wird .kv-seite .eintrag-karte: Die
    Karten haengen durch die Gruppen eine Ebene tiefer. Das waere der
    fuenfte Direktkind-Selektor gewesen, der nach einem Umbau still
    ausfaellt -- diesmal vorher gesehen.

GEPRUEFT: 186 (lesen, +58), 148 (Route/Teilen, +25), 158 (Kachel,
+64), 67 (Agentur, +5), 67 Messungen (Bild, +14), dazu struktur,
css-klassen, zeichen, deutsche-texte, tippziele, lesbarkeit und
haus-trennung (107, unveraendert). Sechs Gegenproben: alle sechs
Sabotagen werden bemerkt, alle Dateien byte-gleich wiederhergestellt.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-07 22:39:03 +02:00
co-authored by Claude Opus 5
parent d7720a3687
commit cadab24a5f
58 changed files with 3950 additions and 798 deletions
+414 -14
View File
@@ -2679,7 +2679,12 @@ body[data-ton] .kopf-zeile::before {
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) {
/* `:not(.ev-punkt--regel)` ist am 07.10.2026 entfallen: Diese
Klasse gibt es nicht mehr (sie heisst jetzt `.ev-zeile` und ist
kein `.ev-punkt`). Ein `:not()` auf eine Klasse, die niemand
mehr setzt, schliesst nichts aus und liest sich wie eine
Bedingung, die es noch gibt. */
.ev-punkt:not(:has(.ev-punkt__knopf)) {
display: flex; align-items: flex-start; gap: 10px;
padding: 9px 10px;
font-size: .9rem; line-height: 1.45;
@@ -2768,22 +2773,30 @@ body[data-ton] .kopf-zeile::before {
.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 {
/* Eine Zeile in einem Abschnitt: dasselbe Raster wie bei den
Aufgaben, aber ein Strich statt eines Kaestchens -- man sieht auf
einen Blick, dass hier nichts zu tun ist.
HIESS BIS ZUM 07.10.2026 `.ev-punkt--regel` und stand in einer
flachen Regelliste. Die gibt es nicht mehr (siehe `gliederung()`
in kampagne-lesen.mjs); umbenannt statt verdoppelt, damit nicht
zwei Zeilenstile mit demselben Aussehen nebeneinander liegen. */
.ev-zeile {
display: flex; align-items: flex-start; gap: 10px;
padding: 6px 10px;
font-size: .87rem; line-height: 1.5;
margin: 0; padding: 5px 2px;
font-size: .87rem; line-height: 1.55;
color: var(--text-leise);
}
.ev-punkt__zeichen {
.ev-zeile__text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.ev-punkt__zeichen,
.ev-zeile__zeichen {
flex: none;
width: 19px; height: 19px;
margin-top: 1px;
position: relative;
}
.ev-punkt__zeichen::before {
.ev-punkt__zeichen::before,
.ev-zeile__zeichen::before {
content: "";
position: absolute; left: 3px; top: 9px;
width: 11px; height: 1.5px;
@@ -3152,12 +3165,36 @@ body[data-ton] .kopf-zeile::before {
/* Der Abstand kommt jetzt vom Raster. Zwei Abstandsquellen ergeben
einen doppelten Abstand -- und zwar nur zwischen den Zeilen, was
schwer zu finden ist. */
/* ==== `.kv-seite` OHNE `>` -- DAS FUENFTE MAL (07.10.2026) =====
Hier stand `.kv-seite > .eintrag-karte`. Seit die Spalten nach
Zustand und Alter gegliedert sind, haengen die Karten in einem
`.ev-gruppe__inhalt` -- also eine Ebene tiefer, und der
Direktkind-Selektor haette aufgehoert zu greifen. Folge waere
gewesen: Eventkacheln ohne `display: flex`, also ohne den Fuss
unten und mit der Buehne eingerahmt vom Kartenpolster.
DAS IST IM PROJEKT SCHON VIERMAL PASSIERT -- zuletzt am
07.10.2026 bei `#liste > .eintrag-karte[data-event=...]`, wo
alle drei Zustandsfarben ausfielen und ein Feature vom Vortag
rueckgaengig gemacht wurde, ohne dass eine Pruefung rot wurde.
Diesmal vorher gesehen, nicht hinterher gemessen.
WAS DIREKTKIND BLEIBT UND WARUM: `> *:not(.ev-buehne)` meint
die eigenen Kinder DER KARTE. Die Karte ist nach wie vor der
Vater ihres Inhalts; dieser Selektor beschreibt also wirklich
eine Vater-Kind-Beziehung und nicht eine Lage im Baum. Nur die
Strecke von der SPALTE zur Karte wird frei.
Dass `.kv-seite` ueberhaupt davorsteht, bleibt noetig: Ohne sie
hiesse die Regel „jede Eventkachel ueberall" -- und `#liste >`
darueber waere dann sinnlos. */
#liste > .eintrag-karte,
.kv-seite > .eintrag-karte,
.kv-seite .eintrag-karte,
.kv-rest > .eintrag-karte { margin-bottom: 0; }
#liste > .eintrag-karte[data-eventkarte="ja"],
.kv-seite > .eintrag-karte[data-eventkarte="ja"] {
.kv-seite .eintrag-karte[data-eventkarte="ja"] {
display: flex;
flex-direction: column;
/* Die Buehne liegt am oberen Rand und soll ihn beruehren -- das
@@ -3166,13 +3203,13 @@ body[data-ton] .kopf-zeile::before {
padding: 0 0 15px;
}
#liste > .eintrag-karte[data-eventkarte="ja"] > *:not(.ev-buehne),
.kv-seite > .eintrag-karte[data-eventkarte="ja"] > *:not(.ev-buehne) {
.kv-seite .eintrag-karte[data-eventkarte="ja"] > *:not(.ev-buehne) {
padding-left: 18px; padding-right: 18px;
}
#liste > .eintrag-karte[data-eventkarte="ja"] .ev-block,
.kv-seite > .eintrag-karte[data-eventkarte="ja"] .ev-block { margin-left: 0; margin-right: 0; }
.kv-seite .eintrag-karte[data-eventkarte="ja"] .ev-block { margin-left: 0; margin-right: 0; }
#liste > .eintrag-karte[data-eventkarte="ja"] .eintrag-fuss,
.kv-seite > .eintrag-karte[data-eventkarte="ja"] .eintrag-fuss { margin-top: auto; }
.kv-seite .eintrag-karte[data-eventkarte="ja"] .eintrag-fuss { margin-top: auto; }
/* =====================================================================
DIE KARTE SAGT, OB DAS EVENT LAEUFT (07.10.2026)
@@ -3740,3 +3777,366 @@ body[data-ton] .kopf-zeile::before {
@media (prefers-reduced-motion: reduce) {
.kampagneholen textarea { transition: none; }
}
/* =====================================================================
GEGLIEDERTE INFOS, GESCHENKE, AUFKLAPPEN, GRUPPEN (07.10.2026)
Filipe, mit einem Bildschirmfoto des Regelblocks: „man soll die
titel in den texten besser erkennen und sehen was zu was passt.
ich will auch dass man sieht welche geschenke bei dem event zaehlen
und so. ... die kacheln von den events soll auch nicht so lang
sein, die soll man aufklappen koennen damit die nicht zu lange
sind. genau wie eine trennung fuer heute, letzte woche letzten
monat und so."
AUGENSCHONEND (CLAUDE.md, 12.08.2026): Die Unterscheidung laeuft
ueber GROESSE, GEWICHT, SPERRUNG und EINRUECKUNG -- nicht ueber
grelle Farben. Eine Ueberschrift darf man sehen, ohne dass sie
leuchtet. Die einzige kraeftige Farbe im ganzen Block ist der
Coinpreis, und der ist ein gedecktes Gold, kein Neon.
===================================================================== */
/* ---- Der Abschnitt -------------------------------------------------- */
.ev-abschnitte {
display: flex; flex-direction: column;
gap: 14px;
}
.ev-abschnitt { margin: 0; }
/* STUFE 2 IST EIN EIGENER ABSCHNITT (KAMPAGNENGESCHENKE, BELOHNUNGEN,
TEILNAHME), STUFE 3 EINE UEBERSCHRIFT AUS EINEM FREMDEN TEXT
("Superstars", "1. Liga", "WELCHE LIGA BIN ICH?").
Die Einrueckung macht die Verschachtelung sichtbar, ohne dass der
HTML-Baum sie nachbauen muss -- die Gliederung bleibt eine flache
Liste (Begruendung bei `gliederung()` in kampagne-lesen.mjs). */
.ev-abschnitt[data-stufe="3"] {
margin-left: 2px;
padding-left: 11px;
border-left: 2px solid color-mix(in srgb, var(--rand) 85%, transparent);
}
.ev-abschnitt__titel {
margin: 0 0 6px;
/* .78rem = 12,5 px. In Grossbuchstaben mit Sperrung wirkt eine
Schrift kleiner, als die Zahl sagt -- dieselbe Begruendung steht
schon bei .ev-block__schild. */
font-size: .78rem; font-weight: 700;
letter-spacing: .08em; text-transform: uppercase;
line-height: 1.4;
color: color-mix(in srgb, var(--ev-ton, var(--akzent, #3fbdf5)) 55%, var(--text));
}
/* Die Unterstufe ist KEINE Grossschreibung: Sie kommt aus einem
fremden Text, und "Du kannst an dieser Liga teilnehmen" in
Grossbuchstaben waere geschrien. Sie unterscheidet sich durch
Gewicht und Farbe, nicht durch Lautstaerke. */
.ev-abschnitt[data-stufe="3"] > .ev-abschnitt__titel {
font-size: .85rem; font-weight: 650;
letter-spacing: .01em; text-transform: none;
color: var(--text);
}
.ev-abschnitt__unter {
margin: -2px 0 7px;
font-size: .82rem; line-height: 1.5;
color: var(--text-still);
}
/* Ein Absatz. `--text-leise` und nicht `--text`: Der Regeltext ist
Nachschlagewerk, nicht das, weswegen jemand die Kachel aufmacht. */
.ev-absatz {
margin: 0 0 7px;
font-size: .87rem; line-height: 1.6;
color: var(--text-leise);
overflow-wrap: anywhere;
}
.ev-absatz:last-child { margin-bottom: 0; }
/* Eine Frage mit Antwort darunter -- die Gliederung der
Teilnahmebedingungen. Begruendung bei `gliederung()`. */
.ev-frage {
margin: 11px 0 5px;
font-size: .87rem; font-weight: 650; line-height: 1.5;
color: var(--text);
overflow-wrap: anywhere;
}
.ev-frage:first-child { margin-top: 0; }
.ev-zeilen {
list-style: none;
margin: 0 0 7px; padding: 0;
display: flex; flex-direction: column; gap: 1px;
}
.ev-zeilen:last-child { margin-bottom: 0; }
/* ---- Die Geschenke -------------------------------------------------- */
/* DER EINE BLOCK, DER NICHT HINTER DEM AUFKLAPPEN LIEGT.
"Welche Geschenke zaehlen" ist die Frage, die am haeufigsten
gestellt wird, und die Antwort passt in eine Zeile. Der Rahmen in
gedecktem Gold grenzt ihn vom Aufgabenblock daneben ab, ohne mit
ihm zu konkurrieren. */
.ev-block--gaben {
border-color: color-mix(in srgb, #e2c15a 26%, var(--rand));
background:
linear-gradient(180deg,
color-mix(in srgb, #e2c15a 6%, transparent),
transparent 60%),
rgba(255, 255, 255, .022);
}
.ev-gaben__woher {
margin: 0 0 9px;
font-size: .82rem; line-height: 1.45;
color: var(--text-leise);
}
.ev-gaben__unter {
display: block; margin-top: 2px;
font-size: .8rem; color: var(--text-still);
}
.ev-gaben {
display: flex; flex-wrap: wrap; gap: 7px;
margin: 0; padding: 0; list-style: none;
}
/* Eine Plakette je Geschenk, nicht eine Zeile: Fuenf Geschenke
untereinander sind fuenf Zeilen, nebeneinander eine. Genau darum
ging es -- die Kachel soll kuerzer werden. */
.ev-gabe {
display: inline-flex; align-items: center; gap: 7px;
margin: 0; padding: 5px 10px;
border: 1px solid color-mix(in srgb, #e2c15a 22%, var(--rand));
border-radius: 999px;
background: color-mix(in srgb, #e2c15a 7%, rgba(255, 255, 255, .02));
font-size: .84rem; line-height: 1.35;
color: var(--text);
max-width: 100%;
}
.ev-gabe__name { font-weight: 600; overflow-wrap: anywhere; }
/* Der Coinpreis mit einem Muenzpunkt davor. `tabular-nums`, damit
1 und 12.000 untereinander dieselbe Breite je Ziffer haben. */
.ev-gabe__coins {
display: inline-flex; align-items: center; gap: 5px;
flex: none;
padding: 1px 8px 1px 7px;
border-radius: 999px;
background: color-mix(in srgb, #e2c15a 17%, transparent);
color: color-mix(in srgb, #e2c15a 78%, var(--text));
font-size: .79rem; font-weight: 700;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.ev-gabe__coins::before {
content: "";
flex: none;
width: 8px; height: 8px;
border-radius: 50%;
background: color-mix(in srgb, #e2c15a 72%, transparent);
}
/* Der Hinweis zum Geschenk ("Sammle Feuerwerk in deinem LIVE!") --
bei einem Kampagnengeschenk steht hier, WIE man es bekommt, und
das ist die Auskunft anstelle eines Preises. */
.ev-gabe__zusatz {
font-size: .79rem; color: var(--text-still);
overflow-wrap: anywhere;
}
.ev-gabe__zusatz::before {
content: "·";
margin-right: 5px;
opacity: .7;
}
/* ---- Die Hinweise --------------------------------------------------- */
/* WAS BEIM EINLESEN GEFEHLT HAT -- still, aber nicht versteckt.
Im Bildschirmfoto vom 07.10.2026 standen diese Zeilen MITTEN in
der Regelliste, mit demselben Strich davor wie eine Regel. "Diese
Kampagne hat keine Preisliste auf der Seite" las sich damit wie
eine Teilnahmebedingung. Jetzt stehen sie unter den Bloecken, in
einem eigenen Kasten, mit einem Fragezeichen-Kreis davor. */
.ev-hinweise {
display: flex; flex-direction: column; gap: 5px;
margin: 12px 0 0;
padding: 10px 13px;
border: 1px dashed color-mix(in srgb, var(--rand) 85%, transparent);
border-radius: var(--radius-klein);
background: rgba(255, 255, 255, .012);
}
.ev-hinweis {
display: flex; align-items: flex-start; gap: 8px;
margin: 0;
font-size: .81rem; line-height: 1.5;
color: var(--text-still);
overflow-wrap: anywhere;
}
.ev-hinweis::before {
content: "i";
flex: none;
width: 16px; height: 16px; margin-top: 2px;
border: 1px solid color-mix(in srgb, var(--text-still) 45%, transparent);
border-radius: 50%;
/* .74rem = 11,84 px. NICHT kleiner: pruef-css-klassen zieht die
Grenze bei 11,5 px, und sie gilt auch fuer ein einzelnes Zeichen
-- wer eine Ausnahme fuer "nur ein i" macht, macht die naechste
fuer "nur eine Zahl". Der Kreis ist dafuer 16 px statt 15. */
font-size: .74rem; font-weight: 700; font-style: italic;
line-height: 14px; text-align: center;
color: var(--text-still);
}
/* ---- Der Steckbrief ------------------------------------------------- */
.ev-block--steck { background: rgba(255, 255, 255, .012); }
.ev-steck {
display: grid;
/* Name und Wert in zwei Spalten: `auto` fuer den Namen, damit er
nur so breit wird, wie der laengste ihn braucht. */
grid-template-columns: auto 1fr;
gap: 4px 14px;
margin: 0;
}
.ev-steck__name {
margin: 0;
font-size: .78rem; font-weight: 600;
letter-spacing: .03em;
color: var(--text-still);
}
.ev-steck__wert {
margin: 0;
font-size: .82rem;
font-variant-numeric: tabular-nums;
color: var(--text-leise);
overflow-wrap: anywhere;
}
/* ---- Aufklappen: alles Uebrige hinter einem Druck ------------------- */
.ev-mehr { margin-top: 12px; }
.ev-mehr__knopf {
display: flex; align-items: center; gap: 9px;
width: 100%;
padding: 8px 12px;
font: inherit; font-size: .78rem; font-weight: 600;
letter-spacing: .04em; text-transform: uppercase;
color: var(--text-leise);
text-align: left; cursor: pointer;
background: color-mix(in srgb, var(--tinte) 88%, #ffffff 12%);
border: 1px dashed color-mix(in srgb, var(--rand) 80%, transparent);
border-radius: 9px;
}
.ev-mehr__knopf:hover, .ev-mehr__knopf:focus-visible {
color: var(--text);
border-style: solid;
border-color: color-mix(in srgb, var(--ev-ton, var(--akzent)) 45%, transparent);
}
.ev-mehr__wort { flex: 0 0 auto; }
/* DIE ZAHL SAGT, WAS DAHINTER STEHT. Ein zugeklappter Kasten ohne
Zahl sieht aus, als sei er leer -- und dann drueckt niemand. */
.ev-mehr__zahl {
flex: 1 1 auto; min-width: 0;
font-size: .76rem; font-weight: 500;
letter-spacing: .01em; text-transform: none;
color: var(--text-still);
}
.ev-mehr__knopf .klapp-pfeil { flex: none; }
.ev-mehr__inhalt { margin-top: 10px; }
.ev-mehr__inhalt > .ev-block + .ev-block { margin-top: 10px; }
/* ---- Die Gruppen einer Spalte --------------------------------------- */
/* Dasselbe Aussehen wie die Zeitgruppen der anderen Bretter
(.zeitgruppe__kopf) -- ein zweiter Stil fuer dieselbe Sache waere
eine zweite Wahrheit darueber, wie eine Gruppe aussieht. Nur der
Ton kommt aus der Spalte (`--kv-ton`): So gehoeren die Gruppen
sichtbar zu ihrer Seite der Trennung. */
.ev-gruppe__kopf {
display: flex; align-items: center; gap: 9px;
width: 100%; margin: 10px 0 0;
padding: 7px 10px;
font: inherit; font-size: .76rem; font-weight: 600;
letter-spacing: .04em; text-transform: uppercase;
color: var(--text-leise);
text-align: left; cursor: pointer;
background: color-mix(in srgb, var(--tinte) 88%, #ffffff 12%);
border: 1px solid color-mix(in srgb, var(--rand) 70%, transparent);
border-left: 3px solid color-mix(in srgb, var(--kv-ton, var(--akzent)) 45%, transparent);
border-radius: 9px;
}
.ev-gruppe__kopf:hover, .ev-gruppe__kopf:focus-visible {
color: var(--text);
border-color: color-mix(in srgb, var(--kv-ton, var(--akzent)) 45%, transparent);
}
/* Die erste Gruppe braucht oben keinen Abstand -- darueber steht
schon der Kopf der Spalte. */
.kv-seite__kopf + .ev-gruppe__kopf { margin-top: 2px; }
/* WAS LAEUFT, STEHT KRAEFTIGER DA ALS WAS VORBEI IST. Ein
zugeklappter Kasten „Vorbei — aelter" soll nicht so aussehen wie
„Laeuft gerade": Die Ueberschrift ist zugleich die Auskunft. */
.ev-gruppe__kopf[data-gruppe="laeuft"] {
color: var(--text);
background: color-mix(in srgb, var(--kv-ton, var(--akzent)) 9%, var(--tinte));
}
.ev-gruppe__kopf[data-gruppe^="vorbei"] { opacity: .78; }
.ev-gruppe__wort { flex: 1 1 auto; min-width: 0; }
.ev-gruppe__zahl {
flex: none;
padding: 1px 8px;
font-size: .76rem; font-variant-numeric: tabular-nums;
color: var(--text);
background: color-mix(in srgb, var(--tinte) 70%, #ffffff 30%);
border-radius: 999px;
}
.ev-gruppe__inhalt {
display: flex; flex-direction: column; gap: 12px;
margin-top: 8px;
}
/* ---- Was zu einer Aufgabe noch dasteht ------------------------------ */
/* Beschreibung und Zeitraum einer Missionsgruppe. EINGERUECKT auf die
Hoehe des Aufgabentextes (19 px Kaestchen + 10 px Luecke = 29 px),
damit es unter dem Text haengt und nicht unter dem Kaestchen. */
.ev-punkt__zusatz {
display: flex; flex-wrap: wrap; align-items: baseline;
gap: 4px 9px;
margin: -2px 0 6px 39px;
font-size: .79rem; line-height: 1.45;
color: var(--text-still);
}
.ev-punkt__spanne {
flex: none;
padding: 1px 7px;
border-radius: 999px;
background: color-mix(in srgb, var(--rand) 55%, transparent);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.ev-punkt__worte { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
/* ---- Handy ---------------------------------------------------------- */
@media (max-width: 560px) {
/* Auf dem Handy wird das Polster knapp. Die Einrueckung der
Unterabschnitte halbiert sich, damit der Text nicht in einer
Spalte von acht Zeichen endet. */
.ev-abschnitt[data-stufe="3"] { padding-left: 8px; }
.ev-punkt__zusatz { margin-left: 0; }
/* 44 px Tippziel -- dieselbe Vorgabe wie bei .ev-punkt__knopf. */
.ev-mehr__knopf, .ev-gruppe__kopf { min-height: 44px; }
}
/* ---- Starker Kontrast ----------------------------------------------- */
@media (forced-colors: active) {
/* `color-mix` auf Systemfarben ergibt im Zwangsmodus nichts
Sichtbares -- deshalb echte Raender und Systemfarben. */
.ev-gabe, .ev-hinweise, .ev-mehr__knopf, .ev-gruppe__kopf {
border: 1px solid CanvasText;
}
.ev-gabe__coins { border: 1px solid CanvasText; }
.ev-abschnitt[data-stufe="3"] { border-left: 2px solid CanvasText; }
.ev-abschnitt__titel { color: CanvasText; }
}
/* ---- Weniger Bewegung ----------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
.ev-mehr__knopf, .ev-gruppe__kopf { transition: none; }
}
+394 -25
View File
@@ -375,6 +375,61 @@
}
/* ==== IN WELCHE GRUPPE EINER SPALTE GEHOERT DIESES EVENT? ====
Filipe: „genau wie eine trennung fuer heute, letzte woche
letzten monat und so."
DIE ERSTE FRAGE AN EIN EVENT IST NICHT SEIN ALTER, SONDERN
SEIN ZUSTAND. „Laeuft gerade" und „ist vorbei" entscheiden,
ob man heute etwas tut; ob es vorgestern oder vorletzten
Monat vorbei war, entscheidet nichts mehr. Deshalb zuerst
laeuft / kommt / vorbei.
DAS ALTER TEILT DANN DAS AUF, WAS SICH ANSAMMELT -- und das
ist genau eines: das Vergangene. Von „laeuft gerade" gibt es
nie viele, von „vorbei" mit der Zeit beliebig viele. Genau
dort setzt Filipes Wunsch an, und nur dort traegt er etwas.
GERECHNET WIRD AUF DEM ENDE, nicht auf dem Anlagedatum: Ein
Event, das letzte Woche angelegt wurde und naechsten Monat
endet, ist nicht „letzte Woche".
KALENDERBEZOGEN, NICHT IN TAGEN GEZAEHLT -- dieselbe
Ueberlegung wie bei ZEITSTUFEN weiter unten: „diese Woche"
heisst seit Montag. Am Dienstag ist der Freitag davor nicht
„diese Woche".
EINE LEERE GRUPPE ERSCHEINT NICHT (siehe `continue` beim
Bauen) -- fuenf leere Ueberschriften waeren schlimmer als
eine lange Spalte. */
const EV_GRUPPEN = [
['laeuft', 'Läuft gerade', false],
['kommt', 'Kommt noch', false],
['vorbei_woche', 'Vorbei — diese Woche', true],
['vorbei_monat', 'Vorbei — dieser Monat', true],
['vorbei_alt', 'Vorbei — älter', true],
];
function evGruppeVon(e) {
const st = eventStand(e);
if (!st) return 'vorbei_alt';
if (st.schluessel !== 'vorbei') return st.schluessel;
const heute = window.heuteLokal
? window.heuteLokal() : new Date().toISOString().slice(0, 10);
const tag = new Date(heute + 'T00:00:00');
const ende = new Date(String(e.event_ende || e.datum || '').slice(0, 10) + 'T00:00:00');
if (Number.isNaN(ende.getTime()) || Number.isNaN(tag.getTime())) return 'vorbei_alt';
/* Montag als Wochenanfang -- getDay() liefert Sonntag = 0, und
in Deutschland steht der am Ende. */
const montag = new Date(tag);
montag.setDate(tag.getDate() - ((tag.getDay() + 6) % 7));
if (ende >= montag) return 'vorbei_woche';
const ersterDesMonats = new Date(tag.getFullYear(), tag.getMonth(), 1);
if (ende >= ersterDesMonats) return 'vorbei_monat';
return 'vorbei_alt';
}
/* =====================================================================
WIE LANGE NOCH? (03.10.2026)
@@ -798,6 +853,30 @@
kopf.append(zaehler);
block.append(kopf);
/* WAS ZU EINER AUFGABE NOCH DASTEHT (07.10.2026).
Bei „LIVE Glow Up" hat jede Missionsgruppe eine eigene
Beschreibung (`displayInfo.descText`) und einen eigenen
Zeitraum (`config[0].startTime/endTime`) -- beides stand in
der Kampagnenseite und wurde bis heute nicht gelesen. Es
gehoert UNTER die Aufgabe, nicht in den Regeltext: „Erfuelle
diese Missionen taeglich" ist eine Auskunft ueber DIESE
Aufgabe.
NICHT IN DEN KNOPF. Der Knopf ist die Beschriftung des
Hakens; was darunter steht, hakt man nicht ab. Wer auf die
Beschreibung tippt, soll nichts umschalten. */
const zusatzAnhaengen = (zeile, p) => {
if (!p.beschreibung && !(p.von_tag && p.bis_tag)) return;
const z = el('div', 'ev-punkt__zusatz');
if (p.von_tag && p.bis_tag) {
z.append(el('span', 'ev-punkt__spanne',
datum(p.von_tag) + ' – ' + datum(p.bis_tag)));
}
if (p.beschreibung) z.append(el('span', 'ev-punkt__worte', p.beschreibung));
zeile.append(z);
};
const liste = el('ul', 'ev-liste');
for (const p of punkte) {
const zeile = el('li', 'ev-punkt');
@@ -807,6 +886,7 @@
zeile.dataset.abgehakt = p.meiner ? 'ja' : '';
zeile.append(el('span', 'ev-punkt__kaestchen'));
zeile.append(el('span', 'ev-punkt__text', p.text));
zusatzAnhaengen(zeile, p);
liste.append(zeile);
continue;
}
@@ -851,6 +931,7 @@
} finally { knopf.disabled = false; }
});
zeile.append(knopf);
zusatzAnhaengen(zeile, p);
liste.append(zeile);
}
block.append(liste);
@@ -873,30 +954,229 @@
return block;
}
/** Die Regeln -- dieselbe Liste, aber ohne Haken.
/* =====================================================================
DIE INFOS EINER KACHEL -- GEGLIEDERT STATT FLACH (07.10.2026)
Filipe, mit einem Bildschirmfoto des Regelblocks: „man soll die
titel in den texten besser erkennen und sehen was zu was passt.
ich will auch dass man sieht welche geschenke bei dem event
zaehlen und so. ... die kacheln von den events soll auch nicht
so lang sein, die soll man aufklappen koennen."
HIER STAND EINE EINZIGE FLACHE LISTE. Jede Zeile derselbe
Strich, gleiche Groesse, gleiche Farbe -- „INFOS ZUR KAMPAGNE"
sah aus wie der Absatz darunter, und die Hinweise („Diese
Kampagne hat keine Preisliste") standen zwischen den Regeln,
als waeren sie welche.
DIE GLIEDERUNG KOMMT VOM SERVER (`event_regelabschnitte`, aus
`gliederung()` in kampagne-lesen.mjs). Hier wird nur gemalt --
und zwar genau einmal im Haus: Die Teilen-Seite benutzt
dieselbe Zerlegung. Haetten Kachel und Teilen-Seite je ihre
eigene, wuerden sie irgendwann Verschiedenes zeigen, und
niemand koennte sagen, welche von beiden recht hat.
DREI SORTEN INHALT, DREI ORTE:
Geschenke immer sichtbar, als Plaketten mit Coinpreis --
das ist die Frage, die am haeufigsten gestellt
wird, und sie passt in eine Zeile.
Hinweise immer sichtbar, aber still und abgesetzt. „Die
Preise konnten nicht gelesen werden -- bitte
nachtragen" ist eine Aufgabe; sie hinter einen
Druck zu legen heisst, sie zu verstecken.
Alles Uebrige hinter EINEM Druck. Bei „Gipfelstuermer" sind
das rund 7000 Zeichen -- sechs Preisgruppen, sechs
Ligen, Teilnahmebedingungen. Das ist die Laenge,
die Filipe gemeint hat.
===================================================================== */
/** Eine Coinzahl, wie man sie liest: 12000 -> 12.000 Coins.
*
* 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');
* `Number(null)` IST 0, UND 0 IST ENDLICH -- derselbe Fehler
* wie in `gliederung()`, und er ist mir am 07.10.2026 an EINEM
* Tag ZWEIMAL passiert: einmal auf dem Server, einmal hier. Der
* Server lieferte danach brav `coins: null`, und die Kachel
* schrieb „0 Coins" an das Feuerwerk -- also eine Preisangabe,
* und zwar eine falsche.
*
* Gefunden hat es nicht die Serverpruefung (die war gruen),
* sondern die Gegenprobe im Browser. Zwei Stellen, zwei
* Messungen -- eine Pruefung auf der einen Seite beweist nichts
* ueber die andere. */
function coinText(n) {
if (n === null || n === undefined || n === '') return null;
const z = Number(n);
if (!Number.isFinite(z)) return null;
/* „1 Coins" ist der Schoenheitsfehler, der wie ein Fehler im
Einlesen aussieht -- und das Erste, was jemand meldet. */
return z.toLocaleString('de-DE') + (z === 1 ? ' Coin' : ' Coins');
}
/** Ein Geschenk als Plakette: Name, Coinpreis, Hinweis. */
function evGabe(z) {
const li = el('li', 'ev-gabe');
li.append(el('span', 'ev-gabe__name', z.text));
const c = coinText(z.coins);
/* KEIN PREIS IST NICHT NULL COINS. „Feuerwerk" bei LIVE Glow Up
ist ein Kampagnengeschenk ohne Coinpreis; „0 Coins" daneben
waere eine Preisangabe, und zwar eine falsche. */
if (c) li.append(el('span', 'ev-gabe__coins', c));
if (z.zusatz) li.append(el('span', 'ev-gabe__zusatz', z.zusatz));
return li;
}
/** Eine Zeile eines Abschnitts -- Absatz, Frage, Punkt, Geschenk. */
function evZeile(z) {
if (z.art === 'geschenk') return evGabe(z);
if (z.art === 'punkt') {
const li = el('li', 'ev-zeile');
li.append(el('span', 'ev-zeile__zeichen'));
li.append(el('span', 'ev-zeile__text', z.text));
return li;
}
if (z.art === 'hinweis') return el('p', 'ev-hinweis', z.text);
if (z.art === 'frage') return el('p', 'ev-frage', z.text);
return el('p', 'ev-absatz', z.text);
}
/** Zeilen in ihrer REIHENFOLGE anhaengen.
*
* Aufeinanderfolgende Punkte kommen in EINE Liste, ein Absatz
* dazwischen beendet sie. Erst alle Absaetze und dann alle Punkte
* zu malen waere einfacher und falsch: Bei den
* Teilnahmebedingungen steht zwischen zwei Aufzaehlungen ein
* Satz, der sich auf die ZWEITE bezieht -- umsortiert bezieht er
* sich auf nichts. */
function evZeilenAnhaengen(ziel, zeilen) {
let liste = null;
for (const z of zeilen || []) {
const k = evZeile(z);
if (k.tagName === 'LI') {
if (!liste) {
liste = el('ul', 'ev-zeilen');
if (z.art === 'geschenk') liste.classList.add('ev-gaben');
ziel.append(liste);
}
liste.append(k);
continue;
}
liste = null;
ziel.append(k);
}
}
/** Ein Abschnitt mit seiner Ueberschrift. */
function evAbschnitt(a) {
const s = el('section', 'ev-abschnitt');
s.dataset.stufe = String(a.stufe || 2);
if (a.art) s.dataset.art = a.art;
if (a.titel) s.append(el('h5', 'ev-abschnitt__titel', a.titel));
if (a.unter) s.append(el('p', 'ev-abschnitt__unter', a.unter));
evZeilenAnhaengen(s, a.zeilen);
return s;
}
/** Die Geschenke, die zaehlen -- der eigene Block oben auf der Kachel. */
function evGabenBlock(a) {
const gaben = (a.zeilen || []).filter((z) => z.art === 'geschenk');
if (!gaben.length) return null;
const block = el('section', 'ev-block ev-block--gaben');
const kopf = el('div', 'ev-block__kopf');
kopf.append(el('h4', 'ev-block__schild', 'Regeln'));
kopf.append(el('h4', 'ev-block__schild', 'Diese Geschenke zählen'));
kopf.append(el('span', 'ev-zaehler', String(gaben.length)));
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);
/* DIE UEBERSCHRIFT DER KAMPAGNE BLEIBT STEHEN. „Sammle dein
Feuerwerk -- Erhalte doppelte Punkte fuer die Rangliste" sagt,
WARUM diese Geschenke zaehlen. Ein selbst erfundenes
„Geschenke" daneben haette diese Auskunft verschluckt. */
if (a.titel) {
const woher = el('p', 'ev-gaben__woher', a.titel);
if (a.unter) woher.append(el('span', 'ev-gaben__unter', a.unter));
block.append(woher);
}
const liste = el('ul', 'ev-zeilen ev-gaben');
for (const z of gaben) liste.append(evGabe(z));
block.append(liste);
/* Was im Geschenkabschnitt steht und KEIN Geschenk ist, geht
nicht verloren -- sonst haengt die Vollstaendigkeit der
Kachel daran, dass TikTok dort nie etwas anderes hinschreibt. */
evZeilenAnhaengen(block, (a.zeilen || []).filter((z) => z.art !== 'geschenk'));
return block;
}
/** Was beim Einlesen gefehlt hat -- still, aber sichtbar. */
function evHinweisBlock(zeilen) {
if (!zeilen.length) return null;
const kasten = el('aside', 'ev-hinweise');
for (const z of zeilen) kasten.append(el('p', 'ev-hinweis', z.text));
return kasten;
}
/** Der Steckbrief: die harten Tatsachen aus der Kampagnenseite. */
const STECK_NAME = {
nummer: 'Kampagnennummer', kurznummer: 'Kurznummer',
region: 'Markt', sprache: 'Sprache', verbund: 'Verbund',
zeitzone: 'Zeitzone',
anmeldung_ab: 'Anmeldung ab', ergebnis_bis: 'Ergebnisse bis',
};
const STECK_DATUM = new Set(['anmeldung_ab', 'ergebnis_bis']);
function evSteckbrief(e) {
const s = e.event_steckbrief;
if (!s || typeof s !== 'object') return null;
/* UEBER STECK_NAME GELAUFEN, nicht ueber die Felder des Servers:
So bestimmt die Reihenfolge dieser Liste die Reihenfolge auf
dem Bildschirm, und ein neues Feld erscheint erst, wenn es
hier auch einen deutschen Namen hat. Sonst stuende irgendwann
„activity_kind_flag: 1" auf der Kachel. */
const paare = Object.keys(STECK_NAME)
.filter((f) => s[f] !== null && s[f] !== undefined && s[f] !== '')
.map((f) => [STECK_NAME[f], STECK_DATUM.has(f) ? datum(s[f]) : String(s[f])]);
if (!paare.length) return null;
const block = el('section', 'ev-block ev-block--steck');
const kopf = el('div', 'ev-block__kopf');
kopf.append(el('h4', 'ev-block__schild', 'Steckbrief'));
block.append(kopf);
const liste = el('dl', 'ev-steck');
for (const [name, wert] of paare) {
liste.append(el('dt', 'ev-steck__name', name));
liste.append(el('dd', 'ev-steck__wert', wert));
}
block.append(liste);
return block;
}
/** Die Abschnitte des Regeltextes -- Hinweise und Geschenke heraus. */
function evInfos(e) {
const alle = Array.isArray(e.event_regelabschnitte) ? e.event_regelabschnitte : null;
if (!alle) return null;
const hinweise = [];
let gaben = null;
const rest = [];
for (const a of alle) {
/* Der Block GANZ OBEN ohne Ueberschrift sind die Hinweise des
Lesers. Ein Hinweis MITTEN in einem Abschnitt bleibt dort --
er bezieht sich dann auf diesen Abschnitt. */
if (a.art === 'hinweise' && !a.titel) { hinweise.push(...(a.zeilen || [])); continue; }
if (!gaben && a.art === 'geschenke') { gaben = a; continue; }
rest.push(a);
}
return { hinweise, gaben, rest };
}
/** Alles Uebrige in einem Block. */
function evRegelBlock(abschnitte) {
if (!abschnitte.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 & Infos'));
kopf.append(el('span', 'ev-zaehler', String(abschnitte.length)));
block.append(kopf);
const kasten = el('div', 'ev-abschnitte');
for (const a of abschnitte) kasten.append(evAbschnitt(a));
block.append(kasten);
return block;
}
/** Der dritte Ausgang fuer die Event-Bloecke.
*
* Kommt vom Server keine Punkteliste (alte Fassung im Netz, Feld
@@ -1340,8 +1620,27 @@
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);
/* DER DRITTE AUSGANG (CLAUDE.md, 03.09.2026): in Ordnung /
nicht in Ordnung / konnte nicht nachsehen.
`evInfos` gibt `null`, wenn der Server keine Gliederung
geschickt hat -- alte Serverfassung, leeres Feld, Abfrage
gescheitert. Dann steht der ROHTEXT da (`event_regeln`
kommt weiterhin mit). Eine Kachel ohne Regeln waere der
schlechteste von drei Ausgaengen; eine schlichte Textwand
ist ein Schoenheitsfehler.
GLIEDERUNG DA, ABER LEER ist etwas anderes als „keine
Gliederung": Dann hat das Event wirklich keine Regeln, und
dann soll auch nichts dastehen. */
const infos = evInfos(e);
const gaben = infos?.gaben ? evGabenBlock(infos.gaben) : null;
const hinweise = infos ? evHinweisBlock(infos.hinweise) : null;
const reg = infos
? evRegelBlock(infos.rest)
: (e.event_regeln ? eventRueckfall('Regeln', e.event_regeln) : null);
const steck = evSteckbrief(e);
/* BEIDE IN EINE REIHE, nicht untereinander an die Karte.
Auf 1280 px ist eine Karte rund 1160 px breit. "30
@@ -1353,12 +1652,57 @@
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) {
/* IMMER SICHTBAR: was man tun soll, und was dabei zaehlt. */
if (auf || gaben) {
const reihe = el('div', 'ev-bloecke');
if (auf) reihe.append(auf);
if (reg) reihe.append(reg);
if (gaben) reihe.append(gaben);
k.append(reihe);
}
/* HINTER EINEM DRUCK: alles Uebrige (07.10.2026).
Filipe: „die kacheln von den events soll auch nicht so
lang sein, die soll man aufklappen koennen damit die nicht
zu lange sind." Gemessen an „Gipfelstuermer" sind das rund
7000 Zeichen -- sechs Preisgruppen, sechs Ligen,
Teilnahmebedingungen, Themenideen.
DERSELBE KLAPPMECHANISMUS WIE IM GANZEN HAUS
(`abschnittKlappbar` aus kopf.js). Ein eigener waere ein
zweiter, der sich anders verhaelt -- und er merkt sich die
eigene Wahl, was hier wichtig ist: Das Brett baut sich bei
jeder Aktualisierung neu auf, und eine Kachel, die dabei
wieder zuklappt, waehrend man liest, ist aergerlicher als
eine lange.
ZU ALS VORGABE. Das ist der Zweck der Uebung. Die Zahl im
Knopf sagt, was dahinter steht -- ein zugeklappter Kasten
ohne Zahl sieht aus, als sei er leer. */
if (reg || steck) {
const mehr = el('div', 'ev-mehr');
const knopf = el('button', 'ev-mehr__knopf');
knopf.type = 'button';
knopf.append(el('span', 'ev-mehr__wort', 'Alle Infos'));
const n = (infos?.rest.length || 0) + (steck ? 1 : 0);
knopf.append(el('span', 'ev-mehr__zahl',
n === 1 ? '1 Abschnitt' : n + ' Abschnitte'));
if (window.klappPfeil) knopf.append(window.klappPfeil());
const inhalt = el('div', 'ev-mehr__inhalt');
if (reg) inhalt.append(reg);
if (steck) inhalt.append(steck);
mehr.append(knopf);
mehr.append(inhalt);
k.append(mehr);
window.abschnittKlappbar?.(knopf, inhalt, 'event:' + e.id + ':infos', true);
}
/* DIE HINWEISE ZULETZT UND SICHTBAR. „Die Preise konnten
nicht gelesen werden -- bitte nachtragen" ist eine
Aufgabe; hinter dem Aufklappen waere sie verborgen. Sie
stehen unter den Bloecken, nicht zwischen den Regeln --
genau das war der Fehler im Bildschirmfoto. */
if (hinweise) k.append(hinweise);
}
/* ---- STEHT IM KALENDER (17.09.2026) ------------------------------
@@ -3353,11 +3697,36 @@
? 'Noch keine Einladung eingelesen.'
: 'Noch kein eigenes Event angelegt.'));
}
for (const e of drin) {
const k = karte(e);
k.dataset.quelle = seite;
karten.set(e.id, k);
spalte.append(k);
/* NACH ZUSTAND UND ALTER GEGLIEDERT (07.10.2026) --
Begruendung bei EV_GRUPPEN. „Laeuft gerade" steht
offen, Vergangenes zu: Es ist Nachschlagewerk, nicht
das, weswegen jemand die Seite oeffnet.
DER MERKER TRAEGT DIE SEITE MIT. Ohne ihn teilten sich
„Laeuft gerade" links und rechts denselben Zustand,
und wer die eine zuklappt, klappt die andere mit --
derselbe Fehler, der bei den Zeitstufen schon einmal
behoben wurde. */
for (const [schluessel, name, zuVorgabe] of EV_GRUPPEN) {
const fach = drin.filter((e) => evGruppeVon(e) === schluessel);
if (!fach.length) continue;
const gkopf = el('button', 'ev-gruppe__kopf');
gkopf.type = 'button';
gkopf.dataset.gruppe = schluessel;
gkopf.append(el('span', 'ev-gruppe__wort', name));
gkopf.append(el('span', 'ev-gruppe__zahl', String(fach.length)));
if (window.klappPfeil) gkopf.append(window.klappPfeil());
spalte.append(gkopf);
const kasten = el('div', 'ev-gruppe__inhalt');
for (const e of fach) {
const k = karte(e);
k.dataset.quelle = seite;
karten.set(e.id, k);
kasten.append(k);
}
spalte.append(kasten);
window.abschnittKlappbar?.(gkopf, kasten,
'agentur:' + seite + ':' + schluessel, zuVorgabe);
}
trennung.append(spalte);
}