Musterseite verworfen, Zahnrad-Regression behoben, Fotomenue als Aktionsblatt

Musterseite entfernt (companion-app/muster, vite.muster.config.ts,
/config/www/dmmuster). Ansage des Eigentuemers: sie fuehrt nur zu neuen
Fehlerquellen. Ausloeser war ein Befund, den ich als App-Fehler gemeldet hatte
- Ringe und Typenschild schwarz auf schwarz - und der nur dort bestand.

Behoben:

* Das Zahnrad im Autohaus-Kasten hatte seine Ecke verlassen (58/288 statt
  8/8). Ursache war mein HIG-Durchgang von gestern: .zahnrad kam in die
  Gruppe fuer die 44x44-Trefferflaeche und bekam damit position:relative,
  was das absolute aus dem Basisblatt ueberschrieb. Die Auflage war
  ueberfluessig - .zahnrad ist dort schon 44x44.
* Das Fotomenue hing mit position:absolute an der Kachel statt am
  Bildschirm und lag bei der hohen Raeder-Kachel ausserhalb des Sichtfelds;
  einen Abbrechen-Knopf hatte es nie. Das Panel benutzt jetzt dasselbe
  Aktionsblatt wie die App. .bildmenu ist samt Zustand, Markup, totem
  Handler und CSS entfernt.
* Der Blattkopf las sich als dritte Auswahl (linksbuendig, 15px, --fg).
  Jetzt zentriert, 13px, gedaempft - Apples Muster, in beiden Codebasen.
* "Anstehende Termine" stand 5px ueber "Oelwechsel" und wirkte als dessen
  Beischrift. Entfernt.
* Wartungsplan-Formular: Art nach oben, Datum 124px statt Browser-Vorgabe,
  Werkstatt ueber die volle Zeile (218 -> 306px), Kosten 116px. Zwei
  Erklaertexte entfernt.
* Thema folgt jetzt einer Systemumstellung im laufenden Betrieb. Vorher las
  es prefers-color-scheme genau einmal beim Laden. Drei Tests, die ohne die
  Behebung nachweislich fallen.

Geprueft: 295 App-Tests, Panel node --check, 0 Tracebacks, alle Aenderungen
live an der Testinstanz gemessen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 02:24:08 +02:00
parent 863284e540
commit e9c16325cc
16 changed files with 2674 additions and 2467 deletions
+147
View File
@@ -9952,6 +9952,17 @@ strukturell, nicht optisch belegt.
## CG. Eine Musterseite loest den wiederkehrenden Pruefblocker (2026.9.4.12)
> **Zurueckgenommen am 05.09.2026.** Die Musterseite ist entfernt -
> `companion-app/muster/`, `vite.muster.config.ts`, `dist-muster/` und
> `/config/www/dmmuster` im Testcontainer. Ansage des Eigentuemers: *"der
> fehler ist nur in deiner musterseite. Verwerfe diese - die fuehrt nur zu
> neuen Fehlerquellen"*. Ausloeser war ein Befund, den ich als App-Fehler
> gemeldet habe (Vier Ringe und Typenschild schwarz auf schwarz im
> Nachtmodus) und der nur auf der Musterseite bestand - genau die Klasse
> Fehlalarm, die eine zweite Bauweise derselben Oberflaeche erzeugt. Fuer
> Live-Pruefungen gilt jetzt allein der Weg ueber den Zugangstoken
> (Abschnitt CP). Der folgende Text bleibt als Aufzeichnung stehen.
**Das Problem, das seit Wochen wiederkehrt:** die App zeigt ohne gespeicherten
Zugang gar nichts, jede OPTISCHE Pruefung haengt damit an einem Zugangstoken -
und fast jeder Fehler, den dieses Projekt findet, ist nur am gerenderten Bild
@@ -11451,3 +11462,139 @@ sie war es nicht:
Beides hat dazu gefuehrt, dass ich zwischenzeitlich alte Dateien gelesen und
fuer neue gehalten habe. Gegenprobe, die das aufdeckt: `find <ziel> -name
manifest.json | wc -l` muss `1` ergeben.
## CS. Fuenf Meldungen aus einer Sitzung - und ein Fehlalarm, den ich selbst gebaut hatte (2026.9.5.10)
### Der Fehlalarm zuerst: die Musterseite ist weg
Gemeldet als App-Fehler: "RS4 Logo ist im Nachtmodus schwarz und nicht mehr
lesbar, Audi Ringe rechts oben sind schwarz auf schwarz". Ich habe daraufhin
eine Ursache benannt, die keine war - `fill: rgb(0,0,0)` aus meiner Messung ist
die geerbte CSS-Eigenschaft des `<img>`-Elements und sagt ueber das gezeichnete
SVG nichts. Die Gegenprobe im Dunkelmodus zeigte weisse Ringe.
Der Befund stimmte, aber nur auf der **Musterseite**. Ansage des Eigentuemers:
*"der fehler ist nur in deiner musterseite. Verwerfe diese - die fuehrt nur zu
neuen Fehlerquellen"*. Sie ist entfernt (Abschnitt CG traegt jetzt den
Ruecknahme-Vermerk). Eine zweite Bauweise derselben Oberflaeche erzeugt
Fehlalarme - genau das ist hier passiert.
**Ein echter Mangel blieb aus der Suche uebrig**: `theme.ts` und das Panel
lasen `prefers-color-scheme` genau einmal beim Laden und horchten danach nie
wieder. Stellt das Geraet im Betrieb um - automatische Umschaltung bei
Sonnenuntergang -, blieb die Oberflaeche bis zum naechsten Neuladen stehen.
Beide horchen jetzt; eine selbst getroffene Wahl bleibt unangetastet. Drei
Tests, die ohne die Behebung nachweislich fallen (gegengeprueft).
### Das Zahnrad im Autohaus-Kasten - meiner
Gemeldet: *"Das Zahnrad vom Autohaus hat seine Position (rechts oben in der
Kachel) verlassen. Warum"*. Gemessen: 58px von oben, 288px von rechts statt
8/8.
Ursache war mein eigener HIG-Durchgang vom 04.09.2026 (Commit b0ec8b4). Ich
hatte `.zahnrad` in die Gruppe fuer die 44x44-Trefferflaeche aufgenommen:
#back, .ringsbtn, .icon-btn, .zahnrad { position: relative; }
`audi-dashboard-ios.css` laedt nach `audi-dashboard.css`, gleiche Spezifitaet -
das `position: absolute`, das das Zahnrad in die Kachelecke heftet, war damit
ueberschrieben. Die Auflage war ausserdem ueberfluessig: `.zahnrad` ist im
Basisblatt schon 44x44. Ich hatte die Gruppe zusammengestellt, ohne zu pruefen,
welche Knoepfe ihre Flaeche laengst haben. `.zahnrad` ist wieder heraus, die
drei anderen sind nicht absolut positioniert und bleiben.
### Das Fotomenue: an der Kachel statt am Bildschirm, ohne Ausweg
Gemeldet: *"Der Popup zum ersetzen erscheint ausserhalb vom Sichtfeld... es
fehlt ein abbrechen button wenn man zufaellig auf das rad klickt."*
`.bildmenu` war `position: absolute` und hing damit an der **Kachel**, nicht am
Bildschirm. Bei der hohen Raeder-Kachel lag es unterhalb des Sichtfelds. Einen
Abbrechen-Knopf hatte es nie.
Die App hatte den Fall schon geloest - `<ActionSheet>` mit abgesetztem
Abbrechen. Das Panel zieht jetzt nach: `aktionsblatt(titel, aktionen)` neben
`bestaetigen()`/`hinweis()`, gerendert am Wurzelelement. `.bildmenu` samt
`bildMenuOffen`, den beiden Markup-Bloecken, dem toten
`data-bildloeschen`-Handler und allen CSS-Regeln in beiden Blaettern ist
entfernt. `.bildmenu-catcher` bleibt - den benutzt das SmartDeal-Blatt.
Gemessen am laufenden Panel: Blatt bei 553-760px in einem 794px-Fenster, drei
Knoepfe "Foto ersetzen / Foto loeschen / Abbrechen".
### Der Blattkopf las sich als dritte Auswahl
Gemeldet: *"Die Ueberschrift des Menues Sommerrad ersetzen/loeschen lautet
'Sommerrad'... Auf mich wirkt es wie eine dritte Auswahl"*. Zu Recht: er stand
linksbuendig, 15px, in `--fg` - fast genau wie die Knopfzeilen darunter. Apples
Aktionsblatt setzt den Titel **zentriert, kleiner (13px) und gedaempft**
(`--fg2`). Beide Codebasen nachgezogen.
### "Anstehende Termine" gehoerte scheinbar zum Oelwechsel
Gemeldet: *"Aktuell wirkt es als wuerde es zu Oelwechsel gehoeren"*. Gemessen:
13px, Gewicht 400, `--fg3`, keine Grossschrift - und nur **5px** ueber
"Oelwechsel" (16px, `--fg`). Der Kopf ist entfernt: die drei Zeilen benennen
sich selbst, die Seite heisst bereits "Service".
**Offen und nicht angefasst:** dieselbe schwache Behandlung hat die
Kachelueberschrift ueberall im Panel (`.label`, iOS-Auflage: 13px, `--fg3`,
keine Grossschrift). Die App setzt an derselben Stelle 10px in Grossschrift mit
0.2em Sperrung - eine erkennbare Augenbraue. Das ist eine Abweichung ueber
Dutzende Kacheln und gehoert nicht in einen Nebensatz.
### Wartungsplan-Formular
Vorgabe: Art nach oben, Feldbreiten nach Inhalt, zwei Erklaertexte weg.
Gemessen vorher/nachher:
| Feld | vorher | jetzt |
|---|---|---|
| Art | 3. Zeile | 1. Zeile |
| Datum | ~170-190px (Browser-Vorgabe) | 124px Panel / 150px App |
| Werkstatt | 218px | 306px, eigene Zeile |
| Kosten | 96px | 116px |
Die Browser-Vorgabe ist inhaltsblind: **jedes** Textfeld ohne `size` bekommt
dieselbe Breite. Deshalb schwamm das Datum und wurde der Werkstattname
abgeschnitten.
Der Werkstattname passt in keine Spalte neben einem Label: ein echter Name
("Audi AG Kundendienst Center Ingolstadt") braucht 322px, auf 375px Geraetebreite
bleiben neben dem Label 218. Er steht deshalb in einer `.feld--breit`-Zeile.
**Auch das reicht fuer diesen Extremfall nicht ganz** - 306 gegen 322px, das
Feld scrollt dann. Weiter kaeme man nur mit kleinerer Schrift, und unter 16px
zoomt iOS beim Antippen die ganze Seite.
Die App hat ein natives `type="date"` mit Kalendersymbol, das Panel ein
Textfeld - daher 150 gegen 124px. Das ist ein Bauunterschied, kein Versehen.
### Raederzaehler: er rechnet, ihm fehlt nur Futter
Frage: *"Funktioniert der Raederzaehler ueberhaupt? Ich sehe hier wenig
Bewegung bei den Fahrten."* Gemessen an der Testinstanz:
| | Anzahl |
|---|---|
| Fahrten gesamt | 19 |
| mit `reifensatz` | 4 |
| davon mit gemessener Strecke | 2 |
| noch nicht gutgeschrieben | 0 |
Die vier gestempelten Fahrten summieren 9,9 km, und **genau 9,9 km** stehen auf
den Sommerraedern. Der Zaehler arbeitet also fehlerfrei. Wenig Bewegung hat
drei Gruende, alle beabsichtigt oder bekannt:
* Importierte Fahrten (6) werden **bewusst nicht** gestempelt - ihre Kilometer
stecken schon im gespeicherten Radstand, ein erneuter Import wuerde sie sonst
ein zweites Mal aufschlagen (Kommentar in `historienimport.py`).
* Sieben `ha`-Fahrten stammen aus der Zeit vor dem Stempel und tragen ihn nicht
nach.
* Zwei der vier gestempelten Fahrten haben noch keine gemessene Strecke; sie
werden gutgeschrieben, sobald das Screening sie fuellt.
Der eigentliche Grund auf der **realen** Instanz ist ein anderer und heute
zuerst gefunden worden: dort wurden ueberhaupt keine Fahrten erkannt, weil die
Erkennung still das Trip-Signal des Dongles bevorzugte, das nie ankam. Ohne
Fahrten kein Radstand.
-2
View File
@@ -1,7 +1,5 @@
node_modules/
dist/
# Bau der Musterseite (vite.muster.config.ts) - reines Ergebnis, nie in der .ipa.
dist-muster/
*.local
.DS_Store
-12
View File
@@ -1,12 +0,0 @@
<!doctype html>
<html lang="de" data-theme="nacht">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>DataMetric360 Musterseite</title>
</head>
<body>
<div id="wurzel" class="ads-root"></div>
<script type="module" src="./muster.tsx"></script>
</body>
</html>
-79
View File
@@ -1,79 +0,0 @@
/**
* Musterseite: die Oberflaeche der App mit Beispieldaten, ohne Server.
*
* WOFUER
* ------
* Die App braucht sonst einen gespeicherten Zugang, um ueberhaupt etwas
* anzuzeigen - und damit war jede OPTISCHE Pruefung an ein Zugangstoken
* gebunden. Genau daran ist in diesem Projekt wiederholt die Verifikation
* haengengeblieben, obwohl fast jeder gefundene Fehler (CSS-Spezifitaet,
* Flex-Basis in Spalten, unsichtbare Rahmen) nur am gerenderten Bild
* sichtbar war und von tsc und den Tests nicht gefunden werden konnte.
*
* Diese Seite mountet dieselben Bildschirme mit demselben Stylesheet, aber
* mit `beispielApi()` aus den Tests. Kein Netz, kein Token, keine echten
* Daten - und trotzdem echtes CSS.
*
* NICHT TEIL DER APP
* ------------------
* Gebaut wird sie ueber `vite.muster.config.ts` nach `dist-muster/`. Der
* normale `npm run build` fasst sie nicht an, sie liegt also auch nicht in
* der .ipa.
*
* Aufruf: `?seite=einst` (Vorgabe), jeder Name aus navigation.ts TITEL.
* Dazu `?seite=einrichtung` fuer den Willkommensbildschirm - der ist keine
* Route (er steht VOR der Anmeldung) und braucht deshalb einen eigenen Zweig.
*/
import { StrictMode, useState } from "react"
import { createRoot } from "react-dom/client"
import "@audi-dash/ui/styles.css"
import "../src/stile/grundlage.css"
import "../src/stile/audi-schrift.css"
import "../src/stile/rahmen.css"
import "../src/stile/screens.css"
import { DatenAnbieter } from "../src/daten/DatenKontext"
import { Shell } from "../src/Shell"
import type { SeitenName } from "../src/navigation"
import { TITEL } from "../src/navigation"
import { Bestaetigungsblatt } from "../src/screens/bestaetigung"
import { Einrichtung } from "../src/screens/Einrichtung"
import { SeiteFuer } from "../src/screens/register"
import { beispielApi } from "../src/tests/beispieldaten"
function Muster() {
const gewaehlt = new URLSearchParams(location.search).get("seite") || "einst"
const start = gewaehlt as SeitenName
const [route, setzeRoute] = useState<{ name: SeitenName; id?: string }>({ name: start })
const geheZu = (name: SeitenName, id?: string) =>
setzeRoute(id === undefined ? { name } : { name, id })
// Der Willkommensbildschirm haengt an keinem Zugang und an keiner Route.
if (gewaehlt === "einrichtung") return <Einrichtung fertig={() => {}} />
return (
<DatenAnbieter api={beispielApi()}>
<Bestaetigungsblatt />
<Shell route={route} geheZu={geheZu} tabBeschriftung>
<SeiteFuer
route={route}
geheZu={geheZu}
tabBeschriftung
setzeTabBeschriftung={() => {}}
beiAbmeldung={() => {}}
/>
</Shell>
</DatenAnbieter>
)
}
// Zum schnellen Springen zwischen den Seiten, ohne die Adresse zu tippen.
;(window as unknown as { SEITEN: string[] }).SEITEN = Object.keys(TITEL)
const wurzel = document.getElementById("wurzel")
if (!wurzel) throw new Error("Wurzelelement #wurzel fehlt")
createRoot(wurzel).render(
<StrictMode>
<Muster />
</StrictMode>,
)
+19 -21
View File
@@ -130,7 +130,6 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
return (
<>
<Tile>
<span className="ads-eyebrow">Anstehende Termine</span>
<div className="dm-termingruppe">
<div className="dm-tgkopf">
@@ -504,17 +503,6 @@ export function Servicebuch({
<Tile>
<span className="ads-eyebrow">Eintrag</span>
<Feld label="Datum">
<Datumsfeld wert={entwurf.datum ?? ""} beiAenderung={(w) => setze("datum", w)} />
</Feld>
<Feld label="Kilometerstand" unit="km">
<input
className="dm-eingabe"
inputMode="numeric"
value={entwurf.km ?? ""}
onChange={(e) => setze("km", Number(e.target.value))}
/>
</Feld>
<Feld label="Art">
<select
className="dm-auswahl"
@@ -531,25 +519,38 @@ export function Servicebuch({
))}
</select>
</Feld>
<Feld label="Werkstatt">
<Feld label="Datum">
<Datumsfeld wert={entwurf.datum ?? ""} beiAenderung={(w) => setze("datum", w)} />
</Feld>
<Feld label="Kilometerstand" unit="km">
<input
className="dm-eingabe"
inputMode="numeric"
value={entwurf.km ?? ""}
onChange={(e) => setze("km", Number(e.target.value))}
/>
</Feld>
{/* Label oben, Feld über die ganze Zeile - wie der Reifen-Kommentar
und .feld--breit im Panel. Ein echter Werkstattname braucht 322px;
neben einem Label bleiben auf 375px nur 218 (gemessen 05.09.2026). */}
<div className="ads-feld" style={{ display: "block" }}>
<label style={{ display: "block", marginBottom: 8 }}>Werkstatt</label>
<input
className="dm-eingabe"
style={{ width: "100%", textAlign: "left" }}
value={entwurf.werkstatt ?? ""}
onChange={(e) => setze("werkstatt", e.target.value)}
/>
</Feld>
</div>
<Feld label="Kosten" unit="€" last>
<input
className="dm-eingabe"
className="dm-eingabe dm-eingabe--betrag"
inputMode="decimal"
placeholder="noch offen"
value={entwurf.kosten ?? ""}
onChange={(e) => setze("kosten", e.target.value === "" ? null : Number(e.target.value))}
/>
</Feld>
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
Kosten fließen in die Jahresübersicht ein
</span>
</Tile>
<Tile>
@@ -561,9 +562,6 @@ export function Servicebuch({
value={entwurf.arbeiten ?? ""}
onChange={(e) => setze("arbeiten", e.target.value)}
/>
<span className="ads-eyebrow" style={{ marginTop: "var(--sp-3)" }}>
Beim Verkauf ist diese Liste mehr wert als die Rechnung sie zeigt, dass gepflegt wurde
</span>
</Tile>
<ActionButton variant="primary" onClick={() => void speichern()} disabled={laeuft}>
File diff suppressed because it is too large Load Diff
+57
View File
@@ -38,3 +38,60 @@ describe("Theme", () => {
})
})
/* Stellt das Gerät im laufenden Betrieb um - etwa durch die automatische
Umschaltung bei Sonnenuntergang -, muss das Thema folgen. Bis zum
05.09.2026 las theme.ts die Systemeinstellung genau einmal beim Laden des
Moduls und horchte danach nie wieder; ein Wechsel wirkte erst nach dem
nächsten Neuladen. */
describe("Systemumstellung im laufenden Betrieb", () => {
function medienabfrageStellen(dunkel: boolean) {
const horcher: (() => void)[] = []
const stand = { dunkel }
// matches als Getter, nicht als kopierter Wert: theme.ts haelt die
// Abfrage vom Laden fest und liest matches erst im Horcher wieder.
vi.stubGlobal("matchMedia", (text: string) => ({
get matches() {
return text.includes("dark") ? stand.dunkel : !stand.dunkel
},
addEventListener: (_art: string, bei: () => void) => horcher.push(bei),
removeEventListener: () => {},
}))
return {
umstellenAuf(neuDunkel: boolean) {
stand.dunkel = neuDunkel
for (const bei of horcher) bei()
},
}
}
beforeEach(() => {
localStorage.clear()
vi.resetModules()
})
it("folgt dem Wechsel nach hell, ohne neu zu laden", async () => {
const system = medienabfrageStellen(true)
const { themeStartwert } = await import("./theme")
expect(themeStartwert()).toBe("nacht")
system.umstellenAuf(false)
expect(themeStartwert()).toBe("tag")
})
it("folgt dem Wechsel nach dunkel, ohne neu zu laden", async () => {
const system = medienabfrageStellen(false)
const { themeStartwert } = await import("./theme")
expect(themeStartwert()).toBe("tag")
system.umstellenAuf(true)
expect(themeStartwert()).toBe("nacht")
})
it("lässt eine selbst getroffene Wahl unangetastet", async () => {
localStorage.setItem("dm360.theme", "tag")
const system = medienabfrageStellen(false)
const { themeStartwert } = await import("./theme")
expect(themeStartwert()).toBe("tag")
system.umstellenAuf(true)
expect(themeStartwert()).toBe("tag")
})
})
+35
View File
@@ -68,6 +68,16 @@ function einstellung<T>(
return {
stand: () => wert,
/** Ob überhaupt etwas in der Ablage steht - also eine bewusste Wahl. */
gewaehlt: () => ablageLesen(schluessel) !== null,
/* Setzt den gehaltenen Wert, OHNE ihn zu speichern. Für Werte, die dem
System folgen, solange niemand selbst gewählt hat: eine Systemumstellung
ist keine Entscheidung des Nutzers und darf deshalb keine ablegen. */
ausSystem: (neu: T) => {
if (neu === wert) return
wert = neu
for (const bei of horcher) bei()
},
horchen: (bei: () => void) => {
horcher.add(bei)
return () => horcher.delete(bei)
@@ -95,6 +105,31 @@ const THEMA = einstellung<Theme>(
(wert) => wert,
)
/* Systemumstellung im laufenden Betrieb nachziehen.
Der Startwert oben liest `prefers-color-scheme` genau einmal, beim Laden des
Moduls. Stellt das Gerät danach um - etwa durch die automatische Umschaltung
bei Sonnenuntergang -, folgte das Stylesheet sofort (es hängt selbst an der
Medienabfrage), der React-Zustand aber nie. Alles, was sein Bild aus
`useTheme()` wählt statt aus CSS, blieb damit auf der alten Fassung stehen:
die Vier Ringe und das Typenschild sind `<img>` mit eingebettetem SVG und
je einer hellen und einer dunklen Fassung (siehe VierRinge in
Typenschild.tsx). Auf dunklem Grund stand dann die dunkle Fassung - schwarz
auf schwarz, vom Eigentümer am 05.09.2026 gemeldet. Ein Neuladen räumte es
auf, weshalb es sprunghaft wirkte.
Nur solange niemand selbst gewählt hat: eine gespeicherte Wahl in
"Einstellungen" bleibt unangetastet. */
if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
const dunkel = window.matchMedia("(prefers-color-scheme: dark)")
const nachziehen = () => {
if (THEMA.gewaehlt()) return
THEMA.ausSystem(dunkel.matches ? "nacht" : "tag")
}
if (typeof dunkel.addEventListener === "function") dunkel.addEventListener("change", nachziehen)
else if (typeof dunkel.addListener === "function") dunkel.addListener(nachziehen)
}
const TABBESCHRIFTUNG = einstellung<boolean>(
"dm360.tabbeschriftung",
-17
View File
@@ -1,17 +0,0 @@
/**
* Bau der Musterseite (siehe muster/muster.tsx).
*
* Bewusst eine EIGENE Konfiguration mit eigenem Ausgabeordner: die Seite darf
* nicht in `dist/` landen, weil `dist/` die Huelle der iOS-App ist. Was dort
* liegt, wird mit ausgeliefert.
*/
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
root: "muster",
base: "./",
plugins: [react()],
define: { __APP_VERSION__: JSON.stringify("muster") },
build: { outDir: "../dist-muster", emptyOutDir: true },
})
@@ -1 +1 @@
{"version":"2026.9.5.3","sha256":"6c9e0c3ef0429fd6ec6264af982494b9c699e8b3483fea145946d991805b0e47","bytes":324399,"gebaut":"2026-09-04T23:51:45Z"}
{"version":"2026.9.5.10","sha256":"6bdd14f277d1d3f6f2c24dc0ab660f2751daac8fc71efdb14440df87fe7a5af6","bytes":324448,"gebaut":"2026-09-05T00:22:21Z"}
@@ -2906,7 +2906,12 @@ let svAuswahl = 0;
function vService() {
const sv = CAR.service;
return `
<div class="tile"><span class="label">Anstehende Termine</span>
<!-- Ohne Kachelueberschrift: "Anstehende Termine" stand 13px grau und nur
5px ueber "Oelwechsel" (16px, --fg) und las sich damit als dessen
Beischrift statt als Titel (gemessen und gemeldet am 05.09.2026). Die
drei Zeilen benennen sich selbst, und die Seite heisst bereits
"Service" - eine Ueberschrift traegt hier nichts bei. -->
<div class="tile">
${termine().map((t) => {
const artIcon = ART_ICON[t.art];
const iconMarkup = artIcon ? ciSVG(artIcon, ART_ICON_GROESSE[t.art]) : "";
@@ -3295,25 +3300,27 @@ function vSbuch() {
<span class="label" style="margin-top:12px">${e.datum} · ${esc(e.werkstatt)}</span>
</div>
<div class="tile"><span class="label">Eintrag</span>
<div class="feld"><label>Datum</label><input value="${esc(e.datum)}" data-sb="${i}|datum"></div>
<div class="feld"><label>Kilometerstand</label>
<span class="mitEinheit"><input type="number" value="${e.km}" data-sb="${i}|km"><span>km</span></span></div>
<div class="feld"><label>Art</label>
<select data-sb="${i}|art">
${!artBekannt ? `<option value="${esc(e.art)}" selected>${esc(e.art)}</option>` : ""}
${SERVICE_ARTEN.map((a) => `<option ${a === e.art ? "selected" : ""}>${esc(a)}</option>`).join("")}
</select></div>
<div class="feld"><label>Werkstatt</label><input value="${esc(e.werkstatt)}" data-sb="${i}|werkstatt"></div>
<div class="feld"><label>Datum</label><input class="w-datum" value="${esc(e.datum)}" data-sb="${i}|datum"></div>
<div class="feld"><label>Kilometerstand</label>
<span class="mitEinheit"><input type="number" value="${e.km}" data-sb="${i}|km"><span>km</span></span></div>
<!-- Label oben, Feld ueber die ganze Zeile: ein echter Werkstattname
("Audi AG Kundendienst Center Ingolstadt") braucht 322px, neben einem
Label bleiben auf 375px aber nur 218 - gemessen am 05.09.2026. Dieselbe
Bauweise wie der Reifen-Kommentar. -->
<div class="feld feld--breit"><label>Werkstatt</label>
<input value="${esc(e.werkstatt)}" data-sb="${i}|werkstatt"></div>
<div class="feld" style="border-bottom:0"><label>Kosten</label>
<span class="mitEinheit"><input type="number" step="0.01" value="${e.kosten ?? ""}"
<span class="mitEinheit"><input class="w-betrag" type="number" step="0.01" value="${e.kosten ?? ""}"
data-sb="${i}|kosten" placeholder="noch offen"><span></span></span></div>
<span class="label" style="margin-top:14px">Kosten fließen in die Jahresübersicht ein</span>
</div>
<div class="tile"><span class="label">Durchgeführte Arbeiten</span>
<textarea class="notiz" data-sb="${i}|arbeiten" rows="7"
placeholder="Was wurde gemacht, welche Teile wurden getauscht, was ist aufgefallen">${esc(e.arbeiten || "")}</textarea>
<span class="label" style="margin-top:12px">Beim Verkauf ist diese Liste mehr wert als die Rechnung
sie zeigt, dass gepflegt wurde</span>
</div>
${istEntwurf ? `<button class="aktion" data-sbspeichern>Speichern</button>`
: `<button class="aktion loeschen" data-sbdel="${i}">Eintrag löschen</button>`}`;
@@ -3647,19 +3654,13 @@ function vEinst() {
${(() => {
const info = bildInfo(startIndex());
return `<div style="margin-top:12px">
<div data-bildklick="${esc(info.datei)}">
<div data-bildklick="${esc(info.datei)}" data-bildname="${esc(info.label)}">
${/* "klein" = das Bild bestimmt die Hoehe, statt im 16:9-Kasten
beschnitten zu werden (siehe .carfix.klein). Wer hier prueft,
ob das richtige Foto hochgeladen ist, muss es ganz sehen. */""
}${bildMitPlatzhalter(info.src, info.datei, info.label, "carfix klein", null, true)}
</div>
<input type="file" accept="image/*" hidden data-bildupload="${esc(info.datei)}">
${bildMenuOffen === info.datei ? `
<div class="bildmenu-catcher" data-bildmenu-schliessen></div>
<div class="bildmenu ansicht">
<button type="button" data-bildersetzen="${esc(info.datei)}">Foto ersetzen</button>
<button type="button" class="loeschen" data-bildloeschen="${esc(info.datei)}">Löschen</button>
</div>` : ""}
</div>`;
})()}
</div>
@@ -3879,7 +3880,26 @@ let debugOffen = false;
"Speichern" uebernimmt ihn, weil derselbe Wert den Schlaf-Timeout des
Dongles stellt. Siehe die Kachel in vEinst(). */
let PAUSENZEIT_ENTWURF = null;
let bildMenuOffen = null;
/* Das Blatt zu einem vorhandenen Foto: ersetzen oder loeschen. Loest
bildMenuOffen ab - der Zustand wird nicht mehr gebraucht, weil das Blatt
selbst der Zustand ist (siehe aktionsblatt()). */
function bildAktionsblatt(dateiname, name) {
aktionsblatt(name, [
{ text: "Foto ersetzen", tun: () => {
const input = ROOT.querySelector(`[data-bildupload="${dateiname}"]`);
if (input) input.click();
} },
{ text: "Foto löschen", destruktiv: true, tun: () => {
bestaetigen(LOESCH_TEXT.bild, LOESCH_HINWEIS.bild, "Foto löschen", () => {
(async () => {
try { await HASS.callService(DOMAIN, "bild_loeschen", { dateiname }); }
catch (err) { hinweis("Löschen fehlgeschlagen", err.message); return; }
bildVersion = Date.now(); render();
})();
});
} },
]);
}
let smartdealPopupOffen = false;
// "sommer"|"winter", welcher Reifensatz gerade sein km-Korrektur-Feld offen
// hat (Editier-Symbol neben der km-Anzeige) - null wenn keins offen ist.
@@ -4172,7 +4192,7 @@ function vReifen() {
${t.km != null ? de(t.km) : ""}<span class="unit">km</span>
</div>
</div>
<div data-bildklick="${radDatei}">
<div data-bildklick="${radDatei}" data-bildname="${esc(radLabel)}">
${bildMitPlatzhalter(bildAdresse(radDatei), radDatei, radLabel, "radbild")}
</div>
</div>
@@ -4189,12 +4209,6 @@ function vReifen() {
<button class="aktion" data-kmauf="${k}">Abbrechen</button>
</div>` : ""}
<input type="file" accept="image/*" hidden data-bildupload="${radDatei}">
${bildMenuOffen === radDatei ? `
<div class="bildmenu-catcher" data-bildmenu-schliessen></div>
<div class="bildmenu rad">
<button type="button" data-bildersetzen="${radDatei}">Foto ersetzen</button>
<button type="button" class="loeschen" data-bildloeschen="${radDatei}">Löschen</button>
</div>` : ""}
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:var(--sp-4)">
<button data-satz="${k === "sommer" ? "Sommer" : "Winter"}" class="montiert ${aktiv ? "on" : ""}">Montiert</button>
<button class="iconbtn archivbtn" data-reifenarchivieren="${k}" aria-label="Neue Räder anlegen">${ciSVG(CI.packageS, 18)}</button>
@@ -5178,6 +5192,34 @@ function themeGewaehlt() {
if (gespeichert === "nacht" || gespeichert === "tag") return gespeichert;
return window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches ? "tag" : "nacht";
}
/* Systemumstellung im laufenden Betrieb nachziehen - Gegenstueck zum
gleichnamigen Horcher in companion-app/src/theme.ts.
themeGewaehlt() liest die Systemeinstellung genau einmal, beim Einhaengen
des Elements. Stellte das Geraet danach um - etwa durch die automatische
Umschaltung bei Sonnenuntergang -, blieb das Panel auf dem alten Stand
stehen, bis jemand neu lud. Das Stylesheet haengt an data-theme, nicht an
der Medienabfrage, es blieb also stimmig - aber eben stimmig falsch.
Nur solange niemand selbst gewaehlt hat: eine gespeicherte Wahl bleibt. */
function themaSystemNachziehen() {
if (!window.matchMedia) return;
const dunkel = window.matchMedia("(prefers-color-scheme: dark)");
const nachziehen = () => {
let gespeichert = null;
try { gespeichert = localStorage.getItem(THEME_KEY); } catch (e) { /* Privatmodus */ }
if (gespeichert === "nacht" || gespeichert === "tag") return;
if (!HOST) return;
const neu = dunkel.matches ? "nacht" : "tag";
if (HOST.dataset.theme === neu) return;
HOST.dataset.theme = neu;
themeIcon();
tiles();
};
if (dunkel.addEventListener) dunkel.addEventListener("change", nachziehen);
else if (dunkel.addListener) dunkel.addListener(nachziehen);
}
function themeSetzen(modus) {
HOST.dataset.theme = modus;
try { localStorage.setItem(THEME_KEY, modus); } catch (e) { /* Privatmodus */ }
@@ -5197,6 +5239,22 @@ function hinweis(titel, text) {
sheet = { titel, text, knopf: null, aktion: null };
render();
}
/* Mehrere Wahlmoeglichkeiten in einem Blatt, plus abgesetztes Abbrechen -
Apples Aktionsblatt. Gegenstueck zu <ActionSheet> in der App
(design-system), das dort schon seit dem 31.08.2026 das schwebende
Bildmenue abgeloest hat.
Warum das Panel nachzieht: das alte .bildmenu war mit position:absolute an
die KACHEL geheftet, nicht an den Bildschirm. Bei einer hohen Kachel - den
Raedern - lag es dadurch unterhalb des Sichtfelds, und einen Weg hinaus gab
es auch nicht: wer das Rad nur versehentlich antippte, fand kein Abbrechen.
Beides am 05.09.2026 vom Eigentuemer gemeldet. Das Blatt haengt am Wurzel-
Element und ist deshalb immer zu sehen. */
function aktionsblatt(titel, aktionen) {
sheet = { titel, text: null, knopf: null, aktion: null, aktionen };
render();
}
/* ------------------------------------------------------- Beleg-Upload
Ein Weg für alle drei Quellen (Ziehen, Einfügen, Dateiauswahl). Kein
serviceRufen() hier: dessen catch loggt nur in die Konsole - siehe den
@@ -5501,6 +5559,15 @@ function vBelegPopup() {
function sheetMarkup() {
if (!sheet) return "";
const kopf = `<div class="sheet-kopf"><strong>${esc(sheet.titel)}</strong>${sheet.text ? `<span>${esc(sheet.text)}</span>` : ""}</div>`;
if (sheet.aktionen) {
const knoepfe = sheet.aktionen.map((a, i) =>
`<button data-sheet-aktion="${i}" class="${a.destruktiv ? "loeschen" : ""}">${esc(a.text)}</button>`).join("");
return `<div class="sheet-catcher" data-sheet-abbrechen></div>
<div class="sheet" role="dialog" aria-modal="true">
<div class="sheet-gruppe">${kopf}${knoepfe}</div>
<button class="sheet-abbrechen" data-sheet-abbrechen>Abbrechen</button>
</div>`;
}
const aktion = sheet.knopf
? `<button data-sheet-ok class="${sheet.destruktiv ? "loeschen" : ""}">${esc(sheet.knopf)}</button>`
: `<button data-sheet-abbrechen>Verstanden</button>`;
@@ -6281,6 +6348,13 @@ function ereignisseVerdrahten() {
ROOT.addEventListener("click", (e) => {
// B4: Das Action Sheet liegt über allem und fängt daher zuerst ab.
if (e.target.closest("[data-sheet-abbrechen]")) { sheetSchliessen(); return; }
const blattAktion = e.target.closest("[data-sheet-aktion]");
if (blattAktion) {
const gewaehlt = sheet && sheet.aktionen && sheet.aktionen[Number(blattAktion.dataset.sheetAktion)];
sheet = null; render();
if (gewaehlt && gewaehlt.tun) gewaehlt.tun();
return;
}
if (e.target.closest("[data-sheet-ok]")) {
const aktion = sheet && sheet.aktion;
sheet = null; render();
@@ -6370,18 +6444,6 @@ function ereignisseVerdrahten() {
bestaetigen(LOESCH_TEXT.sbuch, LOESCH_HINWEIS.sbuch, "Löschen", () => { eintragLoeschen("sbuch", id); go("service"); });
return;
}
const bildDel = e.target.closest("[data-bildloeschen]");
if (bildDel) {
const dateiname = bildDel.dataset.bildloeschen;
bestaetigen(LOESCH_TEXT.bild, LOESCH_HINWEIS.bild, "Foto löschen", () => {
(async () => {
try { await HASS.callService(DOMAIN, "bild_loeschen", { dateiname }); }
catch (err) { hinweis("Löschen fehlgeschlagen", err.message); return; }
bildMenuOffen = null; bildVersion = Date.now(); render();
})();
});
return;
}
// Fahrzeugbild (Radbild wie Fahrzeugbilder-Slots): vorhandenes Foto -> Popup
// mit Ersetzen/Löschen, fehlendes Foto (Platzhalter, bild-fehlt) -> Dateiauswahl
// direkt öffnen.
@@ -6394,18 +6456,10 @@ function ereignisseVerdrahten() {
const input = container.querySelector(`[data-bildupload="${dateiname}"]`);
if (input) input.click();
} else {
bildMenuOffen = dateiname; render();
bildAktionsblatt(dateiname, bildKlick.dataset.bildname || "Foto");
}
return;
}
if (e.target.closest("[data-bildmenu-schliessen]")) { bildMenuOffen = null; render(); return; }
const bildErsetzen = e.target.closest("[data-bildersetzen]");
if (bildErsetzen) {
const container = bildErsetzen.closest(".tile");
const input = container.querySelector(`[data-bildupload="${bildErsetzen.dataset.bildersetzen}"]`);
if (input) input.click();
return;
}
if (e.target.closest("[data-sdabbrechen]")) { smartdealPopupOffen = false; render(); return; }
if (e.target.closest("[data-sdbestaetigen]")) {
const datum = $("#sdPopupBis").value;
@@ -6887,7 +6941,7 @@ function ereignisseVerdrahten() {
const daten_base64 = await bildAlsBase64(klein);
await HASS.callService(DOMAIN, "bild_hochladen", { dateiname, daten_base64 });
} catch (err) { hinweis("Hochladen fehlgeschlagen", err.message); }
bildMenuOffen = null; bildVersion = Date.now(); render();
bildVersion = Date.now(); render();
})();
return;
}
@@ -7312,6 +7366,7 @@ class AudiDashboardPanel extends HTMLElement {
/* A6: Standard folgt der Systemeinstellung, eine manuelle Wahl überlebt den
Reload. Vorher stand die App nach jedem Neuladen wieder auf Nacht. */
HOST.dataset.theme = HOST.dataset.theme || themeGewaehlt();
themaSystemNachziehen();
schriftenLaden();
@@ -236,15 +236,10 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
folgen der neuen Lage: das Aktionsblatt steht frei ueber der unteren
Kante und ist rundum rund, das Formularblatt sitzt auf der Kante auf und
ist nur oben rund. */
.bildmenu{border:0;border-radius:16px;background:rgba(40,44,52,.92);
-webkit-backdrop-filter:saturate(180%) blur(22px);backdrop-filter:saturate(180%) blur(22px);
box-shadow:0 12px 40px rgba(0,0,0,.5)}
.sdpopup{border:0;border-radius:16px 16px 0 0;background:rgba(40,44,52,.92);
-webkit-backdrop-filter:saturate(180%) blur(22px);backdrop-filter:saturate(180%) blur(22px);
box-shadow:0 -8px 40px rgba(0,0,0,.5)}
:host([data-theme="tag"]) .bildmenu,:host([data-theme="tag"]) .sdpopup{background:rgba(250,250,252,.92)}
.bildmenu button{padding:16px 18px;font-size:16px;min-height:52px;border-bottom:1px solid var(--ios-sep)}
.bildmenu button:last-child{border-bottom:none}
:host([data-theme="tag"]) .sdpopup{background:rgba(250,250,252,.92)}
/* ---------------- Tab-Leiste ---------------- */
.tabbar{border-top:1px solid var(--ios-sep);padding-bottom:26px;
@@ -363,7 +358,7 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
ruhigen Karte. max-width + margin:auto zentriert sie innerhalb des
bestehenden left/right-Abstands, ohne die transform-basierte
Einblend-Animation dieser Elemente zu beruehren. */
.sdpopup,.sheet,.beleg-popup,.bildmenu{max-width:560px;margin-left:auto;margin-right:auto}
.sdpopup,.sheet,.beleg-popup{max-width:560px;margin-left:auto;margin-right:auto}
/* .setup-popup breiter als die übrigen Popups: die Sensorkennung neben der
Feld-Überschrift (entitaetIdKurz()) kann ein echter Rohsensorname von
Herstellerseite sein, teils 40-60 Zeichen lang
@@ -401,11 +396,21 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
Wortgleich mit rahmen.css/screens.css der App (04.09.2026 gemessen).
Das Overlay laesst das Bild unveraendert und vergroessert nur, was der
Finger trifft; Mindesthoehen dort, wo das Element ohnehin Flaeche ist. */
#back, .ringsbtn, .icon-btn, .zahnrad {
/* .zahnrad gehoert NICHT in diese Gruppe. Es ist in audi-dashboard.css
bereits 44x44 gross und mit position:absolute in die obere rechte
Kachelecke gesetzt. Dieses Blatt laedt danach, gleiche Spezifitaet - das
position:relative hier hat das absolute also ueberschrieben, und das
Zahnrad rutschte aus der Ecke in den Textfluss (im Autohaus-Kasten 58px
von oben, 288px von rechts; gemeldet vom Eigentuemer am 05.09.2026).
Die Auflage brauchte es ohnehin nie: die 44x44 stehen schon im Basisblatt.
Hinzugefuegt hatte ich sie beim HIG-Durchgang am 04.09.2026, ohne zu
pruefen, welche Knoepfe ihre Flaeche laengst haben. */
#back, .ringsbtn, .icon-btn {
position: relative;
}
#back::after, .ringsbtn::after, .icon-btn::after, .zahnrad::after {
#back::after, .ringsbtn::after, .icon-btn::after {
content: "";
position: absolute;
left: 50%;
@@ -808,6 +808,18 @@ img.carfix.klein { aspect-ratio: auto; height: auto; }
.mitEinheit input { min-width: 0; width: 96px; text-align: right; }
.mitEinheit span:last-child { font-size: 13px; color: var(--fg2); }
.druckfeld { width: 58px !important; min-width: 0 !important; }
/* Feldbreiten nach dem, was drinsteht, statt nach dem Browser-Standardwert.
Ohne diese Klassen bekommt jedes Textfeld dieselbe intrinsische Breite
(~170-190px, Safaris Vorgabe ohne size-Attribut) - unabhaengig vom Inhalt.
Im Wartungsplan hiess das: "01.03.2025" schwamm in einem viel zu breiten
Feld, waehrend ein Werkstattname im selben Mass abgeschnitten wurde
(gemeldet am 05.09.2026). Die Breiten sind an ihrem laengsten erwarteten
Inhalt gemessen ("01.03.2025" bzw. "1234,56"). Der Werkstattname passt in
keine Spalte neben einem Label und steht deshalb in einer .feld--breit-Zeile
ueber die ganze Breite. */
.feld input.w-datum { width: 124px; flex: 0 0 auto; }
.mitEinheit input.w-betrag { width: 116px; }
.notiz { width: 100%; text-align: left; margin-top: 14px; line-height: 1.55; resize: vertical; }
.switch { position: relative; display: inline-block; flex-shrink: 0; width: 46px; height: 27px; }
@@ -1319,34 +1331,9 @@ img.carfix.klein { aspect-ratio: auto; height: auto; }
background: rgba(0,0,0,.45);
animation: sheet-fade .18s ease-out;
}
.bildmenu {
position: absolute; z-index: 1001;
left: 10px; right: 10px;
bottom: max(10px, env(safe-area-inset-bottom));
top: auto;
transform: none;
background: var(--tile-deckend, var(--canvas));
border-radius: 16px;
overflow: hidden;
padding: 0;
display: flex; flex-direction: column;
animation: sheet-rein .24s cubic-bezier(.22,.61,.36,1);
}
/* Die beiden Lage-Varianten sind gegenstandslos geworden - ein Blatt
kommt immer von unten. Die Klassen bleiben im Markup stehen, damit die
Aufrufstellen unberührt bleiben; hier tragen sie nichts mehr bei. */
.bildmenu.rad, .bildmenu.ansicht { left: 10px; right: 10px; top: auto; transform: none; }
.bildmenu button {
display: block; width: 100%; text-align: left;
background: none; border: none;
color: var(--fg); font-family: inherit; font-size: 16px;
padding: 16px 18px; min-height: 52px; border-radius: 0;
border-bottom: 1px solid var(--line);
cursor: pointer;
}
.bildmenu button:last-child { border-bottom: none; }
.bildmenu button:active { background: var(--shade); }
.bildmenu button.loeschen { color: var(--bad); }
/* .bildmenu ist am 05.09.2026 entfallen: das Fotomenue laeuft jetzt ueber
das Aktionsblatt (aktionsblatt() in audi-dashboard-app.js), wie in der
App. .bildmenu-catcher bleibt - den benutzt das SmartDeal-Blatt. */
/* Formularblatt (siehe Kasten oben): unten angeschlagen, nur oben rund,
blickdicht. Stand vorher als Karte 60px unter dem oberen Rand - ausser
@@ -1486,12 +1473,22 @@ img.carfix.klein { aspect-ratio: auto; height: auto; }
overflow: hidden;
display: flex; flex-direction: column;
}
/* Der Kopf eines Aktionsblatts ist eine Aufschrift, keine Wahl.
Er stand linksbuendig, 15px, in --fg - also fast genau wie die Knopfzeilen
darunter, nur eine Haarlinie davon getrennt. Beim Radfoto ("Sommerrad")
las der Eigentuemer ihn deshalb als dritte Auswahl (gemeldet 05.09.2026).
Apples Aktionsblatt setzt den Titel zentriert, kleiner und in der
gedaempften Farbe - dieselben drei Unterschiede, die ihn hier wieder als
Überschrift lesbar machen. */
.sheet-kopf {
padding: 15px 18px 14px;
padding: 14px 18px 12px;
border-bottom: 1px solid var(--line);
display: flex; flex-direction: column; gap: 4px;
display: flex; flex-direction: column; align-items: center;
text-align: center; gap: 4px;
}
.sheet-kopf strong { font-size: 15px; font-weight: 400; color: var(--fg); }
.sheet-kopf strong { font-size: 13px; font-weight: 600; color: var(--fg2); }
.sheet-kopf span { font-size: 13px; color: var(--fg2); line-height: 1.45; }
.sheet-gruppe button {
background: none; border: none;
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.5.3",
"version": "2026.9.5.10",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": [
+15 -4
View File
@@ -139,18 +139,29 @@
flex-direction: column;
}
/* Der Kopf eines Aktionsblatts ist eine Aufschrift, keine Wahl.
Er stand linksbündig, 15px, in --fg - also fast genau wie die Knopfzeilen
darunter, nur eine Haarlinie davon getrennt. Beim Radfoto ("Sommerrad")
las der Eigentümer ihn deshalb als dritte Auswahl (gemeldet 05.09.2026).
Apples Aktionsblatt setzt den Titel zentriert, kleiner und in der
gedämpften Farbe - dieselben drei Unterschiede, die ihn hier wieder als
Überschrift lesbar machen. */
.ads-aktionsblatt__kopf {
padding: 15px 18px 14px;
padding: 14px 18px 12px;
border-bottom: 1px solid var(--line);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 4px;
}
.ads-aktionsblatt__kopf strong {
font-size: 15px;
font-weight: 400;
color: var(--fg);
font-size: 13px;
font-weight: 600;
color: var(--fg2);
}
.ads-aktionsblatt__kopf span {