/* ================================================================
   Audi-Dashboard — panel_custom Custom Element
   ================================================================
   Umbau von dashboard-muster-ohne-bilder.html (der verbindliche Prototyp
   aus dem Lastenheft) zu einem panel_custom-Element für Home Assistant.

   Strategie: die Render-Logik des Prototyps bleibt praktisch unverändert
   (dieselben Funktionen vHome(), vTrips(), render() usw.) - geändert wurde
   nur die Datenschicht (CONFIG/CAR/TRIPS/FILLS werden jetzt von den
   pyscript-Zustandsentitäten geladen statt fest im Code zu stehen) und die
   DOM-Anbindung (Shadow DOM statt document, siehe $ weiter unten).

   Bekannte Lücken in diesem Stand:
   - Kein Bild-Ordner vorhanden; Bilder fallen auf die im Prototyp bereits
     vorgesehenen Platzhalter-Maße zurück (§7a), erscheinen aber als
     gebrochene Bilder, bis bilder/ tatsächlich befüllt ist.
   - Leaflet lädt weiterhin per CDN (wie im Prototyp) - funktioniert nur,
     wenn das Gerät zusätzlich zur Tailscale-Verbindung Internetzugang hat.
*/

/* ---------------------------------------------------------------------
   Modul-weiter Zustand. Bewusst außerhalb der Klasse, damit die praktisch
   unveränderte Render-Logik des Prototyps (freie Funktionen, kein `this`)
   weiterhin funktioniert - siehe Kopfkommentar. Für eine Einzel-Panel-
   Anwendung wie diese ist eine Instanz gleichbedeutend mit "die eine
   Oberfläche", genau wie im ursprünglichen Prototyp auch nur ein
   <body> existierte.
   --------------------------------------------------------------------- */
let HASS = null;
let ROOT = null;   // this.shadowRoot der Panel-Instanz
let HOST = null;   // die Panel-Instanz selbst (für :host([data-theme]))

let CONFIG = null;
let CAR = null;
let TRIPS = [];
let FILLS = [];
let BATTERIEVERLAUF = [];
let DATEN_GELADEN = false;
// Zeitpunkt (ms) des letzten echten Fahrzeugstatus-Updates vom Backend -
// null, solange keiner bekannt ist (siehe standAlterText()).
let LETZTES_UPDATE_TS = null;

/* Cache-Buster fuer audi-dashboard.css: diese Datei selbst wird von HA mit
   derselben langen /local/-Cache-Zeit ausgeliefert wie audi-dashboard-panel.js
   (siehe dessen Kopfkommentar) - ohne Versions-Query bleibt ein Browser, der
   sie einmal geladen hat, auch nach einer CSS-Aenderung auf dem alten Stand
   haengen, obwohl app.js selbst per audi-dashboard-version.json korrekt neu
   geladen wird. Die eigene Script-URL (?v=N, von audi-dashboard-panel.js
   gesetzt) liefert dieselbe Versionsnummer, ohne version.json ein zweites
   Mal abfragen zu muessen.
   */
const CSS_VERSION = (() => {
  try {
    return new URL(import.meta.url).searchParams.get("v") || Date.now();
  } catch (e) {
    return Date.now();
  }
})();

const $ = (s) => ROOT.querySelector(s);

/* ---------------------------------------------------------------- Format */
const de = (n, d = 0) => Number(n ?? 0).toLocaleString("de-DE", { minimumFractionDigits: d, maximumFractionDigits: d });
const eur = (n) => de(n, 2);
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
const sum = (a, f) => a.reduce((s, x) => s + f(x), 0);
/* Durchschnittspreis IMMER mengengewichtet: Summe Kosten / Summe Liter */
const schnittpreis = (list) => sum(list, (f) => f.fuel_total_eur ?? f.summe ?? 0) / sum(list, (f) => f.liters ?? f.liter ?? 0);

/* -------------------------------------------------------- Adapter-Schicht
   Übersetzt die Fahrzeugprofil-Struktur aus dem Backend (fahrzeug/
   einstellungen/versicherung/steuer/reifen/service/technik/ausstattung) in
   die CONFIG/CAR-Form, mit der der Rest dieser Datei arbeitet - direkt aus
   dem Prototyp übernommen, um dessen Render-Funktionen unverändert
   weiterzuverwenden. */
function profilZuConfig(p) {
  return {
    fahrzeugtitel: p.fahrzeug.titel,
    fahrzeugzusatz: p.fahrzeug.zusatz,
    fahrzeuguntertitel: p.fahrzeug.untertitel,
    kennzeichen: p.fahrzeug.kennzeichen,
    startbild: p.einstellungen.uebersichtsbild,
    version: "1.0",
    tankvolumen: p.fahrzeug.tankvolumen_liter,
    nachtVon: p.einstellungen.nacht_von,
    nachtBis: p.einstellungen.nacht_bis,
    pauseMin: p.einstellungen.fahrten_pausenzeit_min,
    markenname: p.einstellungen.kraftstoffanbieter,
    wlanName: p.fahrzeug.wlan_name,
    backupIntervall: p.einstellungen.backup_intervall || "aus",
    letztesBackup: p.einstellungen.letztes_backup || null,
    ausfuehrung: p.fahrzeug.ausfuehrung || "",
  };
}

function profilZuCar(p, status) {
  // km ist kein abgeleiteter Wert mehr, sondern wird vom Backend
  // (reifenzaehler.py) direkt im Profil fortgeschrieben - jeder gefahrene
  // Kilometer zählt dort auf den zum Zeitpunkt aktiven Satz.
  const reifenSatz = (k) => p.reifen.saetze[k] || {};
  return {
    fin: p.fahrzeug.fin,
    details: p.fahrzeug.details,
    erstzulassung: p.fahrzeug.erstzulassung,
    hu: p.fahrzeug.hauptuntersuchung_faellig,
    odo: status.km ?? 0,
    odoBekannt: status.km != null,
    tankPct: status.tankprozent ?? 0,
    reichweite: status.reichweite_km,
    batteriespannung: status.batteriespannung,
    gesichert: status.gesichert,
    sicherheitscheck: status.sicherheitscheck || [],
    oelwechselFaelligTs: status.oelwechsel_faellig_ts,
    oelwechselFaelligKm: status.oelwechsel_faellig_km,
    inspektionFaelligTs: status.inspektion_faellig_ts,
    inspektionFaelligKm: status.inspektion_faellig_km,
    kmProTag: 18.3, // Fallback, nur solange kein Servicebuch-Eintrag existiert
    // "Gültig bis" ist einschließlich - der Rabatt gilt noch am Stichtag
    // selbst, erst am Folgetag schaltet der Schalter automatisch ab, ohne
    // dass der Nutzer manuell eingreifen muss.
    smartdeal: { aktiv: p.einstellungen.smartdeal.aktiv && (!p.einstellungen.smartdeal.laeuft_ab ||
      p.einstellungen.smartdeal.laeuft_ab >= new Date().toISOString().slice(0, 10)), laeuftAb: p.einstellungen.smartdeal.laeuft_ab },
    oel: {
      modus: p.einstellungen.oelwechsel_intervall.modus,
      km: p.einstellungen.oelwechsel_intervall.km,
      monate: p.einstellungen.oelwechsel_intervall.monate,
      herstellerKm: p.einstellungen.oelwechsel_intervall.hersteller_km,
      herstellerMonate: p.einstellungen.oelwechsel_intervall.hersteller_monate,
    },
    versicherung: p.versicherung,
    steuer: { ...p.steuer, faellig: p.steuer.faellig || "" },
    reifen: {
      aktiv: p.reifen.aktiv === "winter" ? "Winter" : "Sommer",
      anzugsmoment: p.reifen.anzugsmoment_nm,
      wechsel: p.reifen.wechsel,
      sommer: reifenSatz("sommer"),
      winter: reifenSatz("winter"),
    },
    service: p.service,
    technik: p.technik,
    ausstattung: p.ausstattung,
  };
}

/* Rückrichtung: CONFIG/CAR (im Speicher gehalten, vom Prototyp-Code direkt
   mutiert) zurück in die Profil-Form für audi_dashboard_profil_schreiben. */
function configCarZuProfil(vorherigesProfil) {
  const p = JSON.parse(JSON.stringify(vorherigesProfil));
  p.fahrzeug.titel = CONFIG.fahrzeugtitel;
  p.fahrzeug.zusatz = CONFIG.fahrzeugzusatz;
  p.fahrzeug.untertitel = CONFIG.fahrzeuguntertitel;
  p.fahrzeug.kennzeichen = CONFIG.kennzeichen;
  p.fahrzeug.wlan_name = CONFIG.wlanName;
  p.fahrzeug.hauptuntersuchung_faellig = CAR.hu;
  p.fahrzeug.fin = CAR.fin;
  p.fahrzeug.erstzulassung = CAR.erstzulassung;
  p.fahrzeug.ausfuehrung = CONFIG.ausfuehrung;
  p.einstellungen.uebersichtsbild = CONFIG.startbild;
  p.einstellungen.fahrten_pausenzeit_min = CONFIG.pauseMin;
  p.einstellungen.backup_intervall = CONFIG.backupIntervall;
  p.einstellungen.smartdeal = { aktiv: CAR.smartdeal.aktiv, laeuft_ab: CAR.smartdeal.laeuftAb };
  p.einstellungen.oelwechsel_intervall = {
    modus: CAR.oel.modus, km: CAR.oel.km, monate: CAR.oel.monate,
    hersteller_km: CAR.oel.herstellerKm, hersteller_monate: CAR.oel.herstellerMonate,
  };
  p.versicherung = CAR.versicherung;
  p.steuer = { ...CAR.steuer, faellig: CAR.steuer.faellig || null };
  p.reifen.aktiv = CAR.reifen.aktiv.toLowerCase();
  p.reifen.anzugsmoment_nm = CAR.reifen.anzugsmoment;
  p.reifen.wechsel = CAR.reifen.wechsel;
  for (const k of ["sommer", "winter"]) {
    // km wird vom Backend fortgeschrieben (reifenzaehler.py, reagiert auf
    // den Kilometerstand-Sensor) - hier nie mitschreiben, sonst würde ein im
    // Browser gehaltener älterer Stand eine zwischenzeitliche Fortschreibung
    // überschreiben.
    const { km, ...rest } = CAR.reifen[k];
    p.reifen.saetze[k] = { ...p.reifen.saetze[k], ...rest };
  }
  p.service = CAR.service;
  return p;
}

let PROFIL_ROH = null; // letzter vom Backend gelesener Rohstand, für configCarZuProfil()

async function profilSpeichern() {
  const neuesProfil = configCarZuProfil(PROFIL_ROH);
  PROFIL_ROH = neuesProfil;
  await HASS.callService("pyscript", "audi_dashboard_profil_schreiben", { profil_json: JSON.stringify(neuesProfil) });
}

/* ------------------------------------------------------------- Bilder
   Feste Dateinamen laut §7a, referenziert unter /local/ (aus www/ serviert).
   Der Ordner bilder/ selbst ist noch nicht befüllt (siehe Kopfkommentar) -
   die Flächen behalten dennoch ihre Maße, das Layout springt nicht. */
const BILDER = [
  { name: "Seitenansicht", label: "Auto Seite", datei: "seitenansicht.webp" },
  { name: "Front schräg", label: "Auto Front schräg", datei: "front-schraeg.webp" },
  { name: "Frontansicht", label: "Auto Front", datei: "frontansicht.webp" },
  { name: "Heckansicht", label: "Auto Heck", datei: "heckansicht.webp" },
  { name: "Cockpit", label: "Innenraum Cockpit", datei: "cockpit.webp" },
  { name: "Scheinwerfer", label: "Scheinwerfer", datei: "scheinwerfer.webp" },
  { name: "Sitze", label: "Innenraum Sitze", datei: "sitze.webp" },
];
let bild = 0;
const SEITE_WINTER = { label: "Auto Seite mit Winterrädern", datei: "seitenansicht-winter.webp" };
// Alle einzeln hochladbaren Bild-Plätze (§ Fahrzeugbilder-Upload in
// Einstellungen) - dieselben festen Dateinamen wie BILDER, plus die separat
// geführte Winter-Variante der Seitenansicht.
const BILDER_UPLOAD_SLOTS = [...BILDER, { name: "Seitenansicht (Winter)", label: SEITE_WINTER.label, datei: SEITE_WINTER.datei }];
// Bei jedem Hochladen/Löschen eines Fahrzeugfotos hochgezählt und als
// Query-Parameter an alle bilder/-URLs gehängt (siehe bildInfo()
// unten) - ohne das würde der Browser nach einem Upload weiter das alte Bild
// oder den zwischenzeitlich gecachten 404 unter derselben URL zeigen.
let bildVersion = Date.now();

/* Platzhalter statt kaputtem Bild-Symbol: bei Ladefehler (Bilder aus §7a
   noch nicht vorhanden) wird die Bildfläche durch ein generisches Symbol
   plus eindeutiger Beschriftung und dem erwarteten Dateinamen ersetzt -
   damit klar ist, welche Datei unter welchem Namen nachgereicht werden muss,
   ohne dass sich am Layout etwas verschiebt (Maße bleiben über die CSS-
   Klassen erhalten, siehe carfix/carfix.klein/radbild in audi-dashboard.css). */
/* A5: Der Platzhalter nennt das Motiv und die nächste Handlung - der
   Dateiname ist Entwicklersprache und steht nur noch in den Einstellungen
   (siehe BILDER_UPLOAD_SLOTS). Die Maße bleiben über die CSS-Klassen erhalten,
   das Layout springt beim Nachliefern eines Fotos nicht. */
function bildMitPlatzhalter(src, datei, label, klasse, imgId, dateiZeigen = false) {
  const idTeil = imgId ? ` id="${imgId}"` : "";
  return `<div class="bildbox ${klasse}" data-bildbox title="${esc(label)}">
    <img class="${klasse}"${idTeil} src="${src}" alt="${esc(label)}" data-bildlabel="${esc(label)}" data-bilddatei="${esc(datei)}"
         onerror="this.closest('[data-bildbox]').classList.add('bild-fehlt')">
    <div class="platzhalter ${klasse}">
      <svg viewBox="0 0 24 24" class="platzhalter-icon" fill="currentColor"><path fill-rule="evenodd" d="m23.35 1.35-22 22-.7-.7 22-22zM1 3h17.5v1H2v16H1zm21 11.85V4h1v17H6v-1h16v-3.77l-3.24-3.08-4.57 4.48-2.85-2.84.7-.7 2.15 2.14 4.56-4.47 3.25 3.1ZM4 8.5a2.5 2.5 0 1 1 5 0 2.5 2.5 0 0 1-5 0M6.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3" clip-rule="evenodd"/></svg>
      <span class="platzhalter-label">${esc(label)}</span>
      <span class="platzhalter-aktion">${dateiZeigen ? esc(datei) : "Foto hinzufügen"}</span>
    </div>
  </div>`;
}

function startIndex() {
  const i = BILDER.findIndex((b) => b.name === CONFIG.startbild);
  return i < 0 ? 0 : i;
}
function bildInfo(i) {
  const b = BILDER[i];
  if (b.name === "Seitenansicht" && CAR.reifen.aktiv === "Winter") {
    return { label: SEITE_WINTER.label, datei: SEITE_WINTER.datei, src: `/local/bilder/${SEITE_WINTER.datei}?v=${bildVersion}` };
  }
  return { label: b.label, datei: b.datei, src: `/local/bilder/${b.datei}?v=${bildVersion}` };
}
function bildSrc(i) { return bildInfo(i).src; }
function carImg(modus) {
  if (modus === "fest") {
    const info = bildInfo(startIndex());
    return bildMitPlatzhalter(info.src, info.datei, info.label, "carfix klein");
  }
  const info = bildInfo(bild);
  return bildMitPlatzhalter(info.src, info.datei, info.label, "carfix", "carimg") +
    `<div class="dots" id="dots">${BILDER.map((_, i) => `<i class="${i === bild ? "on" : ""}"></i>`).join("")}</div>`;
}
/* Bildmarke des Kraftstoffanbieters (Shell). Anders als bei den Fahrzeug-
   fotos ist hier kein generischer Platzhalter-Rahmen sinnvoll, weil das Logo
   inline neben Text steht (.marke) - Fehlerzustand blendet stattdessen den
   Markennamen als kleine Textmarke ein (.marke .ph in audi-dashboard.css),
   exakt wie im Prototyp vorgesehen. Die Bilddatei selbst liefert der Nutzer
   nach (offizielles Shell-Logo, hier nicht nachgezeichnet). */
const MARKENLOGO_DATEI = "shell-logo.svg";
function markenlogo() {
  if (!CAR.smartdeal.aktiv) return "";
  const label = CONFIG.markenname || "Logo";
  return `<span class="marke-logo" data-markelogo title="${esc(label)} (${esc(MARKENLOGO_DATEI)})">
    <img src="/local/bilder/${MARKENLOGO_DATEI}" alt="${esc(label)}" data-bilddatei="${esc(MARKENLOGO_DATEI)}"
         onerror="this.closest('[data-markelogo]').classList.add('logo-fehlt')">
    <span class="ph">${esc(label)}</span>
  </span>`;
}
const OHNE_SZENE = ["Cockpit", "Sitze", "Scheinwerfer"];
function szeneKlasse(i) {
  if (!bodenAn) return "";
  if (OHNE_SZENE.includes(BILDER[i].name)) return "";
  return CAR.reifen.aktiv === "Winter" ? "winter" : "strasse";
}
function bildWeiter() {
  bild = (bild + 1) % BILDER.length;
  const img = ROOT.getElementById("carimg");
  if (!img) return;
  const info = bildInfo(bild);
  img.src = info.src;
  img.alt = info.label;
  const box = img.closest("[data-bildbox]");
  if (box) {
    box.classList.remove("bild-fehlt"); // neues Bild kann laden, auch wenn das vorherige fehlte
    box.querySelector(".platzhalter-label").textContent = info.label;
    box.querySelector(".platzhalter-datei").textContent = info.datei;
  }
  const sz = ROOT.getElementById("szene");
  if (sz) sz.className = "szene " + szeneKlasse(bild);
  [...ROOT.getElementById("dots").children].forEach((d, i) => d.classList.toggle("on", i === bild));
}

/* ---------------------------------------------------- Termine/Vorhersage
   heute() statt einer eingefrorenen Konstante - der Prototyp nutzte ein
   festes Demo-Datum (new Date(2026,7,3)), das für den echten Betrieb durch
   das tatsächliche Tagesdatum ersetzt werden muss. */
function heute() { return new Date(); }
const dTag = 86400000;
const mmjj = (d) => String(d.getMonth() + 1).padStart(2, "0") + "/" + String(d.getFullYear()).slice(2);
const dedat = (d) => d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" });
function dat(s) { const [d, m, j] = s.split(".").map(Number); return new Date(j, m - 1, d); }

const MISCHDAUER = 180;
function tagesleistung() {
  // §9: fällt die Datenquelle aus, wird das sichtbar gemacht statt mit einem
  // Platzhalterwert weiterzurechnen - ohne diese Sperre rechnete die
  // Prognose mit CAR.odo=0 weiter und erzeugte Termine weit im Jahr -13000.
  if (!CAR.odoBekannt) return { wert: CAR.kmProTag, quelle: "Kilometerstand unbekannt", tage: 0, km: 0 };
  const b = CAR.service.buch;
  if (!b.length) return { wert: CAR.kmProTag, quelle: "Vorgabe", tage: 0, km: 0 };
  const alt = b[b.length - 1];
  const tageGesamt = Math.max(1, (heute() - dat(alt.datum)) / dTag);
  const langfristig = (CAR.odo - alt.km) / tageGesamt;
  const letzter = b[0];
  const tageSeit = (heute() - dat(letzter.datum)) / dTag;
  const kmSeit = CAR.odo - letzter.km;
  if (tageSeit < 14 || kmSeit <= 0)
    return { wert: langfristig, quelle: "gesamter Verlauf", tage: Math.round(tageGesamt), km: CAR.odo - alt.km };
  const aktuell = kmSeit / tageSeit;
  const g = Math.min(1, tageSeit / MISCHDAUER);
  return {
    wert: langfristig * (1 - g) + aktuell * g,
    quelle: g >= 1 ? "seit dem letzten Service" : "gemischt, " + Math.round(g * 100) + " % aktueller Abschnitt",
    tage: Math.round(tageSeit), km: kmSeit,
  };
}
function kmProTag() { return tagesleistung().wert; }
function prognose(zielKm) {
  if (!CAR.odoBekannt) return { tage: null, datum: null };
  const rest = zielKm - CAR.odo;
  if (rest <= 0) return { tage: 0, datum: heute() };
  const tage = Math.round(rest / kmProTag());
  return { tage, datum: new Date(heute().getTime() + tage * dTag) };
}
function ics(titel, datum, ort, notiz) {
  const tag = datum.replace(/-/g, "");
  const naechster = new Date(new Date(datum).getTime() + dTag).toISOString().slice(0, 10).replace(/-/g, "");
  const text = [
    "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//Audi Dashboard//DE", "BEGIN:VEVENT",
    "UID:" + Date.now() + "@audi-dashboard.local",
    "DTSTAMP:" + new Date().toISOString().replace(/[-:]/g, "").split(".")[0] + "Z",
    "DTSTART;VALUE=DATE:" + tag, "DTEND;VALUE=DATE:" + naechster,
    "SUMMARY:" + titel, "LOCATION:" + (ort || ""), "DESCRIPTION:" + (notiz || ""),
    "BEGIN:VALARM", "TRIGGER:-P2D", "ACTION:DISPLAY", "DESCRIPTION:" + titel, "END:VALARM",
    "END:VEVENT", "END:VCALENDAR",
  ].join("\r\n");
  const a = document.createElement("a");
  a.href = "data:text/calendar;charset=utf-8," + encodeURIComponent(text);
  a.download = titel.replace(/[^\wäöüÄÖÜß ]/g, "").replace(/\s+/g, "-") + ".ics";
  document.body.appendChild(a); a.click(); a.remove();
}
function ring(pct) {
  const r = 64, c = 2 * Math.PI * r, on = (c * pct) / 100;
  return `<div class="ringwrap">
    <svg width="150" height="150" viewBox="0 0 150 150">
      <circle cx="75" cy="75" r="${r}" fill="none" stroke="var(--line)" stroke-width="2"/>
      <circle cx="75" cy="75" r="${r}" fill="none" stroke="var(--red)" stroke-width="2"
        stroke-linecap="round" stroke-dasharray="${on} ${c}" transform="rotate(-90 75 75)"/>
    </svg>
    <div class="mid">
      <div class="fig" style="font-size:40px">${pct}<span class="unit">%</span></div>
      <span class="label" style="margin-top:8px">${de(CONFIG.tankvolumen * pct / 100, 1)} Liter</span>
    </div></div>`;
}

/* ------------------------------------------------------------------ Karte
   Leaflet weiterhin per CDN geladen (siehe Kopfkommentar). */
let MAP = null, TILE = null, LEAFLET_LADEND = null;
const TILES = {
  nacht: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
  tag: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
};
const ATTR = "&copy; OpenStreetMap &middot; CARTO";

function leafletLaden() {
  if (window.L) return Promise.resolve();
  if (LEAFLET_LADEND) return LEAFLET_LADEND;
  LEAFLET_LADEND = new Promise((resolve, reject) => {
    const css1 = document.createElement("link");
    css1.rel = "stylesheet";
    css1.href = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css";
    document.head.appendChild(css1);
    const script = document.createElement("script");
    script.src = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js";
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
  return LEAFLET_LADEND;
}

function mapBox() { return `<div class="mapbox"><div id="map"></div></div>`; }

/* Kartenmittelpunkt ohne eigene Positionsdaten (z. B. Fahrt ohne GPS-Punkte):
   die Werkstatt-Koordinaten aus dem Profil statt einer im Code fest
   hinterlegten Region - die liegen ohnehin meist in der Nähe und kommen
   schon aus einer echten, dafür vorgesehenen Datenquelle. Ohne Werkstatt-
   Koordinaten bleibt nur ein neutraler Mittelpunkt (geografische Mitte
   Deutschlands), keine an dieses Fahrzeug gebundene Vermutung. */
function kartenMittelpunkt() {
  const pos = CAR.service.autohaus.pos;
  return pos && pos.length === 2 ? pos : [51.1657, 10.4515];
}

function fakeTrack(a, b, n = 13) {
  const pts = []; let seed = Math.round((a[0] + b[1]) * 1000);
  const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff - 0.5; };
  for (let i = 0; i <= n; i++) {
    const t = i / n, k = Math.sin(t * Math.PI);
    pts.push([a[0] + (b[0] - a[0]) * t + rnd() * 0.05 * k, a[1] + (b[1] - a[1]) * t + rnd() * 0.05 * k]);
  }
  pts[0] = a; pts[n] = b; return pts;
}
function tiles() {
  if (!MAP || !window.L) return;
  const url = TILES[HOST.dataset.theme];
  if (TILE) MAP.removeLayer(TILE);
  TILE = window.L.tileLayer(url, { maxZoom: 19, attribution: ATTR }).addTo(MAP);
}
async function initMap() {
  await leafletLaden();
  const el = ROOT.getElementById("map");
  if (!el || typeof window.L === "undefined") return;
  MAP = window.L.map(el, { zoomControl: true, attributionControl: true, scrollWheelZoom: false });
  TILE = null; tiles();
  const rot = getComputedStyle(HOST).getPropertyValue("--red").trim() || "#F50537";
  if (route.name === "trip") {
    const t = TRIPS.find((x) => x.trip_id === route.id);
    if (t && t.start_lat != null && t.end_lat != null) {
      const track = fakeTrack([t.start_lat, t.start_lon], [t.end_lat, t.end_lon]);
      const line = window.L.polyline(track, { color: rot, weight: 2.5, opacity: 0.9 }).addTo(MAP);
      track.slice(1, -1).forEach((pt) => window.L.circleMarker(pt, { radius: 2.5, color: rot, weight: 0, fillColor: rot, fillOpacity: 0.85 }).addTo(MAP));
      window.L.circleMarker([t.start_lat, t.start_lon], { radius: 6, color: "#fff", weight: 2, fillColor: "#000", fillOpacity: 1 }).addTo(MAP).bindPopup("Start · " + (t.start_address || ""));
      window.L.circleMarker([t.end_lat, t.end_lon], { radius: 6, color: rot, weight: 2, fillColor: rot, fillOpacity: 1 }).addTo(MAP).bindPopup("Ziel · " + (t.end_address || ""));
      MAP.fitBounds(line.getBounds(), { padding: [26, 26] });
    } else {
      MAP.setView(kartenMittelpunkt(), 11); // kein Track ohne GPS-Daten (§7a-Backend-Lücke)
    }
  } else if (route.name === "fill") {
    const f = FILLS.find((x) => x.tank_id === route.id);
    if (f && f.station_lat != null) {
      MAP.setView([f.station_lat, f.station_lon], 14);
      window.L.circleMarker([f.station_lat, f.station_lon], { radius: 7, color: rot, weight: 2, fillColor: rot, fillOpacity: 0.85 }).addTo(MAP).bindPopup((f.station_name || "") + "<br>" + (f.station_address || ""));
    } else {
      MAP.setView(kartenMittelpunkt(), 11);
    }
  }
  setTimeout(() => MAP && MAP.invalidateSize(), 60);
}

/* -------------------------------------------------------------- Ansichten
   Ab hier praktisch unverändert aus dem Prototyp übernommen. */
function badge() {
  // Offizielle Audi-Markenassets aus www/badges/ (positive = für hellen,
  // negative = für dunklen Hintergrund) - welche Variante sichtbar ist,
  // entscheidet reines CSS über :host([data-theme=...]) (siehe .badge img in
  // audi-dashboard.css), genau wie beim Farbwechsel des restigen Interfaces
  // beim Tag/Nacht-Umschalten. Kein Re-Render beim Theme-Wechsel nötig.
  //
  // Der Zusatz neben dem Badge (z. B. "Avant competition") setzt sich aus
  // dem Modellnamen selbst zusammen - alles nach dem erkannten Badge-Präfix
  // ("Avant"/"Sportback"/...) - plus der separat gepflegten Ausführung, statt
  // aus dem alten, nicht mehr an die Modell-Auswahl gekoppelten
  // CONFIG.fahrzeugzusatz (siehe Kopfkommentar bei FAHRZEUG_MODELLE).
  const praefix = badgePraefix();
  if (!praefix) return "";
  const datei = BADGE_DATEIEN[praefix];
  const modellZusatz = (CONFIG.fahrzeugtitel || "").slice(praefix.length).trim();
  const zusatz = [modellZusatz, CONFIG.ausfuehrung].filter(Boolean).join(" ");
  const alt = esc(CONFIG.fahrzeugtitel) + " Badge";
  return `<div class="badge">
    <img class="positive" src="/local/badges/${datei}-positive.svg" alt="${alt}">
    <img class="negative" src="/local/badges/${datei}-negative.svg" alt="${alt}">
    ${zusatz ? `<span class="zusatz">${esc(zusatz)}</span>` : ""}</div>`;
}

function teaser() {
  if (!TRIPS.length && !FILLS.length) return "";
  const t = TRIPS[0], f = FILLS[0];
  return `<div class="tile"><span class="label">Zuletzt</span>
    ${t ? `<button class="leaf" style="padding-left:0" data-go="trip:${t.trip_id}">
      <span class="k">Letzte Fahrt<small>${esc(t.start_address || "")} → ${esc(t.end_address || "")}</small></span>
      <span class="v">${t.distance_km != null ? de(t.distance_km) + " km" : "offen"}<small>${t.status}</small></span>
      <svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>` : ""}
    ${f ? `<button class="leaf" style="padding-left:0;border-bottom:0" data-go="fill:${f.tank_id}">
      <span class="k">Letzter Tankvorgang<small>${esc(f.station_name || "")}</small></span>
      <span class="v">${f.liters != null ? de(f.liters, 2) + " l" : ""}<small>${f.fuel_total_eur != null ? eur(f.fuel_total_eur) + " €" : ""}</small></span>
      <svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>` : ""}</div>`;
}

function naechsterTermin() { return termine().slice().sort((a, b) => a.ziel - b.ziel)[0]; }

function intervalle(t) {
  const oel = CAR.oel, b = t.basis;
  if (!b) return null;
  const bauen = (km, monate) => ({ km: km ? b.km + km : null, datum: monatePlus(b.datum, monate) });
  if (t.art === "Hauptuntersuchung") {
    const eig = bauen(null, 24);
    return { hersteller: null, eigen: eig, prognose: eig.datum };
  }
  const eKm = t.art === "Ölwechsel" ? (oel.modus === "hersteller" ? oel.herstellerKm : Math.min(oel.km, oel.herstellerKm)) : 30000;
  const eM = t.art === "Ölwechsel" ? (oel.modus === "hersteller" ? oel.herstellerMonate : Math.min(oel.monate, oel.herstellerMonate)) : 24;
  const eig = bauen(eKm, eM);
  // "Herstellervorgabe" gilt bei Ölwechsel nur, wenn der Kunde in "Einrichten"
  // tatsächlich die Herstellervorgabe gewählt hat - bei eigenem Intervall ist
  // "eigen" maßgeblich, sonst würde anzeige = iv.hersteller || iv.eigen unten
  // immer die Herstellerwerte zeigen und das Kundenintervall ignorieren.
  const her = t.art === "Ölwechsel" ? (oel.modus === "hersteller" ? bauen(oel.herstellerKm, oel.herstellerMonate) : null) : bauen(30000, 24);
  const perKm = prognose(eig.km).datum;
  return { hersteller: her, eigen: eig, prognose: perKm && perKm < eig.datum ? perKm : eig.datum };
}
/* Vom Fahrzeug selbst gemeldete Fälligkeit (VW Group EU Data Act, siehe
   einstellungen.py) - ergänzt die App-eigene, aus dem Servicebuch berechnete
   Prognose (intervalle()/terminText() oben), ersetzt sie aber bewusst nicht:
   die Werkscomputer-Logik dahinter ist nicht bekannt und könnte von den
   selbst eingestellten Intervallen abweichen. null, wenn das Fahrzeug dazu
   nichts meldet (Sensor fehlt/nicht verfügbar). */
function fahrzeugMeldungRoh(art) {
  if (art === "Ölwechsel") return { ts: CAR.oelwechselFaelligTs, km: CAR.oelwechselFaelligKm };
  if (art === "Inspektion") return { ts: CAR.inspektionFaelligTs, km: CAR.inspektionFaelligKm };
  return { ts: null, km: null };
}

/* App-eigene Ölwechsel-Prognose, eigenständig statt über die allgemeine
   tagesleistung()/prognose()-Mischrechnung (die für Ölwechsel zu träge ist,
   weil sie den langfristigen Durchschnitt seit dem allerersten Servicebuch-
   eintrag mit einfließen lässt): die Berechnung beginnt bewusst komplett neu
   ab dem Datum des letzten tatsächlich durchgeführten Ölwechsels.

   Logik (wie vorgegeben): km und Tage seit dem letzten Ölwechsel ergeben die
   aktuelle Fahrleistung pro Tag; hochgerechnet auf die restlichen km bis zum
   eingestellten Intervall (Kundeneigene Einstellung, siehe CAR.oel) ergibt
   das ein Datum - gedeckelt auf spätestens das Intervall-Zeitlimit (z. B. 1
   Jahr) nach dem letzten Ölwechsel, falls seither wenig gefahren wurde. */
function oelwechselPrognose() {
  if (!CAR.odoBekannt) return null;
  const letzter = letzterEintrag("öl");
  if (!letzter) return null;
  const oel = CAR.oel;
  const intervallKm = oel.modus === "hersteller" ? oel.herstellerKm : Math.min(oel.km, oel.herstellerKm);
  const intervallMonate = oel.modus === "hersteller" ? oel.herstellerMonate : Math.min(oel.monate, oel.herstellerMonate);
  const zielKm = letzter.km + intervallKm;
  const restKm = Math.max(0, zielKm - CAR.odo);
  const kappDatum = monatePlus(letzter.datum, intervallMonate);
  let datum;
  if (restKm <= 0) {
    datum = heute();
  } else {
    const tageSeit = (heute() - dat(letzter.datum)) / dTag;
    const kmSeit = CAR.odo - letzter.km;
    if (tageSeit <= 0 || kmSeit <= 0) {
      // Noch keine belastbare Fahrleistung seit dem Ölwechsel bekannt (z. B.
      // direkt danach) - ohne Rate keine seriöse Km-Prognose, also das
      // Zeitlimit als einzig belastbare Grenze zeigen.
      datum = kappDatum;
    } else {
      const kmProTagSeitOel = kmSeit / tageSeit;
      const tageBisZiel = restKm / kmProTagSeitOel;
      const berechnet = new Date(heute().getTime() + tageBisZiel * dTag);
      datum = berechnet < kappDatum ? berechnet : kappDatum;
    }
  }
  return { km: zielKm, restKm, datum };
}

function terminText(t) {
  const restKm = t.km !== null && CAR.odoBekannt ? t.km - CAR.odo : null;
  const restKmAbs = restKm !== null ? Math.abs(restKm) : null;
  const haupt = t.zuerst === "km" && restKm !== null ? "in " + de(restKmAbs) + " km" : dedat(t.datum);
  const neben = t.zuerst === "km" ? "voraussichtlich " + dedat(t.kmDatum) + " · " + dedat(t.datum)
    : (t.km !== null ? "in " + de(restKmAbs) + " km" : (t.art === "Hauptuntersuchung" ? "" : "aus dem Servicebuch fortgeschrieben"));
  return { haupt, neben, restKm };
}

/* Zählt seit LETZTES_UPDATE_TS (echter Zeitpunkt des letzten Fahrzeugstatus-
   Updates vom Backend, siehe datenLaden()) selbständig hoch und schaltet
   dabei die Einheit um - Sek. bis 60, dann Min. bis 60, dann Std. bis 24,
   dann Tage. */
function standAlterText() {
  if (LETZTES_UPDATE_TS == null) return "unbekannt";
  const sekunden = Math.max(0, Math.floor((Date.now() - LETZTES_UPDATE_TS) / 1000));
  if (sekunden < 60) return `vor ${sekunden} Sek.`;
  const minuten = Math.floor(sekunden / 60);
  if (minuten < 60) return `vor ${minuten} Min.`;
  const stunden = Math.floor(minuten / 60);
  if (stunden < 24) return `vor ${stunden} Std.`;
  return `vor ${Math.floor(stunden / 24)} Tage`;
}
function standAlterTicken() {
  if (!ROOT || !CAR) return;
  const sy = $("#sync span"); if (sy) sy.textContent = standAlterText();
}

function vHome() {
  const g = CAR.gesichert;
  const statusText = g === null ? "Status unbekannt" : g ? "Sicher abgestellt" : "Nicht sicher abgestellt";
  const statusDot = g === null ? "warn" : g ? "" : "bad";
  const pct = CAR.tankPct;
  const balkenFarbe = pct <= 10 ? "var(--bad)" : pct <= 25 ? "var(--warn)" : "var(--ok)";
  const liter = CONFIG.tankvolumen ? Math.round((CONFIG.tankvolumen * pct) / 100) : null;
  const t = naechsterTermin();
  /* C2/C5/D1: Reihenfolge der myAudi-App - randloses Fahrzeugbild, Name und
     Status, große Reichweite mit farbigem Balken, dann Aktionskacheln. Die
     Kacheln führen zu Ansichten statt zu Fernsteuerung, die das Panel bewusst
     nicht kann. */
  return `
  <div class="szene bleed ${szeneKlasse(startIndex())}"><div class="carbox">${carImg("fest")}</div></div>
  <div class="tile flat" style="margin-bottom:20px">
    ${badge()}
    <span class="label" style="margin-top:10px">${esc(CONFIG.kennzeichen)}</span>
    <button class="status" data-go="sicherheit:x">
      <span class="dot ${statusDot}"></span>
      <div><div class="t">${statusText}</div></div>
      <svg class="chev" viewBox="0 0 6 10" style="margin-left:auto"><polyline points="1,1 5,5 1,9"/></svg>
    </button>
  </div>
  <div class="tile flat" style="margin-bottom:22px">
    <div class="tankzeile">
      <div class="fig" style="font-size:60px">${CAR.reichweite != null ? de(CAR.reichweite) : "–"}<span class="unit">km</span></div>
      <span class="pct">${pct} %</span>
    </div>
    <div class="balken"><i style="width:${pct}%;background:${balkenFarbe}"></i></div>
    <span class="label" style="margin-top:12px">Reichweite${liter != null ? ` · noch etwa ${de(liter)} l im Tank` : ""}</span>
  </div>
  <div class="duo">
    <button class="tile tilebtn" data-go="sicherheit:x"><span class="label">Fahrzeugstatus</span>
      <div class="wert" style="margin-top:10px;color:var(--fg);font-size:15px">${g === null ? "unbekannt" : g ? "gesichert" : "prüfen"}</div></button>
    <button class="tile tilebtn" data-go="service:x"><span class="label">Service</span>
      <div class="wert" style="margin-top:10px;color:var(--fg);font-size:15px">${t ? esc(t.art) : "kein Eintrag"}</div></button>
    <button class="tile tilebtn" data-go="reifen:x"><span class="label">Reifen</span>
      <div class="wert" style="margin-top:10px;color:var(--fg);font-size:15px">${esc(CAR.reifen.aktiv)}</div></button>
    <button class="tile tilebtn" data-go="fuel:x"><span class="label">Tanken</span>
      <div class="wert" style="margin-top:10px;color:var(--fg);font-size:15px">${FILLS.length ? de(FILLS.length) + " Vorgänge" : "noch keiner"}</div></button>
  </div>
  <div class="duo">
    <div class="tile"><span class="label">Kilometerstand</span>
      <div class="fig" style="font-size:29px;margin-top:14px">${CAR.odoBekannt ? de(CAR.odo) : "–"}<span class="unit">km</span></div></div>
    ${(() => { if (!t) return `<div class="tile"><span class="label">Service</span><div class="wert" style="margin-top:14px">kein Eintrag</div></div>`;
      const artLabel = t.art === "Hauptuntersuchung" ? "Haupt-<br>untersuchung" : esc(t.art);
      if (t.art === "Ölwechsel") {
        const restKm = t.km !== null && CAR.odoBekannt ? Math.abs(t.km - CAR.odo) : null;
        const datum = t.kmDatum || t.datum;
        const teile = [];
        if (restKm !== null) teile.push(de(restKm / 1000, 1) + " tkm");
        if (datum) teile.push(mmjj(datum));
        return `<div class="tile"><span class="label">${artLabel}</span>
          <div class="fig" style="font-size:29px;margin-top:14px">${teile.join("/ ")}</div></div>`;
      }
      const teileHU = [];
      if (t.zuerst === "km") {
        teileHU.push(de(t.km - CAR.odo) + " km");
        if (t.kmDatum) teileHU.push(mmjj(t.kmDatum));
      } else {
        teileHU.push(mmjj(t.datum));
        if (t.km !== null && CAR.odoBekannt) teileHU.push(de(Math.abs(t.km - CAR.odo)) + " km");
      }
      return `<div class="tile"><span class="label">${artLabel}</span>
        <div class="fig" style="font-size:29px;margin-top:14px">${teileHU.join("/ ")}</div></div>`; })()}
  </div>
  ${teaser()}`;
}

function vSicherheit() {
  const eintraege = CAR.sicherheitscheck;
  const g = CAR.gesichert;
  const offen = eintraege.filter((e) => e.ok === false).length;
  const unbekannt = eintraege.filter((e) => e.ok === null).length;
  const sammelText = g === null
    ? `${unbekannt} von ${eintraege.length} Punkten liefern derzeit keinen Wert`
    : g ? "Fahrzeug ist sicher abgestellt" : `${offen} Punkt${offen === 1 ? "" : "e"} prüfen`;
  /* C6: Draufsicht oben, grüne Sammelmeldung, dann die Prüfliste - dieselbe
     Reihenfolge wie der Fahrzeugstatus in der myAudi-App. */
  return `
  <div class="szene bleed"><div class="carbox">${bildMitPlatzhalter(`/local/bilder/draufsicht.webp?v=${bildVersion}`, "draufsicht.webp", "Draufsicht", "carfix klein")}</div></div>
  <div class="tile" style="display:flex;align-items:center;gap:12px;padding:16px 20px">
    <span class="dot ${g === null ? "warn" : g ? "" : "bad"}"></span>
    <span style="font-size:15px;color:var(--fg)">${esc(sammelText)}</span>
  </div>
  <div class="tile"><span class="label">Geprüfte Punkte</span>
    <dl class="rows" style="margin-top:10px">
      ${eintraege.length ? eintraege.map((e, i, arr) => `<div class="row" ${i === arr.length - 1 ? 'style="border-bottom:0"' : ""}>
        <dt>${esc(e.label)}</dt>
        <dd style="display:flex;align-items:center;justify-content:flex-end;gap:8px">
          <span class="dot ${e.ok === null ? "warn" : e.ok ? "" : "bad"}"></span>
          ${e.ok === null ? "unbekannt" : e.ok ? "in Ordnung" : "prüfen"}
        </dd>
      </div>`).join("") : `<div class="row" style="border-bottom:0"><dt style="color:var(--fg3)">Keine Daten verfügbar</dt></div>`}
    </dl>
  </div>`;
}

function vAudi() {
  const r = CAR.reifen, a = r[r.aktiv.toLowerCase()];
  return `
  <div class="szene ${szeneKlasse(bild)}" id="szene"><div class="carbox">${carImg("galerie")}</div></div>
  <div class="tile flat" style="margin:14px 0 16px">
    ${badge()}
    <span class="label" style="margin-top:12px">${esc(CONFIG.kennzeichen)}</span>
  </div>
  <button class="tile tilebtn" data-go="ident:x">
    <span class="label">Fahrzeug</span>
    <svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
    <dl class="rows" style="margin-top:10px">
      <div class="row"><dt>Fahrgestellnummer</dt><dd>${esc(CAR.fin)}</dd></div>
      <div class="row"><dt>Details</dt><dd>${esc(CAR.details)}</dd></div>
      <div class="row"><dt>Erstzulassung</dt><dd>${esc(CAR.erstzulassung)}</dd></div>
      <div class="row"><dt>Ausführung</dt><dd>${CONFIG.ausfuehrung ? esc(CONFIG.ausfuehrung) : "noch offen"}</dd></div>
      <div class="row" style="border-bottom:0"><dt>Ausstattung</dt>
        <dd>${CAR.ausstattung.reduce((n, g) => n + g.sonder.length, 0)} Positionen</dd></div>
    </dl></button>
  <div class="tile"><span class="label">Zustand</span><dl class="rows">
    <div class="row"><dt>Kilometerstand</dt><dd>${CAR.odoBekannt ? de(CAR.odo) + " km" : "unbekannt"}</dd></div>
    <div class="row"><dt>Tankfüllung</dt><dd>${de(CONFIG.tankvolumen * CAR.tankPct / 100, 1)} l<small>${CAR.tankPct} % von ${CONFIG.tankvolumen} l</small></dd></div>
    <div class="row" style="border-bottom:0;cursor:pointer" data-go="battverlauf:x"><dt>Batteriespannung</dt>
      <dd style="display:flex;align-items:center;gap:6px;justify-content:flex-end">${CAR.batteriespannung != null ? de(CAR.batteriespannung, 1) + " V" : "unbekannt"}
        <svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></dd></div></dl></div>
  <button class="tile tilebtn" data-go="service:x">
    <span class="label">Service</span>
    <svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
    <dl class="rows" style="margin-top:12px">
      ${termine().map((t, i, arr) => {
        const letzte = i === arr.length - 1 ? 'style="border-bottom:0"' : "";
        // Hauptuntersuchung meldet das Fahrzeug nie (§9-Fallback bleibt hier
        // die einzige Quelle) - für Ölwechsel/Inspektion zeigt diese Kachel
        // bewusst NUR den realen, vom Fahrzeug gemeldeten Wert an (kein
        // Forecast, kein "Fahrzeug meldet"-Präfix mehr); fehlt die Meldung
        // (Sensor nicht verfügbar), fällt sie auf dieselbe App-eigene
        // Servicebuch-Schätzung zurück wie Hauptuntersuchung.
        const fm = t.art === "Hauptuntersuchung" ? { ts: null, km: null } : fahrzeugMeldungRoh(t.art);
        if (fm.ts == null && fm.km == null) {
          const x = terminText(t);
          return `<div class="row" ${letzte}>
            <dt>${t.art}</dt><dd>${x.haupt}${x.neben ? `<small style="text-align:left">${x.neben}</small>` : ""}</dd></div>`;
        }
        const haupt = fm.ts ? dedat(new Date(fm.ts)) : "";
        const neben = fm.km != null ? "in " + de(fm.km) + " km" : "";
        // Zusätzlich zur realen Meldung bei Ölwechsel klein die App-eigene
        // Prognose (Servicebuch + tatsächliche Fahrleistung seit dem letzten
        // Wechsel, siehe oelwechselPrognose()) - bei Inspektion bewusst nicht,
        // wie gefordert. Steht links unter dem Kachel-Titel statt rechts bei
        // "am"/"in", die beide gleichwertig (gleiche Schriftgröße wie "am")
        // untereinander in der rechten Spalte stehen.
        const eigen = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
        return `<div class="row" ${letzte}>
          <dt>${t.art}${eigen ? `<br><span style="color:var(--fg3);font-size:11px">${de(eigen.restKm)} km, voraussichtlich am ${dedat(eigen.datum)}</span>` : ""}</dt>
          <dd>${haupt}${neben ? `<span style="display:block">${neben}</span>` : ""}</dd></div>`;
      }).join("")}
    </dl></button>
  <button class="tile tilebtn" data-go="vers:x">
    <span class="label">Versicherung/Steuer</span>
    <svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
    <dl class="rows" style="margin-top:10px">
      <div class="row"><dt>Versicherung</dt><dd>${eur(CAR.versicherung.beitrag)} €<small>im Jahr</small></dd></div>
      <div class="row"><dt>Kfz-Steuer</dt><dd>${eur(CAR.steuer.betrag)} €<small>${esc(CAR.steuer.zeitraum)}</small></dd></div>
      <div class="row" style="border-bottom:0"><dt>Zusammen</dt>
        <dd>${eur(CAR.versicherung.beitrag + CAR.steuer.betrag)} €<small>feste Kosten im Jahr</small></dd></div>
    </dl></button>
  <button class="tile tilebtn" data-go="reifen:x">
    <span class="label">Reifen · ${r.aktiv} montiert</span>
    <svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
    <dl class="rows" style="margin-top:10px">
      <div class="row"><dt>${esc(a.marke)} ${esc(a.modell)}</dt><dd>${esc(a.mass)}</dd></div>
      <div class="row"><dt>Laufleistung</dt><dd>${a.km != null ? de(a.km) + " km" : "unbekannt"}</dd></div>
      <div class="row" style="border-bottom:0"><dt>Wechseltermin</dt>
        <dd>${dedat(new Date(r.wechsel.datum))}<small>${r.wechsel.vereinbart ? "vereinbart" : "offen"}</small></dd></div>
    </dl></button>`;
}

/* ------------------------------------------------- Batteriespannungs-Verlauf
   Handgebautes Diagramm ohne externe Bibliothek (kein Bundler im Projekt,
   siehe INSTALL.md) - Zoom per Zwei-Finger-Pinch/Mausrad, Verschieben per
   Ziehen mit einem Finger, Antippen wählt den nächstgelegenen Messpunkt.
   bvZeichnen() aktualisiert nur die SVG/Tooltip-Inhalte (kein render()), damit
   Zoom/Pan flüssig bleiben. */
let bvVollDomain = null, bvDomain = null, bvPunkte = [], bvAusgewaehlt = null;
const bvZeiger = new Map(); // pointerId -> {x,y}, für Pinch (2 Finger) vs. Pan (1 Finger)
let bvGestStart = null;     // {type:"pan"|"pinch", ...Ausgangswerte der Geste}
let bvTapKandidat = null, bvBewegtSeit = false;
const BV_W = 320, BV_H = 170, BV_ML = 40, BV_MR = 10, BV_MT = 10, BV_MB = 22;
const kurzdat = (d) => d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "2-digit" });

/* Referenzwerte für AGM-Starterbatterien (12V), recherchiert u. a. bei
   aboutdarwin.com/agm-battery-voltage-charts und brsbattery.com - eine volle
   AGM-Batterie liegt in Ruhe (min. 2h ohne Last/Ladung) bei 12,8-13,0 V,
   50 % Ladezustand bei 12,2-12,3 V, unter 11,8 V gilt sie als kritisch
   entladen. Der Ladealternator liefert im Betrieb üblicherweise 13,5-14,8 V -
   deutlich über dem Ruhespannungs-Maximum.

   Das Backend (batterieverlauf.py) zeichnet pro Tag min UND max roh auf,
   ohne Filterung - der Sensor meldet künftig mehrfach pro Stunde (aktive
   Fahrt). AGM_RUHE_MAX_V wird deshalb erst hier beim Anzeigen angewendet,
   und zwar auf den Tagesminimalwert: der fällt typischerweise auf den
   Moment kurz nach dem Start, bevor der Alternator hochfährt, und ist damit
   die für den Ladezustand aussagekräftige Größe. Tage, an denen selbst das
   Minimum über der Grenze liegt (Fahrzeug nie im Ruhezustand beobachtet),
   tauchen entsprechend nicht in der Ladezustands-Statistik auf. */
const AGM_RUHE_MAX_V = 13.2;
const BV_SOC_TABELLE = [
  [10.5, 0], [11.8, 10], [11.9, 20], [12.0, 30], [12.1, 40],
  [12.2, 50], [12.4, 60], [12.5, 70], [12.6, 80], [12.7, 90], [12.8, 100],
];
function bvSocProzent(v) {
  const t = BV_SOC_TABELLE;
  if (v <= t[0][0]) return 0;
  if (v >= t[t.length - 1][0]) return 100;
  for (let i = 0; i < t.length - 1; i++) {
    const [v0, s0] = t[i], [v1, s1] = t[i + 1];
    if (v >= v0 && v <= v1) return Math.round(s0 + ((s1 - s0) * (v - v0)) / (v1 - v0));
  }
  return 100;
}
function bvSocLabel(soc) {
  return soc >= 90 ? "Sehr gut" : soc >= 70 ? "Gut" : soc >= 50 ? "Mittel" : soc >= 30 ? "Schwach" : "Kritisch";
}
// Rot (leer/kritisch) -> Gelb (50 %) -> Grün (voll) entlang derselben drei
// Eckwerte wie die SOC-Tabelle oben (11,6 V/0 %, 12,2 V/50 %, 12,8 V/100 %) -
// dieselben Statusfarben wie beim Sicherheitscheck (--bad/--warn/--ok),
// damit die Skala zum Rest der App passt statt eigene Farben zu erfinden.
function bvHexZuRgb(hex) {
  const h = hex.trim().replace("#", "");
  return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
}
function bvMischen(a, b, t) { return a.map((x, i) => Math.round(x + (b[i] - x) * t)); }
function bvFarbstufen() {
  const stil = getComputedStyle(HOST);
  return {
    bad: bvHexZuRgb(stil.getPropertyValue("--bad")),
    warn: bvHexZuRgb(stil.getPropertyValue("--warn")),
    ok: bvHexZuRgb(stil.getPropertyValue("--ok")),
  };
}
function bvFarbe(v, stufen) {
  let rgb;
  if (v <= 11.6) rgb = stufen.bad;
  else if (v <= 12.2) rgb = bvMischen(stufen.bad, stufen.warn, (v - 11.6) / (12.2 - 11.6));
  else if (v <= 12.8) rgb = bvMischen(stufen.warn, stufen.ok, (v - 12.2) / (12.8 - 12.2));
  else rgb = stufen.ok;
  return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
}

/* Gesundheitszustand (SOH) ist mit reiner Ruhespannungs-Beobachtung nur eine
   Näherung, keine Kapazitätsmessung (ein Lasttest wäre dafür nötig - siehe
   Recherche, u. a. battlebornbatteries.com/blogs/articles/battery-state-of-
   health-soh) - deshalb hier bewusst als Trend über die Zeit ausgedrückt,
   nicht als exakter Prozentwert: Durchschnitt der ersten vs. letzten Phase
   der Aufzeichnung, in einem für alternde AGM-Batterien üblichen Rahmen
   eingeordnet (übliche Lebensdauer 4-6 Jahre). */
function bvSohBewertung() {
  const TAG_MS = 86400000;
  if (bvPunkte.length < 6) return { status: null, text: "Noch nicht genug Messwerte für eine Trendbewertung." };
  const spanneTage = (bvPunkte[bvPunkte.length - 1].t - bvPunkte[0].t) / TAG_MS;
  if (spanneTage < 60) return { status: null, text: "Noch zu kurzer Zeitraum für eine Trendbewertung (mind. ~2 Monate nötig)." };
  const fensterMs = Math.min(90, spanneTage / 3) * TAG_MS;
  const alt = bvPunkte.filter((p) => p.t <= bvPunkte[0].t + fensterMs);
  const neu = bvPunkte.filter((p) => p.t >= bvPunkte[bvPunkte.length - 1].t - fensterMs);
  const altD = sum(alt, (p) => p.v) / alt.length, neuD = sum(neu, (p) => p.v) / neu.length;
  const delta = neuD - altD;
  const status = delta >= -0.05 ? "ok" : delta >= -0.15 ? "warn" : "bad";
  const text = delta >= -0.05
    ? "Stabil - kein auffälliger Rückgang der Ruhespannung."
    : delta >= -0.15
      ? "Leicht sinkend - im für alternde AGM-Batterien üblichen Rahmen."
      : "Deutlicher Rückgang - kann auf eine alternde oder geschwächte Batterie hindeuten.";
  return { status, text, delta };
}

function vBatterieverlauf() {
  // Der Diagrammpunkt und die Kurve bestehen ausschließlich aus dem
  // Tagesminimalwert (siehe AGM_RUHE_MAX_V-Kommentar oben) - min_ts ist der
  // echte Zeitstempel genau dieser Einzelmessung, nicht ein fiktiver
  // Tagesmittelpunkt, damit Antippen Datum UND Uhrzeit zeigen kann.
  bvPunkte = BATTERIEVERLAUF.filter((e) => e.min != null && e.min <= AGM_RUHE_MAX_V)
    .map((e) => ({ t: new Date(e.min_ts).getTime(), v: e.min, vMin: e.min, vMax: e.max, minTs: e.min_ts, datum: e.datum }))
    .sort((a, b) => a.t - b.t);
  bvAusgewaehlt = null; bvZeiger.clear(); bvGestStart = null;

  const genug = bvPunkte.length >= 2;
  bvVollDomain = genug ? [bvPunkte[0].t, bvPunkte[bvPunkte.length - 1].t] : null;
  bvDomain = genug ? [...bvVollDomain] : null;

  const stufen = bvFarbstufen();
  const letzter = bvPunkte.length ? bvPunkte[bvPunkte.length - 1] : null;
  const soc = letzter ? bvSocProzent(letzter.v) : null;
  const soh = bvSohBewertung();
  const punktFarbe = (status) => (status === "ok" ? "var(--ok)" : status === "warn" ? "var(--warn)" : status === "bad" ? "var(--bad)" : "var(--fg3)");
  const leerText = BATTERIEVERLAUF.length === 0
    ? "Noch keine Messwerte aufgezeichnet - ein Eintrag entsteht automatisch, sobald eine Spannungsquelle eingerichtet ist."
    : "Noch zu wenige Messwerte für einen Verlauf.";

  return `
  <div class="tile">
    <span class="label">Verlauf</span>
    ${genug ? `
    <div id="bvChart" style="margin-top:14px;touch-action:none;position:relative">
      <svg id="bvSvg" viewBox="0 0 ${BV_W} ${BV_H}" preserveAspectRatio="none" style="width:100%;height:170px;display:block"></svg>
    </div>
    <div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px">
      <span class="label" id="bvRange" style="margin-top:0"></span>
      <button class="aktion" data-bvreset style="padding:8px 16px;font-size:11px;width:auto;margin-top:0">Zoom zurücksetzen</button>
    </div>
    <div id="bvTooltip" style="display:none;margin-top:12px;font-size:13px;color:var(--fg2)"></div>
    <span class="label" style="margin-top:16px">Ziehen zum Verschieben, mit zwei Fingern oder dem Mausrad
      zoomen, auf einen Punkt tippen für Datum, Uhrzeit sowie Min-/Max-Spannung</span>
    ` : `
    <span class="label" style="margin-top:14px;color:var(--fg3)">${leerText}</span>
    `}
    <span class="label" style="margin-top:16px">Farbskala Ladezustand (AGM, Ruhespannung)</span>
    <div style="margin-top:8px;height:8px;border-radius:var(--r-pill);
      background:linear-gradient(to right, ${punktFarbe("bad")}, ${punktFarbe("warn")}, ${punktFarbe("ok")})"></div>
    <div style="display:flex;justify-content:space-between;margin-top:6px">
      <small style="color:var(--fg3)">11,6 V · leer</small>
      <small style="color:var(--fg3)">12,2 V · 50 %</small>
      <small style="color:var(--fg3)">12,8 V · voll</small>
    </div>
  </div>
  <div class="tile">
    <span class="label">Bewertung</span>
    <dl class="rows" style="margin-top:10px">
      <div class="row"><dt>Ladezustand (SOC)</dt>
        <dd style="display:flex;align-items:center;gap:8px;justify-content:flex-end">
          ${letzter
            ? `<span class="dot" style="background:${bvFarbe(letzter.v, stufen)}"></span>${de(letzter.v, 1)} V · ${soc} % · ${bvSocLabel(soc)}`
            : `<span style="color:var(--fg3)">unbekannt</span>`}</dd></div>
      <div class="row" style="border-bottom:0"><dt>Gesundheitszustand (SOH-Trend)</dt>
        <dd style="display:flex;align-items:center;gap:8px;justify-content:flex-end">
          ${soh.status
            ? `<span class="dot" style="background:${punktFarbe(soh.status)}"></span>${(soh.delta >= 0 ? "+" : "") + de(soh.delta, 2)} V`
            : `<span style="color:var(--fg3)">-</span>`}</dd></div>
    </dl>
    <span style="display:block;margin-top:10px;color:var(--fg2);font-size:13.5px;line-height:1.6">${soh.text}</span>
    <span class="label" style="margin-top:12px">Näherung auf Basis der täglich niedrigsten gemessenen Spannung (aussagekräftig für die
      Entladung), keine Kapazitätsmessung - Tageswerte über ${de(AGM_RUHE_MAX_V, 1)} V (Ladevorgang während der Fahrt) fließen nicht ein.
      Für eine verlässliche Aussage ist ein Lasttest an der Batterie nötig.</span>
  </div>`;
}

function bvSkalaX(t) { return BV_ML + ((t - bvDomain[0]) / (bvDomain[1] - bvDomain[0])) * (BV_W - BV_ML - BV_MR); }
function bvSkalaY(v, yMin, yMax) { return BV_H - BV_MB - ((v - yMin) / (yMax - yMin)) * (BV_H - BV_MT - BV_MB); }
function bvDomainKlemmen(domain) {
  let [a, b] = domain;
  const volle = bvVollDomain[1] - bvVollDomain[0];
  let breite = Math.min(b - a, volle);
  breite = Math.max(breite, volle * 0.02);
  if (a < bvVollDomain[0]) { a = bvVollDomain[0]; b = a + breite; }
  if (b > bvVollDomain[1]) { b = bvVollDomain[1]; a = b - breite; }
  return [a, b];
}
function bvPlotRahmen(rect) {
  return { linksPx: rect.left + rect.width * (BV_ML / BV_W), breitePx: rect.width * ((BV_W - BV_ML - BV_MR) / BV_W) };
}

function bvZeichnen() {
  const svg = $("#bvSvg");
  if (!svg || !bvDomain) return;
  const sichtbar = bvPunkte.filter((p) => p.t >= bvDomain[0] && p.t <= bvDomain[1]);
  const basis = sichtbar.length >= 2 ? sichtbar : bvPunkte;
  let yMin = Math.min(...basis.map((p) => p.v)), yMax = Math.max(...basis.map((p) => p.v));
  if (yMax - yMin < 0.4) { const mitte = (yMax + yMin) / 2; yMin = mitte - 0.2; yMax = mitte + 0.2; }
  const pad = (yMax - yMin) * 0.15; yMin -= pad; yMax += pad;

  // etwas über den sichtbaren Bereich hinaus zeichnen, damit die Linie am
  // Rand nicht mitten in der Luft abbricht, sondern zum nächsten (Rand-)Punkt
  // weiterläuft
  const spanne = bvDomain[1] - bvDomain[0];
  const rahmen = bvPunkte.filter((p) => p.t >= bvDomain[0] - spanne * 0.05 && p.t <= bvDomain[1] + spanne * 0.05);
  const linie = rahmen.map((p) => `${bvSkalaX(p.t).toFixed(1)},${bvSkalaY(p.v, yMin, yMax).toFixed(1)}`).join(" ");

  const gitter = [0, 1, 2, 3].map((i) => {
    const v = yMin + ((yMax - yMin) * i) / 3, y = bvSkalaY(v, yMin, yMax);
    return `<line x1="${BV_ML}" y1="${y.toFixed(1)}" x2="${BV_W - BV_MR}" y2="${y.toFixed(1)}" stroke="var(--line)" stroke-width="0.5"/>
      <text x="${BV_ML - 6}" y="${(y + 3).toFixed(1)}" font-size="8" fill="var(--fg3)" text-anchor="end">${de(v, 1)}</text>`;
  }).join("");
  const xLabels = [0, 0.5, 1].map((f) => {
    const x = BV_ML + f * (BV_W - BV_ML - BV_MR);
    const t = bvDomain[0] + f * spanne;
    return `<text x="${x.toFixed(1)}" y="${BV_H - 6}" font-size="8" fill="var(--fg3)"
      text-anchor="${f === 0 ? "start" : f === 1 ? "end" : "middle"}">${kurzdat(new Date(t))}</text>`;
  }).join("");
  const stufen = bvFarbstufen();
  const kreise = rahmen.map((p) => {
    const idx = bvPunkte.indexOf(p);
    const gewaehlt = idx === bvAusgewaehlt;
    return `<circle cx="${bvSkalaX(p.t).toFixed(1)}" cy="${bvSkalaY(p.v, yMin, yMax).toFixed(1)}"
      r="${gewaehlt ? 4.5 : 2.4}" fill="${bvFarbe(p.v, stufen)}"
      ${gewaehlt ? 'stroke="var(--fg)" stroke-width="1"' : ""}/>`;
  }).join("");

  svg.innerHTML = `${gitter}<polyline points="${linie}" fill="none" stroke="var(--fg)" stroke-width="1.5"/>${kreise}${xLabels}`;

  const rangeEl = $("#bvRange");
  if (rangeEl) rangeEl.textContent = `${kurzdat(new Date(bvDomain[0]))} – ${kurzdat(new Date(bvDomain[1]))}`;
  const tip = $("#bvTooltip");
  if (tip) {
    if (bvAusgewaehlt != null) {
      const p = bvPunkte[bvAusgewaehlt];
      const zeit = new Date(p.minTs);
      tip.style.display = "block";
      tip.textContent = `${dedat(zeit)} · ${zeit.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })} Uhr · Min ${de(p.vMin, 1)} V · Max ${de(p.vMax, 1)} V`;
    } else {
      tip.style.display = "none";
    }
  }
}

function bvNaechsterPunkt(clientX) {
  const svg = $("#bvSvg"); if (!svg) return null;
  const rect = svg.getBoundingClientRect();
  const zeit = bvDomain[0] + ((clientX - bvPlotRahmen(rect).linksPx) / bvPlotRahmen(rect).breitePx) * (bvDomain[1] - bvDomain[0]);
  let beste = null, besterAbstand = Infinity;
  bvPunkte.forEach((p, i) => { const a = Math.abs(p.t - zeit); if (a < besterAbstand) { besterAbstand = a; beste = i; } });
  return beste;
}

let svAuswahl = 0;
function vService() {
  const sv = CAR.service;
  return `
  <div class="tile"><span class="label">Anstehende Termine</span>
    <dl class="rows" style="margin-top:10px">
      ${termine().map((t, i, arr) => {
        const letzte = i === arr.length - 1 ? 'style="border-bottom:0"' : "";
        const iv = intervalle(t);
        if (!iv) return `<div class="row" ${letzte}>
          <dt>${t.art}</dt><dd style="color:var(--fg3)">kein Eintrag im Servicebuch</dd></div>`;
        const anzeige = iv.hersteller || iv.eigen;
        const fm = fahrzeugMeldungRoh(t.art);
        const fmDatum = fm.ts ? dedat(new Date(fm.ts)) : null;
        // Forecast-Zeile nur bei Ölwechsel (Kundeneigenes Intervall aus
        // "Einrichten", eigenständige Reset-nach-Service-Logik, siehe
        // oelwechselPrognose()) - bei Inspektion und Hauptuntersuchung
        // bewusst keine Forecast-Zeile mehr.
        const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
        return `<div class="row" ${letzte}>
          <dt>${t.art}
            ${t.vereinbart ? `<br><span style="color:var(--red);font-size:11px">Werkstatt am ${dedat(new Date(t.vereinbart))}</span>` : ""}
          </dt>
          <dd>
            ${iv.hersteller ? `<span class="label" style="margin-bottom:6px">Herstellervorgabe</span>` : ""}
            ${fm.km != null || fmDatum ? `
              <span class="werte">${fm.km != null ? de(fm.km) + " km" : ""}${fm.km != null && fmDatum ? " / " : ""}${fmDatum || ""}</span>
              ${eigenOel ? `<small style="text-align:left">${de(eigenOel.restKm)} km, voraussichtlich am ${dedat(eigenOel.datum)}</small>` : ""}
            ` : `
              <span class="werte">${eigenOel ? de(eigenOel.restKm) + " km · " + dedat(eigenOel.datum) : (anzeige.km !== null ? de(anzeige.km) + " km · " : "") + dedat(anzeige.datum)}</span>
            `}
          </dd>
        </div>`; }).join("")}
    </dl>
  </div>
  <div class="tile"><span class="label">Termin vereinbaren</span>
    ${(() => { const ts = termine(), t = ts[Math.min(svAuswahl, ts.length - 1)];
      const vorschlag = CAR.service.vereinbart[t.art] || (t.ziel ? t.ziel.toISOString().slice(0, 10) : "");
      return `
      <div class="feld"><label for="svArt">Wofür</label>
        <select id="svArt" style="width:132px" data-svsel>${ts.map((x, i) => `<option value="${i}" ${i === svAuswahl ? "selected" : ""}>${x.art}</option>`).join("")}</select></div>
      <div class="feld" style="border-bottom:0"><label for="svDat">Werkstatttermin am</label>
        <input id="svDat" type="date" style="width:132px" value="${vorschlag}" data-vereinbart="${esc(t.art)}"></div>
      <button class="aktion" data-ics="service">In den Kalender übernehmen</button>
      ${CAR.service.vereinbart[t.art] ? `<button class="aktion" data-loeschen="${esc(t.art)}">Termin verwerfen</button>` : ""}`; })()}
  </div>
  <div class="tile">
    <span class="label">Autohaus</span>
    <button class="zahnrad" data-go="werkstatt:x" aria-label="Angaben ändern">
      <svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 2h4.2v2.84c.37.13.75.27 1.11.44L16.7 3.3l3 3.01-1.98 1.99c.17.36.31.74.44 1.11H21v4.3h-2.84c-.13.37-.27.75-.44 1.11l2 2-3.03 2.89-1.98-1.98c-.36.17-.74.31-1.11.44V21H9.4v-2.84a9 9 0 0 1-1.11-.44L6.3 19.7l-3-3.01 1.98-1.99a9 9 0 0 1-.44-1.11H2V9.4h2.84c.13-.37.27-.75.44-1.11L3.3 6.3l3.01-3L8.3 5.28c.36-.17.74-.31 1.11-.44V2Zm1 1v2.56l-.34.11c-.61.2-1.16.4-1.6.66l-.34.2L6.3 4.71 4.7 6.3l1.83 1.82-.2.34c-.27.44-.45.99-.66 1.6l-.11.34H3v2.2h2.56l.11.34c.2.61.4 1.16.66 1.6l.2.34-1.82 1.82 1.59 1.6 1.82-1.83.34.2c.44.27.99.45 1.6.66l.34.11V20h2.2v-2.56l.34-.11a8.3 8.3 0 0 0 1.6-.66l.34-.2 1.83 1.83 1.57-1.5-1.81-1.82.2-.34c.27-.44.45-.99.66-1.6l.11-.34H20v-2.3h-2.56l-.11-.34a8.2 8.2 0 0 0-.66-1.6l-.2-.34 1.82-1.82-1.59-1.6-1.82 1.83-.34-.2a8.2 8.2 0 0 0-1.6-.66l-.34-.11V3zm-2.9 8.5c0-2.18 1.82-4 4-4s4 1.82 4 4-1.82 4-4 4-4-1.82-4-4m4-3c-1.62 0-3 1.38-3 3s1.38 3 3 3 3-1.38 3-3-1.38-3-3-3"/></svg>
    </button>
    <div style="font-size:15px;margin-top:12px">${esc(sv.autohaus.name)}</div>
    <span class="label" style="margin-top:8px">${esc(sv.autohaus.adresse)}</span>
    <a class="aktion" href="tel:${esc(sv.autohaus.tel.replace(/\s/g, ""))}"
       style="display:block;text-decoration:none;text-align:center;margin-top:16px">${esc(sv.autohaus.tel)}</a>
    ${sv.autohaus.mail ? `<a class="aktion" href="mailto:${esc(sv.autohaus.mail)}"
       style="display:block;text-decoration:none;text-align:center">E-Mail schreiben</a>` : ""}
    <a class="aktion" target="_blank" rel="noopener"
       href="https://maps.google.com/?q=${encodeURIComponent(sv.autohaus.adresse)}"
       style="display:block;text-decoration:none;text-align:center">In Karten öffnen</a>
  </div>
  <div class="tile"><span class="label">Servicebuch</span>
    ${sv.buch.length ? sv.buch.map((e, i) => swipeZeile(`sbuch:${i}`, `<button class="leaf" style="padding-left:0" data-go="sbuch:${i}">
      <span class="k">${esc(e.art)}<small>${esc(e.werkstatt)}</small></span>
      <span class="v">${e.datum}<small>${de(e.km)} km${e.kosten != null ? " · " + eur(e.kosten) + " €" : ""}</small></span>
      <svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`)).join("") : `<div class="leer" style="margin-top:12px">
      <strong>Noch keine Einträge</strong>
      <p>Aus dem Servicebuch berechnet die App die Fälligkeit von Inspektion, Ölwechsel und Hauptuntersuchung. Der erste Eintrag ist die Grundlage für jede Prognose.</p>
    </div>`}
    <button class="aktion primaer" data-sbneu>Eintrag hinzufügen</button>
  </div>`;
}

function vIdent() {
  return `
  <div class="tile"><span class="label">Fahrzeug</span>
    <dl class="rows" style="margin-top:10px">
      <div class="row"><dt>Modell</dt><dd>${esc(CONFIG.fahrzeugtitel)}</dd></div>
      <div class="row"><dt>Ausführung</dt><dd>${CONFIG.ausfuehrung ? esc(CONFIG.ausfuehrung) : "noch offen"}</dd></div>
      <div class="row"><dt>Fahrgestellnummer</dt><dd>${esc(CAR.fin)}</dd></div>
      <div class="row"><dt>Motor und Leistung</dt><dd>${esc(CAR.details)}</dd></div>
      <div class="row"><dt>Erstzulassung</dt><dd>${esc(CAR.erstzulassung)}</dd></div>
      <div class="row"><dt>Kennzeichen</dt><dd>${esc(CONFIG.kennzeichen)}</dd></div>
      <div class="row" style="border-bottom:0"><dt>Zulassung</dt><dd>Deutschland</dd></div>
    </dl></div>
  ${CAR.technik.map((g) => `<div class="tile"><span class="label">${esc(g.gruppe)}</span>
    <dl class="rows" style="margin-top:8px">
      ${g.posten.map((pp, i) => `<div class="row" ${i === g.posten.length - 1 ? 'style="border-bottom:0"' : ""}>
        <dt>${esc(pp[0])}</dt>
        <dd style="${pp[1] ? "" : "color:var(--fg3)"}">${pp[1] ? esc(pp[1]) : "noch offen"}</dd></div>`).join("")}
    </dl></div>`).join("")}
  ${CAR.ausstattung.map((g) => `<div class="tile"><span class="label">${esc(g.gruppe)}</span>
    <dl class="rows" style="margin-top:8px">
      ${g.sonder.map((x, i) => `<div class="row" ${i === g.sonder.length - 1 ? 'style="border-bottom:0"' : ""}>
        <dt style="color:var(--fg)">${esc(x)}</dt><dd></dd></div>`).join("")}
    </dl></div>`).join("")}
  <button class="aktion">Daten bearbeiten</button>`;
}

function monatePlus(datum, monate) { const d = dat(datum); d.setMonth(d.getMonth() + monate); return d; }
function letzterEintrag(stichwort) { return CAR.service.buch.find((e) => e.art.toLowerCase().includes(stichwort)) || null; }
function termine() {
  const oel = CAR.oel;
  const oelKm = oel.modus === "hersteller" ? oel.herstellerKm : Math.min(oel.km, oel.herstellerKm);
  const oelM = oel.modus === "hersteller" ? oel.herstellerMonate : Math.min(oel.monate, oel.herstellerMonate);
  const bauen = (art, basis, kmIntervall, monate, ersatzDatum) => {
    const kmZiel = basis && kmIntervall ? basis.km + kmIntervall : null;
    const datZiel = basis ? monatePlus(basis.datum, monate) : (ersatzDatum ? monatePlus(ersatzDatum, monate) : null);
    const kmDatum = kmZiel !== null ? prognose(kmZiel).datum : null;
    const zuerst = (kmDatum && datZiel) ? (kmDatum < datZiel ? "km" : "zeit") : (kmDatum ? "km" : "zeit");
    return { art, km: kmZiel, datum: datZiel, kmDatum, zuerst, basis,
      ziel: zuerst === "km" ? kmDatum : datZiel, vereinbart: CAR.service.vereinbart[art] || null };
  };
  const oelTermin = bauen("Ölwechsel", letzterEintrag("öl"), oelKm, oelM);
  // Genauere, eigenständige Ölwechsel-Prognose (siehe oelwechselPrognose())
  // ersetzt hier den km-Anteil aus dem generischen bauen() oben, damit
  // Übersicht, Service-Kachel und Anstehende Termine überall dieselbe Zahl
  // für "wann ist der nächste Ölwechsel fällig" zeigen statt drei leicht
  // unterschiedlicher Werte.
  const oelProg = oelTermin.basis ? oelwechselPrognose() : null;
  if (oelProg) {
    oelTermin.km = oelProg.km;
    oelTermin.kmDatum = oelProg.datum;
    oelTermin.zuerst = oelTermin.datum && oelProg.datum >= oelTermin.datum ? "zeit" : "km";
    oelTermin.ziel = oelTermin.zuerst === "km" ? oelProg.datum : oelTermin.datum;
  }
  return [
    oelTermin,
    bauen("Inspektion", letzterEintrag("inspektion"), 30000, 24),
    bauen("Hauptuntersuchung", letzterEintrag("hauptuntersuchung"), null, 24,
      CAR.erstzulassung && CAR.erstzulassung.includes("/") ? `01.${CAR.erstzulassung.replace("/", ".")}` : null),
  ];
}

function vWerkstatt() {
  const a = CAR.service.autohaus;
  return `
  <div class="tile"><span class="label">Angaben zum Autohaus</span>
    <div class="feld"><label>Name</label><input value="${esc(a.name)}" data-ah="name"></div>
    <div class="feld"><label>Adresse</label><input value="${esc(a.adresse)}" data-ah="adresse"></div>
    <div class="feld"><label>Telefon</label><input type="tel" value="${esc(a.tel)}" data-ah="tel"></div>
    <div class="feld" style="border-bottom:0"><label>E-Mail</label>
      <input type="email" value="${esc(a.mail || "")}" data-ah="mail" placeholder="noch offen"></div>
  </div>`;
}

function vSbuch() {
  const i = parseInt(route.id, 10), e = CAR.service.buch[i];
  if (!e) return `<div class="tile">Eintrag nicht gefunden</div>`;
  const istEntwurf = entwurfIndex === i;
  const artBekannt = SERVICE_ARTEN.includes(e.art);
  return `
  <div class="tile"><span class="label">${esc(e.art)}</span>
    <div class="fig" style="font-size:40px;margin-top:12px">${de(e.km)}<span class="unit">km</span></div>
    <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" style="border-bottom:0"><label>Kosten</label>
      <span class="mitEinheit"><input 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>`}`;
}

function vVers() {
  const v = CAR.versicherung;
  return `
  <div class="tile"><span class="label">Versicherung - Schaden Notruf 24/7</span>
    <button class="zahnrad" data-go="notruf:x" aria-label="Rufnummern ändern">
      <svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 2h4.2v2.84c.37.13.75.27 1.11.44L16.7 3.3l3 3.01-1.98 1.99c.17.36.31.74.44 1.11H21v4.3h-2.84c-.13.37-.27.75-.44 1.11l2 2-3.03 2.89-1.98-1.98c-.36.17-.74.31-1.11.44V21H9.4v-2.84a9 9 0 0 1-1.11-.44L6.3 19.7l-3-3.01 1.98-1.99a9 9 0 0 1-.44-1.11H2V9.4h2.84c.13-.37.27-.75.44-1.11L3.3 6.3l3.01-3L8.3 5.28c.36-.17.74-.31 1.11-.44V2Zm1 1v2.56l-.34.11c-.61.2-1.16.4-1.6.66l-.34.2L6.3 4.71 4.7 6.3l1.83 1.82-.2.34c-.27.44-.45.99-.66 1.6l-.11.34H3v2.2h2.56l.11.34c.2.61.4 1.16.66 1.6l.2.34-1.82 1.82 1.59 1.6 1.82-1.83.34.2c.44.27.99.45 1.6.66l.34.11V20h2.2v-2.56l.34-.11a8.3 8.3 0 0 0 1.6-.66l.34-.2 1.83 1.83 1.57-1.5-1.81-1.82.2-.34c.27-.44.45-.99.66-1.6l.11-.34H20v-2.3h-2.56l-.11-.34a8.2 8.2 0 0 0-.66-1.6l-.2-.34 1.82-1.82-1.59-1.6-1.82 1.83-.34-.2a8.2 8.2 0 0 0-1.6-.66l-.34-.11V3zm-2.9 8.5c0-2.18 1.82-4 4-4s4 1.82 4 4-1.82 4-4 4-4-1.82-4-4m4-3c-1.62 0-3 1.38-3 3s1.38 3 3 3 3-1.38 3-3-1.38-3-3-3"/></svg>
    </button>
    <a class="aktion" href="tel:${esc(v.service.replace(/\s/g, ""))}"
       style="display:block;text-decoration:none;margin-top:14px;text-align:center">
       Inland · ${esc(v.service)}</a>
    <a class="aktion" href="tel:${esc(v.notfall.replace(/\s/g, ""))}"
       style="display:block;text-decoration:none;text-align:center">
       Ausland · ${esc(v.notfall)}</a>
  </div>
  <div class="tile"><span class="label">Jahresbeitrag ab ${dedat(new Date(v.gueltigAb))}</span>
    <button class="zahnrad" data-go="beitrag:x" aria-label="Beitrag anpassen">
      <svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 2h4.2v2.84c.37.13.75.27 1.11.44L16.7 3.3l3 3.01-1.98 1.99c.17.36.31.74.44 1.11H21v4.3h-2.84c-.13.37-.27.75-.44 1.11l2 2-3.03 2.89-1.98-1.98c-.36.17-.74.31-1.11.44V21H9.4v-2.84a9 9 0 0 1-1.11-.44L6.3 19.7l-3-3.01 1.98-1.99a9 9 0 0 1-.44-1.11H2V9.4h2.84c.13-.37.27-.75.44-1.11L3.3 6.3l3.01-3L8.3 5.28c.36-.17.74-.31 1.11-.44V2Zm1 1v2.56l-.34.11c-.61.2-1.16.4-1.6.66l-.34.2L6.3 4.71 4.7 6.3l1.83 1.82-.2.34c-.27.44-.45.99-.66 1.6l-.11.34H3v2.2h2.56l.11.34c.2.61.4 1.16.66 1.6l.2.34-1.82 1.82 1.59 1.6 1.82-1.83.34.2c.44.27.99.45 1.6.66l.34.11V20h2.2v-2.56l.34-.11a8.3 8.3 0 0 0 1.6-.66l.34-.2 1.83 1.83 1.57-1.5-1.81-1.82.2-.34c.27-.44.45-.99.66-1.6l.11-.34H20v-2.3h-2.56l-.11-.34a8.2 8.2 0 0 0-.66-1.6l-.2-.34 1.82-1.82-1.59-1.6-1.82 1.83-.34-.2a8.2 8.2 0 0 0-1.6-.66l-.34-.11V3zm-2.9 8.5c0-2.18 1.82-4 4-4s4 1.82 4 4-1.82 4-4 4-4-1.82-4-4m4-3c-1.62 0-3 1.38-3 3s1.38 3 3 3 3-1.38 3-3-1.38-3-3-3"/></svg>
    </button>
    <div class="fig" style="font-size:46px;margin-top:12px">${eur(v.beitrag)}<span class="unit">€</span></div>
    <span class="label" style="margin-top:14px">${esc(v.zahlweise)} · enthält ${eur(v.steueranteil)} € Versicherungsteuer (${esc(v.steuersatz)})</span>
    <dl class="rows" style="margin-top:16px">
      ${v.teile.map((t, i) => `<div class="row" ${i === v.teile.length - 1 ? 'style="border-bottom:0"' : ""}>
        <dt>${esc(t.bez)}${t.sf ? `<br><span style="color:var(--fg3);font-size:11px">${esc(t.sf)} · Beitragssatz ${esc(t.satz)}</span></dt>` : "</dt>"}
        <dd>${eur(t.betrag)} €<small>${v.beitrag ? de(t.betrag / v.beitrag * 100, 0) : 0} %</small></dd></div>`).join("")}
    </dl>
    <div class="bar" style="margin-top:18px">
      ${v.teile.map((t, i) => `<i style="flex:${t.betrag || 0.01};background:${["var(--red)", "var(--fg2)", "var(--fg3)"][i] || "var(--fg3)"}"></i>`).join("")}
    </div>
  </div>
  <div class="tile"><span class="label">Vertrag</span>
    <dl class="rows" style="margin-top:8px">
      <div class="row"><dt>Gesellschaft</dt><dd>${esc(v.gesellschaft)}</dd></div>
      <div class="row"><dt>Umfang</dt><dd>${esc(v.vertrag)}</dd></div>
      <div class="row"><dt>Vertragsnummer</dt><dd>${esc(v.nummer)}</dd></div>
      ${v.selbst.map(([b, w]) => `<div class="row"><dt>Selbstbeteiligung ${esc(b)}</dt><dd>${esc(w)}</dd></div>`).join("")}
      <div class="row" style="border-bottom:0"><dt>Schadenfreiheitsklasse</dt>
        <dd>${v.teile.map((t) => t.sf).filter(Boolean).join(" / ")}<small>zuvor ${v.teile.map((t) => t.sfAlt).filter(Boolean).join(" / ")}</small></dd></div>
    </dl>
    <button class="aktion" data-go="vertragsdetails:x">Details</button>
  </div>
  ${vSteuer()}`;
}

function vNotrufBearbeiten() {
  const v = CAR.versicherung;
  return `
  <div class="tile"><span class="label">Rufnummern ändern</span>
    <div class="feld"><label for="vNotrufInland">Inland</label>
      <input id="vNotrufInland" style="width:132px" value="${esc(v.service)}" data-vnotruf="service"></div>
    <div class="feld" style="border-bottom:0"><label for="vNotrufAusland">Ausland</label>
      <input id="vNotrufAusland" style="width:132px" value="${esc(v.notfall)}" data-vnotruf="notfall"></div>
  </div>`;
}

function vBeitrag() {
  const v = CAR.versicherung;
  return `
  <div class="tile"><span class="label">Beitrag anpassen</span>
    <div class="feld"><label for="vGueltigAb">Gültig ab</label>
      <input id="vGueltigAb" type="date" style="width:132px" value="${v.gueltigAb}" data-vgueltigab></div>
    <div class="feld"><label for="vSfHaft">Schadenfreiheitsklasse Haftpflicht</label>
      <input id="vSfHaft" style="width:132px" value="${esc(v.teile[0]?.sf || "")}" data-vteil="0|sf"></div>
    <div class="feld"><label for="vSfKasko">Schadenfreiheitsklasse Vollkasko</label>
      <input id="vSfKasko" style="width:132px" value="${esc(v.teile[1]?.sf || "")}" data-vteil="1|sf"></div>
    <div class="feld"><label for="vKostenHaft">Kosten Haftpflicht</label>
      <span class="mitEinheit"><input id="vKostenHaft" type="number" step="0.01" style="width:132px" value="${v.teile[0]?.betrag ?? 0}" data-vteil="0|betrag"><span>€</span></span></div>
    <div class="feld"><label for="vKostenKasko">Kosten Vollkasko</label>
      <span class="mitEinheit"><input id="vKostenKasko" type="number" step="0.01" style="width:132px" value="${v.teile[1]?.betrag ?? 0}" data-vteil="1|betrag"><span>€</span></span></div>
    <div class="feld" style="border-bottom:0"><label for="vKostenSchutz">Kosten Schutzbrief</label>
      <span class="mitEinheit"><input id="vKostenSchutz" type="number" step="0.01" style="width:132px" value="${v.teile[2]?.betrag ?? 0}" data-vteil="2|betrag"><span>€</span></span></div>
    <span class="label" style="margin-top:14px">Der Jahresbeitrag oben ist die Summe dieser drei Posten und
      rechnet sich beim Speichern automatisch neu — "Gültig ab" steuert den Titel der Jahresbeitrag-Kachel</span>
  </div>`;
}

function vVertragsdetails() {
  const v = CAR.versicherung;
  return `
  <button class="tile tilebtn" data-go="schutz:x">
    <span class="label">Schutzbrief Mobilität</span>
    <svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
    <div style="font-size:14px;margin-top:12px">Panne, Unfall, Diebstahl, Fahrerausfall</div>
    <span class="label" style="margin-top:10px">Nummern und Leistungen für den Ernstfall</span>
  </button>
  ${v.leistungen.map((g) => `<div class="tile"><span class="label">${esc(g.gruppe)}</span>
    <dl class="rows" style="margin-top:8px">
      ${g.posten.map((x, i) => `<div class="row" ${i === g.posten.length - 1 ? 'style="border-bottom:0"' : ""}>
        <dt style="color:var(--fg)">${esc(x)}</dt><dd></dd></div>`).join("")}
    </dl></div>`).join("")}`;
}

function vSchutz() {
  const v = CAR.versicherung, sb = v.schutzbrief;
  return `
  <div class="tile"><span class="label">Die Entscheidung am Schadenort</span>
    <div style="font-size:13px;color:var(--fg2);line-height:1.6;margin-top:10px">
      Diese drei Leistungen schließen einander aus. Wer eine nimmt, verliert den Anspruch auf die
      anderen beiden.</div>
    <dl class="rows" style="margin-top:14px">
      ${sb.entweder.map((x, i) => `<div class="row" ${i === sb.entweder.length - 1 ? 'style="border-bottom:0"' : ""}>
        <dt style="color:var(--fg)">${esc(x.bez)}</dt>
        <dd>${esc(x.grenze)}${x.zusatz ? `<small>${esc(x.zusatz)}</small>` : ""}</dd></div>`).join("")}
    </dl>
    <span class="label" style="margin-top:16px">Mobilität wird binnen 60 Minuten organisiert</span>
  </div>
  <div class="tile"><span class="label">Wann der Schutzbrief greift</span>
    <dl class="rows" style="margin-top:8px">
      ${sb.ausloeser.map((a, i) => `<div class="row" ${i === sb.ausloeser.length - 1 ? 'style="border-bottom:0"' : ""}>
        <dt style="color:var(--fg)">${esc(a)}</dt><dd></dd></div>`).join("")}
    </dl>
    <span class="label" style="margin-top:16px">Gilt in ${esc(sb.gilt)}</span>
    <span class="label" style="margin-top:8px">Versichert: ${esc(sb.versichert)}</span>
  </div>
  ${sb.gruppen.map((g) => `<div class="tile"><span class="label">${esc(g.titel)}</span>
    <dl class="rows" style="margin-top:8px">
      ${g.posten.map((pp, i) => `<div class="row" ${i === g.posten.length - 1 ? 'style="border-bottom:0"' : ""}>
        <dt style="color:var(--fg)">${esc(pp[0])}${pp[2] ? `<br><span style="color:var(--fg3);font-size:11px">${esc(pp[2])}</span>` : ""}</dt>
        <dd>${esc(pp[1])}</dd></div>`).join("")}
    </dl></div>`).join("")}
  <div class="tile"><span class="label">Nicht versichert</span>
    <dl class="rows" style="margin-top:8px">
      ${sb.nicht.map((n, i) => `<div class="row" ${i === sb.nicht.length - 1 ? 'style="border-bottom:0"' : ""}>
        <dt style="color:var(--fg)">${esc(n[0])}<br>
          <span style="color:var(--fg3);font-size:11px">${esc(n[1])}</span></dt><dd></dd></div>`).join("")}
    </dl></div>
  <div class="tile"><span class="label">Deine Pflichten</span>
    <dl class="rows" style="margin-top:8px">
      ${sb.pflichten.map((x, i) => `<div class="row" ${i === sb.pflichten.length - 1 ? 'style="border-bottom:0"' : ""}>
        <dt style="color:var(--fg)">${esc(x)}</dt><dd></dd></div>`).join("")}
    </dl>
    <div style="margin-top:16px;font-size:12.5px;color:var(--fg2);line-height:1.6">${esc(sb.rang)}</div>
    <span class="label" style="margin-top:14px">${esc(sb.stand)}</span>
  </div>`;
}

function vSteuer() {
  const t = CAR.steuer;
  return `
  <div class="tile" style="margin-top:26px"><span class="label">Kfz-Steuer</span>
    <button class="zahnrad" data-go="steuer:x" aria-label="Kfz-Steuer bearbeiten">
      <svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 2h4.2v2.84c.37.13.75.27 1.11.44L16.7 3.3l3 3.01-1.98 1.99c.17.36.31.74.44 1.11H21v4.3h-2.84c-.13.37-.27.75-.44 1.11l2 2-3.03 2.89-1.98-1.98c-.36.17-.74.31-1.11.44V21H9.4v-2.84a9 9 0 0 1-1.11-.44L6.3 19.7l-3-3.01 1.98-1.99a9 9 0 0 1-.44-1.11H2V9.4h2.84c.13-.37.27-.75.44-1.11L3.3 6.3l3.01-3L8.3 5.28c.36-.17.74-.31 1.11-.44V2Zm1 1v2.56l-.34.11c-.61.2-1.16.4-1.6.66l-.34.2L6.3 4.71 4.7 6.3l1.83 1.82-.2.34c-.27.44-.45.99-.66 1.6l-.11.34H3v2.2h2.56l.11.34c.2.61.4 1.16.66 1.6l.2.34-1.82 1.82 1.59 1.6 1.82-1.83.34.2c.44.27.99.45 1.6.66l.34.11V20h2.2v-2.56l.34-.11a8.3 8.3 0 0 0 1.6-.66l.34-.2 1.83 1.83 1.57-1.5-1.81-1.82.2-.34c.27-.44.45-.99.66-1.6l.11-.34H20v-2.3h-2.56l-.11-.34a8.2 8.2 0 0 0-.66-1.6l-.2-.34 1.82-1.82-1.59-1.6-1.82 1.83-.34-.2a8.2 8.2 0 0 0-1.6-.66l-.34-.11V3zm-2.9 8.5c0-2.18 1.82-4 4-4s4 1.82 4 4-1.82 4-4 4-4-1.82-4-4m4-3c-1.62 0-3 1.38-3 3s1.38 3 3 3 3-1.38 3-3-1.38-3-3-3"/></svg>
    </button>
    <dl class="rows" style="margin-top:10px">
      <div class="row"><dt>Betrag</dt><dd>${eur(t.betrag)} €<small>${esc(t.zeitraum)}</small></dd></div>
      <div class="row" style="border-bottom:0"><dt>Nächste Fälligkeit</dt>
        <dd>${t.faellig ? dedat(new Date(t.faellig)) : "unbekannt"}</dd></div>
    </dl>
  </div>`;
}

function vSteuerBearbeiten() {
  const t = CAR.steuer;
  return `
  <div class="tile"><span class="label">Kfz-Steuer</span>
    <div class="feld"><label for="stBetrag">Betrag</label>
      <span class="mitEinheit"><input id="stBetrag" type="number" step="1" style="width:132px" value="${t.betrag}"
        data-steuer><span>€</span></span></div>
    <div class="feld"><label for="stZeit">Zeitraum</label>
      <select id="stZeit" style="width:132px" data-steuerzeit>
        <option ${t.zeitraum === "jährlich" ? "selected" : ""}>jährlich</option>
        <option ${t.zeitraum === "halbjährlich" ? "selected" : ""}>halbjährlich</option>
      </select></div>
    <div class="feld"><label for="stFaellig">Nächste Fälligkeit</label>
      <input id="stFaellig" type="date" style="width:132px" value="${t.faellig || ""}" data-steuerfaellig></div>
    <div class="feld" style="border-bottom:0"><label for="stLast">Lastschrift erteilt</label>
      <label class="switch"><input id="stLast" type="checkbox" data-steuerlast ${t.lastschrift ? "checked" : ""}><span class="track"></span></label></div>
  </div>`;
}

/* B5: Die Wahl gilt dauerhaft, nicht nur bis zum Reload - sie wird beim Aufbau
   der Menüleiste wieder angewendet. */
const SCHRIFT_KEY = "audi-dashboard-tableiste-schrift";
let beschriftung = (() => {
  try { return localStorage.getItem(SCHRIFT_KEY) !== "aus"; } catch (e) { return true; }
})();
let bodenAn = true;
function csvDatei(name, kopf, zeilen) {
  const txt = [kopf.join(";")].concat(zeilen.map((r) => r.join(";"))).join("\r\n");
  const a = document.createElement("a");
  a.href = "data:text/csv;charset=utf-8," + encodeURIComponent("﻿" + txt);
  a.download = name; document.body.appendChild(a); a.click(); a.remove();
}

function vEinst() {
  return `
  <div class="tile"><span class="label">Fahrzeug einrichten</span>
    <span style="display:block;margin-top:10px;color:var(--fg2);
      font-size:13.5px;line-height:1.6">
      Fahrgestellnummer, Kennzeichen, Erstzulassung, Ausführung und Ölwechsel-Intervall — bei der
      Ersteinrichtung oder wenn sich diese Angaben ändern.</span>
    ${einrichtenOffen ? `
      <div class="feld" style="margin-top:16px"><label for="einFin">Fahrgestellnummer (FIN)</label>
        <input id="einFin" value="${esc(CAR.fin || "")}" data-einrichten="fin"></div>
      <div class="feld"><label for="einModell">Modell</label>
        <select id="einModell" data-einrichten="modell">
          ${(FAHRZEUG_MODELLE.includes(CONFIG.fahrzeugtitel) ? FAHRZEUG_MODELLE : [CONFIG.fahrzeugtitel, ...FAHRZEUG_MODELLE])
            .map((m) => `<option ${m === CONFIG.fahrzeugtitel ? "selected" : ""}>${esc(m)}</option>`).join("")}
        </select></div>
      <div class="feld"><label for="einKennzeichen">Kennzeichen</label>
        <input id="einKennzeichen" value="${esc(CONFIG.kennzeichen || "")}" data-einrichten="kennzeichen"></div>
      <div class="feld"><label for="einErstzulassung">Erstzulassung</label>
        <input id="einErstzulassung" value="${esc(CAR.erstzulassung || "")}" data-einrichten="erstzulassung" placeholder="MM/JJJJ"></div>
      <div class="feld"><label for="einAusfuehrung">Ausführung</label>
        <input id="einAusfuehrung" value="${esc(CONFIG.ausfuehrung || "")}" data-einrichten="ausfuehrung"></div>
      <div class="feld" style="border-bottom:0"><label for="einWlan">Fahrzeug-WLAN (SSID)</label>
        <input id="einWlan" value="${esc(CONFIG.wlanName || "")}" data-einrichten="wlan" placeholder="z. B. Audi_MMI_XXXX_5GHz"></div>
      <span class="label" style="margin-top:20px">Ölwechsel-Intervall</span>
      <div class="seg" style="margin-top:8px">
        <button data-oel="hersteller" class="${CAR.oel.modus === "hersteller" ? "on" : ""}">Herstellervorgabe</button>
        <button data-oel="eigen" class="${CAR.oel.modus === "eigen" ? "on" : ""}">Eigenes</button>
      </div>
      ${CAR.oel.modus === "hersteller"
        ? `<dl class="rows" style="margin-top:16px">
            <div class="row"><dt>Laufleistung</dt><dd>${de(CAR.oel.herstellerKm)} km</dd></div>
            <div class="row" style="border-bottom:0"><dt>Zeit</dt><dd>${CAR.oel.herstellerMonate} Monate</dd></div>
          </dl>
          <span class="label" style="margin-top:14px">Aus dem Fahrzeug gelesen</span>`
        : `<div class="feld" style="margin-top:6px"><label for="oelKm">Nach Laufleistung</label>
            <select id="oelKm" data-oelkm>${[5000, 10000, 15000, 20000].map((k) => `<option value="${k}" ${k === CAR.oel.km ? "selected" : ""}>${de(k)} km</option>`).join("")}</select></div>
           <div class="feld" style="border-bottom:0"><label for="oelM">Nach Zeit</label>
            <select id="oelM" data-oelm>${[12, 24].map((m) => `<option value="${m}" ${m === CAR.oel.monate ? "selected" : ""}>${m === 12 ? "1 Jahr" : "2 Jahre"}</option>`).join("")}</select></div>`}
    ` : ""}
    <button class="aktion ${einrichtenOffen ? "primaer" : ""}" data-einrichten-auf style="margin-top:16px">${einrichtenOffen ? "Fertig" : "Einrichten"}</button>
  </div>
  <div class="tile"><span class="label">Darstellung</span>
    <div class="seg">
      <button data-mode="nacht" class="${HOST.dataset.theme === "nacht" ? "on" : ""}">Nacht</button>
      <button data-mode="tag" class="${HOST.dataset.theme === "tag" ? "on" : ""}">Tag</button>
    </div>
    <div class="feld" style="margin-top:16px;border-bottom:0">
      <label for="schrift">Beschriftung in der Menüleiste</label>
      <label class="switch"><input id="schrift" type="checkbox" data-schrift ${beschriftung ? "checked" : ""}><span class="track"></span></label></div>
    <span class="label" style="margin-top:14px">Aus zeigt nur die Symbole und gibt etwas Höhe frei</span>
    <div class="feld" style="margin-top:16px;border-bottom:0">
      <label for="boden">Fahrbahn unter dem Fahrzeug</label>
      <label class="switch"><input id="boden" type="checkbox" data-boden ${bodenAn ? "checked" : ""}><span class="track"></span></label></div>
    <span class="label" style="margin-top:14px">Angedeutete Fahrbahn mit Schatten, nur bei Außenansichten</span>
  </div>
  <div class="tile"><span class="label">Bild der Übersicht</span>
    <div class="feld" style="border-bottom:0"><label for="startbild">Ansicht</label>
      <select id="startbild" data-startbild>${BILDER.map((b) => `<option ${b.name === CONFIG.startbild ? "selected" : ""}>${b.name}</option>`).join("")}</select></div>
    <div style="margin-top:16px">${(() => { const info = bildInfo(startIndex()); return bildMitPlatzhalter(info.src, info.datei, info.label, "carfix"); })()}</div>
  </div>
  <div class="tile"><span class="label">Fahrzeugbilder</span>
    <span class="label" style="margin-top:10px;color:var(--fg2);letter-spacing:0;
      text-transform:none;font-size:12.5px;line-height:1.6">
      Auf ein Bild tippen, um ein eigenes Foto hochzuladen oder zu ersetzen.</span>
    <div class="bildgrid">
      ${BILDER_UPLOAD_SLOTS.map((s) => `
        <div class="bildslot">
          <div data-bildklick="${esc(s.datei)}">
            ${bildMitPlatzhalter(`/local/bilder/${s.datei}?v=${bildVersion}`, s.datei, s.label, "carfix mini", null, true)}
          </div>
          <input type="file" accept="image/*" hidden data-bildupload="${esc(s.datei)}">
          <span class="bildslot-label">${esc(s.name)}</span>
          ${bildMenuOffen === s.datei ? `
          <div class="bildmenu-catcher" data-bildmenu-schliessen></div>
          <div class="bildmenu slot">
            <button type="button" data-bildersetzen="${esc(s.datei)}">Foto ersetzen</button>
            <button type="button" class="loeschen" data-bildloeschen="${esc(s.datei)}">Löschen</button>
          </div>` : ""}
        </div>`).join("")}
    </div>
  </div>
  <div class="tile"><span class="label">Kraftstoff-Rabatt</span>
    <div class="feld"><label for="sdAktiv" style="display:flex;align-items:center;gap:6px">
        <img src="/local/bilder/${MARKENLOGO_DATEI}" alt="Shell" style="height:14px;width:auto" onerror="this.style.display='none'"> SmartDeal aktiv</label>
      <label class="switch"><input id="sdAktiv" type="checkbox" data-sd ${CAR.smartdeal.aktiv ? "checked" : ""}><span class="track"></span></label></div>
    ${CAR.smartdeal.aktiv ? `
    <div class="feld" style="border-bottom:0"><label>Gültig bis</label>
      <span>${dedat(new Date(CAR.smartdeal.laeuftAb))}</span></div>` :
      `<span class="label" style="margin-top:14px">Ersparnis und Bildmarke werden ausgeblendet</span>`}
    ${smartdealPopupOffen ? `
    <div class="bildmenu-catcher" data-sdabbrechen></div>
    <div class="sdpopup">
      <div class="feld" style="border-bottom:0"><label for="sdPopupBis">Gültig bis</label>
        <input id="sdPopupBis" type="date" style="width:132px" value="${CAR.smartdeal.laeuftAb || ""}"></div>
      <div class="seg" style="margin-top:14px">
        <button type="button" data-sdabbrechen>Abbrechen</button>
        <button type="button" class="on" data-sdbestaetigen>Bestätigen</button>
      </div>
    </div>` : ""}
  </div>
  <div class="tile"><span class="label">Fahrten zusammenfassen</span>
    <div class="feld" style="border-bottom:0"><label for="pause">Pause bis</label>
      <span class="mitEinheit"><input id="pause" type="number" step="5" min="0"
        value="${CONFIG.pauseMin}" data-pause><span>Minuten</span></span></div>
    <span class="label" style="margin-top:14px">Steigst du kürzer als diese Zeit aus — etwa zum Tanken —,
      bleibt es eine zusammenhängende Fahrt statt zweier</span>
  </div>
  <div class="tile"><span class="label">Fahrzeugprofil</span>
    <span class="label" style="margin-top:10px;color:var(--fg2);letter-spacing:0;
      text-transform:none;font-size:12.5px;line-height:1.6">
      Alles Fahrzeugspezifische liegt in einer einzigen Datei: Stammdaten, technische Daten,
      Ausstattung, Reifen, Versicherung, Steuer und Werkstatt. Für ein anderes Fahrzeug wird nur
      diese Datei getauscht — die App selbst bleibt unverändert.</span>
    <button class="aktion" data-profil="export">Profil sichern</button>
    <label class="aktion" style="display:block;text-align:center;cursor:pointer">
      Profil laden<input type="file" accept="application/json" data-profil="import" hidden></label>
  </div>
  <div class="tile"><span class="label">Backup</span>
    <span class="label" style="margin-top:10px;color:var(--fg2);letter-spacing:0;
      text-transform:none;font-size:12.5px;line-height:1.6">
      Sichert Fahrzeugprofil, Fahrten und Tankvorgänge gemeinsam. "Jetzt erstellen" legt eine
      Kopie auf dem Server ab, "Exportieren"/"Importieren" übertragen den aktuellen
      Gesamtstand als einzelne Datei.</span>
    ${CONFIG.letztesBackup ? `<span class="label" style="margin-top:14px">Letztes Backup:
      ${dedat(new Date(CONFIG.letztesBackup))} · ${new Date(CONFIG.letztesBackup).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })} Uhr</span>` : ""}
    <button class="aktion" data-backup="jetzt">Backup jetzt erstellen</button>
    <div class="feld" style="margin-top:16px;border-bottom:0"><label for="backupIntervall">Autom. Backup</label>
      <select id="backupIntervall" data-backupintervall>
        ${[["aus", "Aus"], ["taeglich", "Täglich"], ["woechentlich", "Wöchentlich"], ["monatlich", "Monatlich"]]
          .map(([v, l]) => `<option value="${v}" ${v === CONFIG.backupIntervall ? "selected" : ""}>${l}</option>`).join("")}
      </select></div>
    <button class="aktion" data-backup="export" style="margin-top:16px">Backup exportieren</button>
    <label class="aktion" style="display:block;text-align:center;cursor:pointer">
      Backup importieren<input type="file" accept="application/json" data-backup="import" hidden></label>
  </div>
  <div class="tile"><span class="label">Daten ausgeben</span>
    <span style="display:block;margin-top:10px;color:var(--fg2);font-size:13.5px;line-height:1.6">
      Semikolon als Trennzeichen und deutsches Zahlenformat, damit die Dateien ohne Umweg in
      Tabellenprogrammen aufgehen.</span>
    <button class="aktion" data-csv="fahrten">Fahrten als CSV</button>
    <button class="aktion" data-csv="tanken">Tankvorgänge als CSV</button>
    <button class="aktion" data-csv="service">Servicebuch als CSV</button>
  </div>
  <div class="tile"><span class="label">Version</span>
    <dl class="rows">
      <div class="row"><dt>Dashboard</dt><dd>${esc(CONFIG.version)}</dd></div>
      <div class="row"><dt>Fahrzeugdaten</dt><dd>EU Data Act<small>Abruf alle 15 Minuten</small></dd></div>
      <div class="row" style="border-bottom:0"><dt>Position</dt><dd>iPhone<small>Companion App</small></dd></div>
    </dl>
    ${(() => {
      const s = UPDATE_STATUS;
      if (!s || s.stand === "keine_quelle") return `
        <span class="label" style="margin-top:14px">${s ? "Keine Update-Quelle eingerichtet" : "Prüft, ob eine neuere Version verfügbar ist"}</span>
        <button class="aktion" data-update-pruefen style="margin-top:12px">${updatePruefend ? "Prüfe …" : "Update suchen"}</button>`;
      if (s.stand === "fehler") return `
        <span class="label" style="margin-top:14px;color:var(--red)">${esc(s.fehler || "Update-Prüfung fehlgeschlagen")}</span>
        <button class="aktion" data-update-pruefen style="margin-top:12px">${updatePruefend ? "Prüfe …" : "Erneut versuchen"}</button>`;
      if (s.stand === "geprueft" && s.verfuegbar) return `
        <span class="label" style="margin-top:14px">Update verfügbar — Version ${esc(s.neue_version)}</span>
        <button class="aktion" data-update-installieren style="margin-top:12px">${updateInstallierend ? "Installiere …" : "Update installieren"}</button>`;
      return `
        <span class="label" style="margin-top:14px">Aktuell${s.geprueft_um ? " — zuletzt geprüft " + dedat(new Date(s.geprueft_um)) : ""}</span>
        <button class="aktion" data-update-pruefen style="margin-top:12px">${updatePruefend ? "Prüfe …" : "Erneut prüfen"}</button>`;
    })()}
  </div>`;
}

let nmOffen = false;
let einrichtenOffen = false;
let bildMenuOffen = null;
let smartdealPopupOffen = false;
function vReifen() {
  const r = CAR.reifen;
  const feld = (k, schluessel, bez, typ) => {
    const t = r[k];
    return `<div class="feld"><label>${bez}</label>
      <input type="${typ || "text"}" value="${esc(t[schluessel] ?? "")}"
             data-rf="${k}|${schluessel}" placeholder="noch offen"></div>`;
  };
  const satz = (k) => {
    const t = r[k], aktiv = r.aktiv.toLowerCase() === k;
    const radDatei = `rad-${k}.webp`;
    const radLabel = k === "sommer" ? "Sommerrad" : "Winterrad";
    return `<div class="tile">
      <div data-bildklick="${radDatei}">
        ${bildMitPlatzhalter(`/local/bilder/${radDatei}?v=${bildVersion}`, radDatei, radLabel, "radbild")}
      </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 class="radkopf">
        <span class="label">${k === "sommer" ? "Sommer" : "Winter"}</span>
        <div class="fig" style="font-size:32px;margin-top:14px">${t.km != null ? de(t.km) : "–"}<span class="unit">km</span></div>
      </div>
      <div class="seg" style="margin-top:16px">
        <button data-satz="${k === "sommer" ? "Sommer" : "Winter"}" class="${aktiv ? "on" : ""}">Montiert</button>
      </div>
      <div style="margin-top:22px">
        ${feld(k, "marke", "Hersteller")}
        ${feld(k, "modell", "Modell")}
        ${feld(k, "mass", "Größe")}
        ${feld(k, "dot", "DOT")}
        <div class="feld"><label>Solldruck vorn / hinten</label>
          <span class="mitEinheit">
            <input class="druckfeld" value="${esc(t.druck_vorne ?? "")}" data-rf="${k}|druck_vorne" placeholder="–">
            <span>/</span>
            <input class="druckfeld" value="${esc(t.druck_hinten ?? "")}" data-rf="${k}|druck_hinten" placeholder="–">
            <span>bar</span>
          </span>
        </div>
        ${feld(k, "kommentar", "Kommentar")}
      </div>
    </div>`;
  };
  return `
  <div class="tile"><span class="label">Anzugsmoment der Radschrauben</span>
    <button class="zahnrad" data-nmauf aria-label="Anzugsmoment ändern">
      <svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 2h4.2v2.84c.37.13.75.27 1.11.44L16.7 3.3l3 3.01-1.98 1.99c.17.36.31.74.44 1.11H21v4.3h-2.84c-.13.37-.27.75-.44 1.11l2 2-3.03 2.89-1.98-1.98c-.36.17-.74.31-1.11.44V21H9.4v-2.84a9 9 0 0 1-1.11-.44L6.3 19.7l-3-3.01 1.98-1.99a9 9 0 0 1-.44-1.11H2V9.4h2.84c.13-.37.27-.75.44-1.11L3.3 6.3l3.01-3L8.3 5.28c.36-.17.74-.31 1.11-.44V2Zm1 1v2.56l-.34.11c-.61.2-1.16.4-1.6.66l-.34.2L6.3 4.71 4.7 6.3l1.83 1.82-.2.34c-.27.44-.45.99-.66 1.6l-.11.34H3v2.2h2.56l.11.34c.2.61.4 1.16.66 1.6l.2.34-1.82 1.82 1.59 1.6 1.82-1.83.34.2c.44.27.99.45 1.6.66l.34.11V20h2.2v-2.56l.34-.11a8.3 8.3 0 0 0 1.6-.66l.34-.2 1.83 1.83 1.57-1.5-1.81-1.82.2-.34c.27-.44.45-.99.66-1.6l.11-.34H20v-2.3h-2.56l-.11-.34a8.2 8.2 0 0 0-.66-1.6l-.2-.34 1.82-1.82-1.59-1.6-1.82 1.83-.34-.2a8.2 8.2 0 0 0-1.6-.66l-.34-.11V3zm-2.9 8.5c0-2.18 1.82-4 4-4s4 1.82 4 4-1.82 4-4 4-4-1.82-4-4m4-3c-1.62 0-3 1.38-3 3s1.38 3 3 3 3-1.38 3-3-1.38-3-3-3"/></svg>
    </button>
    <div class="fig" style="font-size:34px;margin-top:12px">${de(r.anzugsmoment)}<span class="unit">Nm</span></div>
    ${nmOffen ? `
      <div style="margin-top:18px;border-top:1px solid var(--line);padding-top:16px">
        <div class="feld" style="border-bottom:0"><label for="nm">Neuer Wert</label>
          <span class="mitEinheit"><input id="nm" type="number" step="5" min="0"
            value="${r.anzugsmoment}"><span>Nm</span></span></div>
        <button class="aktion" data-nmspeichern>Speichern</button>
        <button class="aktion" data-nmauf>Abbrechen</button>
      </div>` : ""}
  </div>
  <div class="tile"><span class="label">Wechseltermin</span>
    <div class="feld"><label for="rfDat">Geplant am</label>
      <input id="rfDat" type="date" value="${r.wechsel.datum}"></div>
    <div class="feld" style="border-bottom:0"><label for="rfOk">Termin vereinbart</label>
      <label class="switch"><input id="rfOk" type="checkbox" ${r.wechsel.vereinbart ? "checked" : ""}><span class="track"></span></label></div>
    <button class="aktion" data-ics="reifen">In den Kalender übernehmen</button>
  </div>
  ${satz("sommer")}
  ${satz("winter")}`;
}

function vTripsFormular() {
  // eigene Funktion, damit sowohl der volle als auch der Leer-Zustand
  // (§11 Abnahmekriterium 1: eine Fahrt muss erfasst werden können) dasselbe
  // Formular zeigen - vorher fehlte es im Leer-Zustand komplett, der Knopf
  // wechselte zu "Abbrechen", aber es erschienen keine Felder.
  return `${fahrtOffen ? `<div class="tile"><span class="label">Neue Fahrt</span>
      <div class="feld"><label>Datum</label><input type="date" id="neuFahrtDatum" value="${new Date().toISOString().slice(0, 10)}"></div>
      <div class="feld"><label>Startzeit</label><input type="time" id="neuFahrtStart" value="08:00"></div>
      <div class="feld" style="border-bottom:0"><label>Dauer in Minuten</label><input type="number" id="neuFahrtDauer" placeholder="0"></div>
      <button class="aktion" data-fahrtspeichern>Speichern</button></div>` : ""}
    <button class="aktion primaer" data-fahrt>${fahrtOffen ? "Abbrechen" : "Fahrt erfassen"}</button>`;
}
function vTrips() {
  const jahre = [...new Set(TRIPS.map((t) => new Date(t.ts_start).getFullYear()))].sort((a, b) => b - a);
  /* D5: Leerzustand nennt die Ursache - die Fahrtenerkennung greift erst, wenn
     das Telefon einmal im Fahrzeug-WLAN war. */
  if (!jahre.length) return `<div class="tile"><div class="leer">
      <strong>Noch keine Fahrten</strong>
      <p>Fahrten werden erkannt, sobald sich das Telefon mit dem WLAN des Fahrzeugs verbindet (${esc(CONFIG.wlanName || "noch nicht eingerichtet")}). Bis dahin lässt sich jede Fahrt von Hand erfassen.</p>
    </div></div>
    ${vTripsFormular()}`;
  let h = `<div class="tile">`;
  jahre.forEach((j, ji) => {
    const tj = TRIPS.filter((t) => new Date(t.ts_start).getFullYear() === j);
    h += `<button class="acc-h" aria-expanded="${ji === 0}" data-acc="y${j}"><span class="mark"></span>
        <span class="k">${j}</span><span class="v">${de(sum(tj, (t) => t.distance_km || 0))} km<br>${tj.length} Fahrten</span></button>
      <div class="acc-b ${ji === 0 ? "on" : ""}" id="y${j}">`;
    const monate = [...new Set(tj.map((t) => new Date(t.ts_start).toLocaleDateString("de-DE", { month: "long" })))];
    monate.forEach((m, mi) => {
      const tm = tj.filter((t) => new Date(t.ts_start).toLocaleDateString("de-DE", { month: "long" }) === m);
      h += `<div class="lvl2"><button class="acc-h" aria-expanded="${mi === 0}" data-acc="m${j}${m}">
          <span class="mark"></span><span class="k">${m}</span>
          <span class="v">${de(sum(tm, (t) => t.distance_km || 0))} km</span></button>
        <div class="acc-b ${mi === 0 ? "on" : ""}" id="m${j}${m}">`;
      tm.forEach((t) => { h += swipeZeile(`trip:${t.trip_id}`, `<button class="leaf" data-go="trip:${t.trip_id}">
          <span class="k">${strecke(t)}
            <small>${dedat(new Date(t.ts_start))} · ${esc(t.art)}</small></span>
          <span class="v">${t.distance_km != null ? de(t.distance_km) + " km" : "offen"}<small>${fahrtVerbrauch(t) != null ? de(fahrtVerbrauch(t), 1) + " l/100 km" : t.status}</small></span>
          <svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); });
      h += `</div></div>`;
    });
    h += `</div>`;
  });
  return h + `</div>
    ${vTripsFormular()}`;
}

function vTrip(id) {
  const t = TRIPS.find((x) => x.trip_id === id);
  if (!t) return `<div class="tile">Fahrt nicht gefunden</div>`;
  return `
  ${mapBox()}
  <div class="tile flat" style="margin:18px 0 14px">
    <span class="label">Distanz</span>
    <div class="fig" style="font-size:52px;margin-top:10px">${t.distance_km != null ? de(t.distance_km) : "–"}<span class="unit">km</span></div>
    <div style="margin-top:15px">
      <button class="pill ${t.art === "arbeitsweg" ? "work" : ""}" data-art="${t.trip_id}"
        style="cursor:pointer;font-family:inherit">${esc(t.art)}</button></div>
    <span class="label" style="margin-top:12px">tippen zum Umschalten</span>
  </div>
  <div class="tile"><dl class="rows">
    <div class="row"><dt>Startort</dt><dd>${esc(t.start_address || "unbekannt")}<small>${dedat(new Date(t.ts_start))}</small></dd></div>
    <div class="row"><dt>Zielort</dt><dd>${esc(t.end_address || "unbekannt")}</dd></div>
    <div class="row"><dt>Startkilometer</dt><dd>${t.odo_start != null ? de(t.odo_start) + " km" : "noch offen"}</dd></div>
    <div class="row"><dt>Endkilometer</dt><dd>${t.odo_end != null ? de(t.odo_end) + " km" : "noch offen"}</dd></div>
    <div class="row"><dt>Dauer</dt><dd>${Math.round(t.duration_s / 60)} min</dd></div>
    <div class="row"><dt>Verbrauch</dt><dd>${fahrtVerbrauch(t) != null ? de(fahrtVerbrauch(t), 1) + " l/100 km" : "liegt nicht vor"}<small>vom Fahrzeug gemeldet</small></dd></div>
    <div class="row"><dt>Status</dt><dd>${t.status}<small>Quelle: ${t.km_quelle || "–"}</small></dd></div>
  </dl></div>
  <button class="aktion loeschen" data-tripdel="${t.trip_id}">Fahrt löschen</button>`;
}

function quad(t, v) { return `<span class="label" style="margin-top:20px">${t}</span>
  <div class="quad">${v.map((x) => `<div><div class="n">${x[0]}</div><div class="l">${x[1]}</div></div>`).join("")}</div>`; }
function statTile(id, label, fig, unit, extra, body) {
  return `<div class="tile"><button class="tile-h" aria-expanded="false" data-acc="${id}">
    <span class="label">${label}</span>
    <div style="display:flex;align-items:flex-end;justify-content:space-between;gap:14px;margin-top:12px">
      <div class="fig" style="font-size:34px">${fig}<span class="unit">${unit}</span></div>
      <span class="wert" style="padding-bottom:5px">${extra}</span></div>
  </button><div class="tile-b" id="${id}">${body}</div></div>`;
}
/* Echte Auswertung aus TRIPS/FILLS statt Beispielwerten. Nur abgeschlossene
   Fahrten/Tankvorgänge zählen (distance_km bzw. liters gesetzt) - offene/
   unvollständige Einträge haben noch keinen belastbaren Wert.
   Der Verbrauch ist eine Näherung (getankte Menge im Zeitraum ÷ gefahrene
   km im selben Zeitraum), da einzelne Fahrten keinen eigenen Verbrauchswert
   tragen - erst über einen Zeitraum gemittelt ergibt das einen sinnvollen
   l/100km-Wert. */
function periodStart(art) {
  const jetzt = new Date();
  if (art === "jahr") return new Date(jetzt.getFullYear(), 0, 1);
  if (art === "monat") return new Date(jetzt.getFullYear(), jetzt.getMonth(), 1);
  if (art === "woche") {
    const d = new Date(jetzt); d.setHours(0, 0, 0, 0);
    d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
    return d;
  }
  const d = new Date(jetzt); d.setHours(0, 0, 0, 0); return d;
}
const fahrtenSeit = (art) => TRIPS.filter((t) => t.distance_km != null && new Date(t.ts_start) >= periodStart(art));
const tankSeit = (art) => FILLS.filter((f) => f.liters != null && new Date(f.ts) >= periodStart(art));
function istNachtZeit(ts) {
  const z = new Date(ts).toTimeString().slice(0, 5), von = CONFIG.nachtVon, bis = CONFIG.nachtBis;
  return von > bis ? (z >= von || z < bis) : (z >= von && z < bis);
}
/* D3: Die Strecke führt die Zeile, das Datum steht darunter. Umbruch nur
   zwischen den Orten, nie am Pfeil. */
function strecke(t) {
  const a = t.start_address, b = t.end_address;
  if (!a && !b) return "Fahrt ohne Ortsangabe";
  if (!b) return esc(a);
  if (!a) return esc(b);
  return `${esc(a)}&nbsp;→ ${esc(b)}`;
}

/* A1.1: Der Verbrauch einer einzelnen Fahrt kommt vom Fahrzeug (OBD) und wird
   vom Backend mitgeliefert - er wird hier nie selbst gerechnet. */
const fahrtVerbrauch = (t) => (t && t.verbrauch_l_100km != null ? Number(t.verbrauch_l_100km) : null);
/* Mittelwert der gemeldeten Fahrtverbräuche, nach Distanz gewichtet. */
function schnittFahrtVerbrauch(list) {
  const mit = list.filter((t) => fahrtVerbrauch(t) != null && t.distance_km > 0);
  const km = sum(mit, (t) => t.distance_km);
  return km > 0 ? de(sum(mit, (t) => fahrtVerbrauch(t) * t.distance_km) / km, 1) : "–";
}
/* A1.2: Langzeitverbrauch = alle je getankten Liter geteilt durch alle bis zum
   letzten Tankstop gefahrenen Kilometer. Die frühere Rechnung (Liter der
   Periode ÷ km der Periode) ergab unmögliche Werte, sobald eine Fahrt fehlte
   oder ein Tankvorgang noch unvollständig war. */
function langzeitverbrauch() {
  const tanks = FILLS.filter((f) => f.liters != null).sort((a, b) => new Date(a.ts) - new Date(b.ts));
  if (!tanks.length) return { wert: "–", km: 0, liter: 0, bis: null, basis: "–" };
  const letzter = tanks[tanks.length - 1];
  const bis = new Date(letzter.ts);
  const liter = sum(tanks, (f) => f.liters);
  /* Kilometerbasis: bevorzugt der Kilometerstand an den Tankvorgängen selbst -
     er zählt auch Strecke mit, für die keine Fahrt aufgezeichnet wurde. Fehlt
     er, bleibt die Summe der erfassten Fahrten bis zum letzten Tankstop. */
  const mitOdo = tanks.filter((f) => f.odometer_km != null);
  let km = 0, basis = "erfasste Fahrten";
  if (mitOdo.length >= 2) {
    km = mitOdo[mitOdo.length - 1].odometer_km - mitOdo[0].odometer_km;
    basis = "Kilometerstand an den Tankvorgängen";
  } else {
    km = sum(TRIPS.filter((t) => t.distance_km != null && new Date(t.ts_start) <= bis), (t) => t.distance_km);
  }
  const roh = km > 0 ? (liter / km) * 100 : null;
  /* Plausibilitätsfenster: alles außerhalb gilt als unbekannt, statt einen
     physikalisch unmöglichen Wert als Tatsache anzuzeigen. */
  const gueltig = roh != null && roh >= 3 && roh <= 30;
  return { wert: gueltig ? de(roh, 1) : "–", km, liter, bis, basis, unplausibel: roh != null && !gueltig };
}
function vStat() {
  const jahr = new Date().getFullYear();
  const fJahr = fahrtenSeit("jahr"), fMonat = fahrtenSeit("monat"), fWoche = fahrtenSeit("woche"), fTag = fahrtenSeit("tag");
  const kmJahr = sum(fJahr, (t) => t.distance_km), hJahr = sum(fJahr, (t) => t.duration_s) / 3600;
  const tJahr = tankSeit("jahr");
  const lJahr = sum(tJahr, (f) => f.liters), eJahr = sum(tJahr, (f) => f.fuel_total_eur || 0);
  const tagKm = sum(fJahr.filter((t) => !istNachtZeit(t.ts_start)), (t) => t.distance_km);
  const nachtKm = kmJahr - tagKm;
  const arbeitFahrten = fJahr.filter((t) => t.art === "arbeitsweg"), privatFahrten = fJahr.filter((t) => t.art !== "arbeitsweg");
  const arbeitKm = sum(arbeitFahrten, (t) => t.distance_km), privatKm = kmJahr - arbeitKm;
  const lv = langzeitverbrauch();
  return `
  ${statTile("s1", `Distanz und Fahrzeit ${jahr}`, de(kmJahr), "km", de(hJahr) + " h",
    quad("Gefahrene Distanz", [[de(kmJahr), "Jahr"], [de(sum(fMonat, (t) => t.distance_km)), "Monat"], [de(sum(fWoche, (t) => t.distance_km)), "Woche"], [de(sum(fTag, (t) => t.distance_km)), "Tag"]]) +
    quad("Fahrzeit", [[de(hJahr), "Jahr h"], [de(sum(fMonat, (t) => t.duration_s) / 3600), "Monat h"], [de(sum(fWoche, (t) => t.duration_s) / 3600, 1), "Woche h"], [de(sum(fTag, (t) => t.duration_s) / 3600, 1), "Tag h"]]))}
  ${statTile("s2", `Getankte Menge und Kosten ${jahr}`, de(lJahr), "l", de(eJahr) + " €",
    quad("Getankte Liter", [[de(lJahr), "Jahr"], [de(sum(tankSeit("monat"), (f) => f.liters)), "Monat"], [de(sum(tankSeit("woche"), (f) => f.liters)), "Woche"], [de(sum(tankSeit("tag"), (f) => f.liters)), "Tag"]]) +
    quad("Kraftstoffkosten", [[de(eJahr), "Jahr €"], [de(sum(tankSeit("monat"), (f) => f.fuel_total_eur || 0)), "Monat €"], [de(sum(tankSeit("woche"), (f) => f.fuel_total_eur || 0)), "Woche €"], [de(sum(tankSeit("tag"), (f) => f.fuel_total_eur || 0)), "Tag €"]]))}
  ${statTile("s3", "Langzeitverbrauch", lv.wert, "l/100 km", lv.bis ? "bis " + dedat(lv.bis) : "–",
    `<dl class="rows">
       <div class="row"><dt>Getankt insgesamt</dt><dd>${de(lv.liter, 1)} l</dd></div>
       <div class="row"><dt>Strecke bis zum letzten Tankstop</dt><dd>${de(lv.km)} km<small>${lv.basis}</small></dd></div>
       <div class="row" style="border-bottom:0"><dt>Letzter Tankstop</dt><dd>${lv.bis ? dedat(lv.bis) : "–"}</dd></div>
     </dl>
     ${lv.unplausibel ? `<span class="label" style="margin-top:16px;color:var(--warn)">Aus Litern und Strecke ergibt sich derzeit kein plausibler Wert. Sobald Tankvorgänge und Kilometerstände vollständig vorliegen, erscheint er hier.</span>` : ""}
     ${quad("Vom Fahrzeug gemeldet, je Zeitraum", [[schnittFahrtVerbrauch(fJahr), "Jahr"], [schnittFahrtVerbrauch(fMonat), "Monat"], [schnittFahrtVerbrauch(fWoche), "Woche"], [schnittFahrtVerbrauch(fTag), "Tag"]])}
     <span class="label" style="margin-top:16px">Der Langzeitwert rechnet alle je getankten Liter gegen die Strecke bis zum letzten Tankstop. Die Werte je Zeitraum stammen aus den Verbrauchsmeldungen der einzelnen Fahrten.</span>`)}
  ${statTile("s4", "Tag und Nacht", kmJahr > 0 ? de(Math.round((tagKm / kmJahr) * 100)) : "–", "% bei Tag", de(tagKm) + " km",
    `<div class="bar"><i style="flex:${tagKm};background:var(--fg)"></i><i style="flex:${nachtKm};background:var(--fg3)"></i></div>
     <div class="legend"><span><i style="background:var(--fg)"></i>Tag · ${de(tagKm)} km</span>
       <span><i style="background:var(--fg3)"></i>Nacht · ${de(nachtKm)} km</span></div>
     <span class="label" style="margin-top:16px">Nacht = ${CONFIG.nachtVon} bis ${CONFIG.nachtBis} Uhr</span>`)}
  ${statTile("s5", "Art der Fahrten", kmJahr > 0 ? de(Math.round((arbeitKm / kmJahr) * 100)) : "–", "% Arbeitsweg", de(arbeitKm) + " km",
    `<div class="bar"><i style="flex:${arbeitKm};background:var(--red)"></i><i style="flex:${privatKm};background:var(--fg3)"></i></div>
     <div class="legend"><span><i style="background:var(--red)"></i>Arbeitsweg · ${de(arbeitKm)} km</span>
       <span><i style="background:var(--fg3)"></i>Privat · ${de(privatKm)} km</span></div>
     <dl class="rows" style="margin-top:18px">
       <div class="row"><dt>Fahrten Arbeitsweg</dt><dd>${de(arbeitFahrten.length)}</dd></div>
       <div class="row" style="border-bottom:0"><dt>Fahrten privat</dt><dd>${de(privatFahrten.length)}</dd></div></dl>`)}`;
}

let neuOffen = false;
let fahrtOffen = false;
let tankBearbeitenOffen = false;
// Ergebnis eines geparsten, noch nicht gespeicherten Belegs: { praefix, daten }
// - überlagert in tankFelder() die Beleg-Felder des jeweils offenen Formulars.
let belegEntwurf = null;
// Fehlermeldung aus einem Beleg-Upload (z. B. "Falsches Belegdatum"):
// { context, meldung } - context ist "neuTank"/"bearbTank" (Entwurf) oder
// eine tank_id (Einzelbeleg-Ansicht), je nachdem welcher Beleg-Button es war.
let belegFehler = null;
// Merkt sich beim Auslösen eines nur-parsenden Uploads (neu/bearb), welcher
// Entwurf auf das Ergebnis wartet - die Antwort selbst trägt dafür keine
// praefix-Information, nur tank_id (bei einem Parse-Ergebnis immer null).
let belegZielPraefix = null;

/* Wischen nach links zum Löschen (Fahrten/Tankvorgänge/Servicebuch), analog
   zu iOS-Listen. swipeZeile() umschließt eine bestehende Listenzeile mit
   einem roten "Löschen"-Hintergrund; die eigentliche Wischgeste wird über
   Pointer Events in ereignisseVerdrahten() ausgewertet (nicht Touch Events),
   damit dieselbe Logik auch mit der Maus funktioniert. id folgt demselben
   "art:id"-Schema wie data-go, siehe eintragLoeschen(). */
let swipeAktivWrap = null, swipeStartX = 0, swipeStartY = 0, swipeGeste = null, swipeDX = 0;
let swipeWarGezogen = false;
const SWIPE_BREITE = 84;

/* Ziehen zum Aktualisieren (nur Übersicht) - ptrY0 ist die Y-Position des
   Fingers/der Maus beim Start, null wenn gerade keine Geste läuft. Eigene
   Variablen statt der swipe*-Variablen oben, da beide Gesten unabhängig
   voneinander gleichzeitig "angefasst" werden können (Pointerdown auf einer
   Listenzeile UND ganz oben gescrollt). */
let ptrY0 = null, ptrDrag = 0, ptrLaedt = false;
async function datenAktualisieren() {
  try {
    await HASS.callService("pyscript", "audi_dashboard_jetzt_aktualisieren", {});
    const states = await HASS.callWS({ type: "get_states" });
    const frisch = states.find((s) => s.entity_id === "pyscript.audi_dashboard_fahrzeugstatus");
    if (frisch) NACHGELADEN.status = frisch;
  } catch (e) {
    // bleibt ohne sichtbaren Effekt - der naechste periodische 20s-Takt
    // holt den Stand ohnehin nach
  }
  datenLaden(true);
}
function schliesseAlleSwipes() {
  ROOT.querySelectorAll(".swipe-wrap.swiped").forEach((w) => w.classList.remove("swiped"));
}
function swipeZeile(id, inhaltHtml) {
  return `<div class="swipe-wrap" data-swipe="${id}">
    <div class="swipe-delete"><button class="swipe-loeschen" data-swipe-loeschen="${id}">Löschen</button></div>
    <div class="swipe-content">${inhaltHtml}</div>
  </div>`;
}
const LOESCH_TEXT = {
  trip: "Fahrt löschen?",
  fill: "Tankvorgang löschen?",
  sbuch: "Servicebuch-Eintrag löschen?",
  bild: "Foto löschen?",
};
const LOESCH_HINWEIS = {
  trip: "Die Fahrt wird aus dem Fahrtenbuch entfernt. Das lässt sich nicht widerrufen.",
  fill: "Der Tankvorgang und der zugehörige Beleg werden entfernt. Das lässt sich nicht widerrufen.",
  sbuch: "Der Eintrag wird aus dem Servicebuch entfernt. Das lässt sich nicht widerrufen.",
  bild: "Das Foto wird vom Server gelöscht. Der Platz bleibt bestehen und kann neu belegt werden.",
};
function eintragLoeschen(art, id) {
  if (art === "trip") {
    serviceRufen("audi_dashboard_fahrt_loeschen", { trip_id: id });
    TRIPS = TRIPS.filter((t) => t.trip_id !== id);
  } else if (art === "fill") {
    serviceRufen("audi_dashboard_tankvorgang_loeschen", { tank_id: id });
    FILLS = FILLS.filter((f) => f.tank_id !== id);
  } else if (art === "sbuch") {
    CAR.service.buch.splice(parseInt(id, 10), 1);
    profilSpeichern();
  }
}

/* Index eines gerade per "Eintrag hinzufügen" angelegten, aber noch nicht
   bestätigten Servicebuch-Eintrags. Nur solange gesetzt, wie die Seite
   offen ist und nicht gespeichert wurde - siehe data-sbneu/data-sbspeichern
   und den Verwerfen-Schritt in go(). */
let entwurfIndex = null;
const SERVICE_ARTEN = ["Ölwechsel", "Inspektion", "Hauptuntersuchung", "Weitere"];

/* Update-Funktion (Einstellungen -> "Update suchen", siehe
   updateverwaltung.py). HASS.states wird nach einem Service-Aufruf nicht
   zuverlässig sofort nachgeführt (derselbe Effekt wie beim Nachlade-
   Fallback oben) - deshalb der frische Stand aktiv per WebSocket geholt statt
   passiv auf den nächsten hass-Push zu warten. */
let UPDATE_STATUS = null;
let UPDATE_STATUS_GEHOLT = false;
let updatePruefend = false;
let updateInstallierend = false;
async function updateStatusAbrufen() {
  try {
    const states = await HASS.callWS({ type: "get_states" });
    const s = states.find((x) => x.entity_id === "pyscript.audi_dashboard_update_status");
    UPDATE_STATUS = (s && s.attributes && s.attributes.daten) || null;
  } catch (e) {
    // zeigt dann weiterhin den zuletzt bekannten Stand
  }
}

/* Auswahl für "Modell" unter Einrichten - setzt nur CONFIG.fahrzeugtitel
   (Zeile "Modell" in der Identität, Kopfzeilen, Kalender-Einträge, und den
   Schriftzug unter dem Bild auf der Übersicht). Zusatz, Untertitel, Details
   und die technischen Daten hängen bewusst nicht daran - die bleiben, auch
   bei einem anderen Modellnamen, wie zuletzt im Profil eingetragen.
   Beschränkt auf die Modelle, zu denen ein echtes Badge vorliegt (siehe
   BADGE_DATEIEN/www/badges/) - für alles andere gäbe es sonst ein falsches
   oder gar kein Badge auf der Übersicht. */
const FAHRZEUG_MODELLE = [
  "RS 3 Sportback", "RS 3 Limousine", "RS 4 Avant",
  "RS 5 Coupé", "RS 5 Sportback", "RS 6 Avant", "RS 6 Avant performance",
  "RS 7 Sportback", "S3 Sportback", "S3 Limousine", "SQ7",
];

/* Badge-Zuordnung (www/badges/<datei>-positive.svg / -negative.svg, jeweils
   das offizielle Audi-Markenasset). Schlüssel sind eindeutige Präfixe der
   FAHRZEUG_MODELLE-Einträge - Reihenfolge ist hier egal, da sich keiner der
   Präfixe mit einem anderen überschneidet. */
const BADGE_DATEIEN = {
  "RS 3": "rs3", "RS 4": "rs4", "RS 5": "rs5", "RS 6": "rs6", "RS 7": "rs7",
  "S3": "s3", "SQ7": "sq7",
};
function badgePraefix() {
  const titel = CONFIG.fahrzeugtitel || "";
  return Object.keys(BADGE_DATEIEN).find((p) => titel.startsWith(p)) || null;
}

/* Gemeinsames Formular für "Tankvorgang erfassen" und die Bearbeitung eines
   bestehenden Belegs (Einzelseite) - dieselben Felder an beiden Stellen,
   damit sie nicht auseinanderlaufen können. Preis je Liter ist bewusst kein
   Eingabefeld, sondern wird aus Kosten/Liter berechnet (live über den
   input-Handler in ereignisseVerdrahten, endgültig beim Speichern). */
const KRAFTSTOFFSORTEN = ["V-Power Racing", "Super Plus", "Super"];

/* Vorschlag für "Gefahrene Distanz" bei einer NEUEN Erfassung: Differenz zum
   zeitlich letzten bereits erfassten Tankvorgang in FILLS - dieselbe Regel
   wie backendseitig in _distanz_seit_letzter_tankung() (belegverarbeitung.py),
   hier nur zur Vorbefüllung, da der neue Datensatz noch nicht existiert. Beim
   Bearbeiten eines bestehenden Vorgangs kommt der Wert dagegen direkt aus
   f.distance_km (siehe Aufrufer). */
function distanzVorschlag(aktuellerKm) {
  if (aktuellerKm === "" || aktuellerKm == null || !FILLS.length) return "";
  const letzte = FILLS.reduce((a, b) => (new Date(b.ts) > new Date(a.ts) ? b : a));
  if (letzte.odometer_km == null) return "";
  return Math.round((aktuellerKm - letzte.odometer_km) * 10) / 10;
}

function tankFelder(praefix, f) {
  // Ein zuvor hochgeladener, noch nicht gespeicherter Beleg (belegEntwurf)
  // überlagert hier nur die Beleg-Felder - Kilometerstand/Distanz kommen
  // immer vom Fahrzeug bzw. von f, nie vom Beleg (§7.7 Regel 2).
  const b = belegEntwurf && belegEntwurf.praefix === praefix ? belegEntwurf.daten : null;
  const q = b ? { ...(f || {}), ts: b.ts || (f && f.ts), liters: b.liters, fuel_total_eur: b.fuel_total_eur, discount: b.discount, station_name: b.station_name, fuel_type: b.fuel_type } : f;
  const ts = q && q.ts ? new Date(q.ts) : new Date();
  const datum = ts.toISOString().slice(0, 10);
  const zeit = String(ts.getHours()).padStart(2, "0") + ":" + String(ts.getMinutes()).padStart(2, "0");
  const km = f && f.odometer_km != null ? f.odometer_km : (CAR.odoBekannt ? CAR.odo : "");
  const distanz = f ? (f.distance_km != null ? f.distance_km : "") : distanzVorschlag(km);
  const liter = q && q.liters != null ? q.liters : "";
  const kosten = q && q.fuel_total_eur != null ? q.fuel_total_eur : "";
  const ersparnis = q && q.discount != null ? q.discount : "";
  const kraftstoff = q && q.fuel_type ? q.fuel_type : KRAFTSTOFFSORTEN[0];
  const station = q && q.station_name ? q.station_name : "";
  const preisText = q && q.liters && q.fuel_total_eur != null && q.liters > 0
    ? `Preis je Liter: ${de(q.fuel_total_eur / q.liters, 3)} €` : "Preis je Liter: wird berechnet";
  return `
      <div class="feld"><label>Datum</label><input type="date" id="${praefix}Datum" value="${datum}"></div>
      <div class="feld"><label>Uhrzeit</label><input type="time" id="${praefix}Zeit" value="${zeit}"></div>
      <div class="feld"><label>Kilometerstand</label><input type="number" id="${praefix}Km" value="${km}"></div>
      <div class="feld"><label>Gefahrene Distanz</label>
        <span class="mitEinheit"><input type="number" step="1" id="${praefix}Distanz" value="${distanz}" placeholder="0"><span>km</span></span></div>
      <div class="feld"><label>Kraftstoff</label>
        <select id="${praefix}Kraftstoff">${KRAFTSTOFFSORTEN.map((k) => `<option ${k === kraftstoff ? "selected" : ""}>${k}</option>`).join("")}</select></div>
      <div class="feld"><label>Getankte Liter</label><input type="number" step="0.01" id="${praefix}Liter" data-tankpreisfeld="${praefix}" value="${liter}" placeholder="0,00"></div>
      <div class="feld"><label>Kosten (real, nach Rabatt)</label>
        <span class="mitEinheit"><input type="number" step="0.01" id="${praefix}Kosten" data-tankpreisfeld="${praefix}" value="${kosten}" placeholder="0,00"><span>€</span></span></div>
      ${CAR.smartdeal.aktiv ? `<div class="feld"><label>SmartDeal-Ersparnis</label>
        <span class="mitEinheit"><input type="number" step="0.01" id="${praefix}Ersparnis" value="${ersparnis}" placeholder="0,00"><span>€</span></span></div>` : ""}
      <div class="feld" style="border-bottom:0"><label>Tankstelle</label><input id="${praefix}Station" value="${esc(station)}" placeholder="Name"></div>
      <span class="label" style="margin-top:14px" id="${praefix}PreisAnzeige">${preisText}</span>`;
}

function tankFormularWerte(praefix) {
  const liter = parseFloat(($(`#${praefix}Liter`).value || "0").replace(",", "."));
  const kosten = parseFloat(($(`#${praefix}Kosten`).value || "0").replace(",", "."));
  const kmFeld = $(`#${praefix}Km`).value;
  const distanzFeld = $(`#${praefix}Distanz`).value;
  const ersparnisFeld = CAR.smartdeal.aktiv ? $(`#${praefix}Ersparnis`) : null;
  const datum = $(`#${praefix}Datum`).value, zeit = $(`#${praefix}Zeit`).value;
  return {
    ts: new Date(`${datum}T${zeit}:00`).toISOString(),
    liter, kosten,
    km: kmFeld === "" ? null : parseFloat(kmFeld),
    distanz: distanzFeld === "" ? null : parseFloat(distanzFeld),
    ersparnis: ersparnisFeld && ersparnisFeld.value !== "" ? parseFloat(ersparnisFeld.value.replace(",", ".")) : null,
    kraftstoff: $(`#${praefix}Kraftstoff`).value,
    station: $(`#${praefix}Station`).value,
  };
}

function vFuelFormular() {
  // Gleicher Grund wie bei vTripsFormular(): das Formular muss auch im
  // Leer-Zustand erscheinen, nicht nur wenn schon Tankvorgänge vorhanden sind.
  return `${neuOffen ? `<div class="tile"><span class="label">Neuer Tankvorgang</span>
      ${tankFelder("neuTank", null)}
      ${belegFehler && belegFehler.context === "neuTank" ? `<span class="label" style="margin-top:10px;color:var(--red)">${esc(belegFehler.meldung)}</span>` : ""}
      <button class="aktion" data-beleg="neu">Beleg hochladen</button>
      <button class="aktion primaer" data-tankspeichernbeleg>Speichern</button>
      <button class="aktion" data-tankspeichern>Ohne Beleg speichern</button></div>` : ""}
    <button class="aktion ${neuOffen ? "" : "primaer"}" data-neu>${neuOffen ? "Abbrechen" : "Tankvorgang erfassen"}</button>`;
}
function vFuel() {
  const jahre = [...new Set(FILLS.map((f) => new Date(f.ts).getFullYear()))].sort((a, b) => b - a);
  /* D5 */
  if (!jahre.length) return `<div class="tile"><div class="leer">
      <strong>Noch keine Tankvorgänge</strong>
      <p>Ein Tankvorgang wird automatisch angelegt, sobald der Tankstand des Fahrzeugs steigt. Den Beleg kann man danach hochladen – oder den Vorgang gleich hier von Hand erfassen.</p>
    </div></div>
    ${vFuelFormular()}`;
  let h = `<div class="tile">`;
  jahre.forEach((j, ji) => {
    const fj = FILLS.filter((f) => new Date(f.ts).getFullYear() === j);
    const ersparnisJahr = sum(fj, (f) => f.discount || 0);
    /* D2: Auf der Jahreszeile stehen nur die Kosten, groß. Preis und Menge
       folgen als Zweitzeile, die Ersparnis als neutrale Pille - vorher
       konkurrierten drei gleich große rechtsbündige Werte miteinander. */
    h += `<button class="acc-h" aria-expanded="${ji === 0}" data-acc="fy${j}"><span class="mark"></span>
      <span class="k">${j}</span>
      <span class="v" style="display:flex;flex-direction:column;align-items:flex-end;gap:5px">
        <span style="font-size:17px;color:var(--fg)">${eur(sum(fj, (f) => f.fuel_total_eur || 0))} €</span>
        <span>${de(sum(fj, (f) => f.liters || 0), 1)} l · Ø ${de(schnittpreis(fj), 3)} €/l</span>
        ${CAR.smartdeal.aktiv ? `<span class="marke"><span class="pill" style="font-size:11.5px;padding:4px 10px">${markenlogo()}<span style="margin-left:6px">−${eur(ersparnisJahr)} €</span></span></span>` : ""}
      </span></button>
      <div class="acc-b ${ji === 0 ? "on" : ""}" id="fy${j}">`;
    const monate = [...new Set(fj.map((f) => new Date(f.ts).toLocaleDateString("de-DE", { month: "long" })))];
    monate.forEach((m, mi) => {
      const fm = fj.filter((f) => new Date(f.ts).toLocaleDateString("de-DE", { month: "long" }) === m);
      h += `<div class="lvl2"><button class="acc-h" aria-expanded="${mi === 0}" data-acc="fm${j}${m}">
        <span class="mark"></span><span class="k">${m}</span>
        <span class="v" style="display:flex;flex-direction:column;align-items:flex-end;gap:3px">
          <span style="color:var(--fg)">${eur(sum(fm, (f) => f.fuel_total_eur || 0))} €</span>
          <span>Ø ${de(schnittpreis(fm), 3)} €/l</span></span></button>
        <div class="acc-b ${mi === 0 ? "on" : ""}" id="fm${j}${m}">`;
      fm.forEach((f) => { const preis = f.liters ? f.fuel_total_eur / f.liters : null; h += swipeZeile(`fill:${f.tank_id}`, `<button class="leaf" data-go="fill:${f.tank_id}">
        <span class="k">${dedat(new Date(f.ts))}<small>${esc(f.station_name || "")}</small></span>
        <span class="v">${f.liters != null ? de(f.liters, 2) + " l" : ""}<small>${preis != null ? de(preis, 3) + " €/l" : ""}</small></span>
        <svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); });
      h += `</div></div>`;
    });
    h += `</div>`;
  });
  return h + `</div>
    ${vFuelFormular()}`;
}

function vFill(id) {
  const f = FILLS.find((x) => x.tank_id === id);
  if (!f) return `<div class="tile">Tankvorgang nicht gefunden</div>`;
  const berechneterPreis = f.liters ? f.fuel_total_eur / f.liters : null;

  if (tankBearbeitenOffen) {
    return `
    <div class="tile"><span class="label">Tankvorgang bearbeiten</span>
      ${tankFelder("bearbTank", f)}
      ${belegFehler && belegFehler.context === "bearbTank" ? `<span class="label" style="margin-top:10px;color:var(--red)">${esc(belegFehler.meldung)}</span>` : ""}
      <button class="aktion" data-beleg="bearb">Beleg hochladen</button>
      <button class="aktion" data-tankaktualisieren="${f.tank_id}">Speichern</button>
      <button class="aktion" data-tankbearbeiten>Abbrechen</button>
    </div>`;
  }

  return `
  ${mapBox()}
  <div class="tile flat" style="margin:18px 0 14px">
    <span class="label">Getankt</span>
    <div class="fig" style="font-size:52px;margin-top:10px">${f.liters != null ? de(f.liters, 2) : "–"}<span class="unit">l</span></div>
  </div>
  <div class="tile"><dl class="rows rows-fest">
    <div class="row"><dt>Zeitpunkt</dt><dd>${dedat(new Date(f.ts))}<small>${new Date(f.ts).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })} Uhr</small></dd></div>
    <div class="row"><dt>Kilometerstand</dt><dd>${f.odometer_km != null ? de(f.odometer_km) + " km" : "unbekannt"}</dd></div>
    <div class="row"><dt>Gefahrene Distanz</dt><dd>${f.distance_km != null ? de(f.distance_km) + " km" : "unbekannt"}<small>seit der letzten Tankung</small></dd></div>
    <div class="row"><dt>Kraftstoff</dt><dd>${esc(f.fuel_type || KRAFTSTOFFSORTEN[0])}</dd></div>
    <div class="row"><dt>Getankte Liter</dt><dd>${f.liters != null ? de(f.liters, 2) + " l" : "–"}</dd></div>
    <div class="row"><dt>Kosten</dt><dd>${f.fuel_total_eur != null ? eur(f.fuel_total_eur) + " €" : "–"}<small>real, nach Rabatt</small></dd></div>
    <div class="row"><dt>Preis je Liter</dt><dd>${berechneterPreis != null ? de(berechneterPreis, 3) + " €" : "–"}<small>Kosten ÷ Liter</small></dd></div>
    ${CAR.smartdeal.aktiv && f.discount != null ? `<div class="row"><dt><span class="marke">${markenlogo()} SmartDeal-Ersparnis</span></dt><dd>${eur(f.discount)} €</dd></div>` : ""}
    <div class="row" style="border-bottom:0"><dt>Tankstelle</dt><dd>${esc(f.station_name || "unbekannt")}<small>${esc(f.station_address || "")}</small></dd></div>
  </dl>
  ${belegFehler && belegFehler.context === f.tank_id ? `<span class="label" style="margin-top:14px;color:var(--red)">${esc(belegFehler.meldung)}</span>` : ""}
  <button class="aktion" data-tankbearbeiten>Werte bearbeiten</button>
  <button class="aktion" data-beleg="${f.tank_id}">${f.receipt_file ? "Beleg ersetzen" : "Beleg nachtragen"}</button>
  <button class="aktion loeschen" data-filldel="${f.tank_id}">Tankvorgang löschen</button>
  </div>`;
}

/* ---------------------------------------------------------- Steuerung */
const ICONS = {
  home: '<path d="M4 17a9 9 0 0 1 18 0"/><path d="M13 17l5.5-5"/><circle cx="13" cy="17" r="1.3"/>',
  audi: '<polygon points="21.6,5 16.5,19 2.4,19 7.5,5" transform="translate(0,0)"/>',
  trips: '<circle cx="6" cy="6" r="2.2"/><circle cx="18" cy="18" r="2.2"/><path d="M8 7.5c4 1.5 4.5 3 3 5s-1 3.5 4.5 5"/>',
  stat: '<path d="M4 20V9.5M10 20V4.5M16 20v-8M22 20H2"/>',
  fuel: '<path d="M4.5 20V6.2c0-.9.8-1.7 1.7-1.7h5.6c1 0 1.7.8 1.7 1.7V20"/><path d="M3 20h12.5"/><path d="M4.5 11h10.5"/><path d="M17 20v-8.5a2 2 0 0 0-2-2"/><path d="M17 11.5V7.8l-2-2.3"/>',
};
const TABS = [
  { id: "home", label: "Übersicht", eyebrow: "", title: "Übersicht" },
  { id: "audi", label: "Mein Audi", eyebrow: "", title: "" },
  { id: "trips", label: "Fahrten", eyebrow: "", title: "Fahrten" },
  { id: "stat", label: "Statistik", eyebrow: "", title: "Statistik" },
  { id: "fuel", label: "Tanken", eyebrow: "", title: "Tanken" },
];
let route = { name: "home" };
const ZURUECK = {
  trip: "trips", fill: "fuel", service: "audi", reifen: "audi", ident: "audi",
  vers: "audi", beitrag: "vers", vertragsdetails: "vers", schutz: "vertragsdetails",
  steuer: "vers", notruf: "vers", werkstatt: "service", sbuch: "service", einst: "home",
  battverlauf: "audi",
  /* Fehlte: die Sicherheitsansicht war eine Sackgasse ohne sichtbaren
     Zurück-Pfeil. */
  sicherheit: "home",
};
const ICON = {
  sonne: '<circle cx="10" cy="10" r="4"/><path d="M10 1v2M10 17v2M1 10h2M17 10h2M3.9 3.9l1.4 1.4M14.7 14.7l1.4 1.4M16.1 3.9l-1.4 1.4M5.3 14.7l-1.4 1.4"/>',
  mond: '<path d="M16.5 12.4A7 7 0 1 1 7.6 3.5a5.6 5.6 0 0 0 8.9 8.9z"/>',
};
function themeIcon() { const el = $("#themeicon"); if (el) el.innerHTML = HOST.dataset.theme === "nacht" ? ICON.sonne : ICON.mond; }

/* A6: gespeicherte Wahl, sonst Systemeinstellung. */
const THEME_KEY = "audi-dashboard-theme";
function themeGewaehlt() {
  let gespeichert = null;
  try { gespeichert = localStorage.getItem(THEME_KEY); } catch (e) { /* Privatmodus */ }
  if (gespeichert === "nacht" || gespeichert === "tag") return gespeichert;
  return window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches ? "tag" : "nacht";
}
function themeSetzen(modus) {
  HOST.dataset.theme = modus;
  try { localStorage.setItem(THEME_KEY, modus); } catch (e) { /* Privatmodus */ }
  tiles();
}

/* ------------------------------------------------------------ Action Sheet
   B4: ersetzt confirm()/alert(). Ein offenes Sheet ist Zustand wie jeder
   andere - render() zeichnet es mit, damit es Ansichtswechsel überlebt. */
let sheet = null;
const sheetSchliessen = () => { sheet = null; render(); };
function bestaetigen(titel, text, knopf, aktion, destruktiv = true) {
  sheet = { titel, text, knopf, aktion, destruktiv };
  render();
}
function hinweis(titel, text) {
  sheet = { titel, text, knopf: null, aktion: null };
  render();
}
function sheetMarkup() {
  if (!sheet) return "";
  const kopf = `<div class="sheet-kopf"><strong>${esc(sheet.titel)}</strong>${sheet.text ? `<span>${esc(sheet.text)}</span>` : ""}</div>`;
  const aktion = sheet.knopf
    ? `<button data-sheet-ok class="${sheet.destruktiv ? "loeschen" : ""}">${esc(sheet.knopf)}</button>`
    : `<button data-sheet-abbrechen>Verstanden</button>`;
  return `<div class="sheet-catcher" data-sheet-abbrechen></div>
  <div class="sheet" role="dialog" aria-modal="true">
    <div class="sheet-gruppe">${kopf}${aktion}</div>
    ${sheet.knopf ? `<button class="sheet-abbrechen" data-sheet-abbrechen>Abbrechen</button>` : ""}
  </div>`;
}

let letzteAnsicht = null;
let routeStack = [];
let navRichtung = null;
const istTab = (name) => TABS.some((t) => t.id === name);
function render() {
  if (!DATEN_GELADEN) return;
  // Eine laufende Wischgeste bezieht sich auf Knoten, die render() gleich
  // durch neue ersetzt - Referenz kappen, bevor sie ins Leere zeigt.
  swipeAktivWrap = null; swipeGeste = null; swipeDX = 0;
  const v = $("#view"), b = $("#back");
  const merkeScroll = v.scrollTop;
  const gleicheAnsicht = letzteAnsicht === route.name + "|" + (route.id || "");
  let head;
  if (route.name === "home") { head = ["", "Übersicht"]; v.innerHTML = vHome(); }
  else if (route.name === "trip") { head = ["Fahrt", "Einzelfahrt"]; v.innerHTML = vTrip(route.id); }
  else if (route.name === "fill") { head = ["Tankvorgang", "Einzelbeleg"]; v.innerHTML = vFill(route.id); }
  else if (route.name === "service") { head = ["Wartung", "Service"]; v.innerHTML = vService(); }
  else if (route.name === "reifen") { head = ["Fahrzeug", "Reifen"]; v.innerHTML = vReifen(); }
  else if (route.name === "einst") { head = ["App", "Einstellungen"]; v.innerHTML = vEinst(); }
  else if (route.name === "ident") { head = ["Fahrzeug", "Identität und Technik"]; v.innerHTML = vIdent(); }
  else if (route.name === "sicherheit") { head = ["Fahrzeug", "Sicherheit"]; v.innerHTML = vSicherheit(); }
  else if (route.name === "vers") { head = ["Fahrzeug", "Versicherung/Steuer"]; v.innerHTML = vVers(); }
  else if (route.name === "beitrag") { head = ["Versicherung", "Beitrag anpassen"]; v.innerHTML = vBeitrag(); }
  else if (route.name === "vertragsdetails") { head = ["Versicherung", "Vertragsdetails"]; v.innerHTML = vVertragsdetails(); }
  else if (route.name === "steuer") { head = ["Versicherung", "Kfz-Steuer"]; v.innerHTML = vSteuerBearbeiten(); }
  else if (route.name === "notruf") { head = ["Versicherung", "Rufnummern"]; v.innerHTML = vNotrufBearbeiten(); }
  else if (route.name === "schutz") { head = ["Im Ernstfall", "Schutzbrief"]; v.innerHTML = vSchutz(); }
  else if (route.name === "werkstatt") { head = ["Service", "Autohaus"]; v.innerHTML = vWerkstatt(); }
  else if (route.name === "battverlauf") { head = ["Fahrzeug", "Batteriespannung"]; v.innerHTML = vBatterieverlauf(); bvZeichnen(); }
  else if (route.name === "sbuch") { head = ["Servicebuch", "Eintrag"]; v.innerHTML = vSbuch(); }
  else { const t = TABS.find((x) => x.id === route.name); head = [t.eyebrow, t.id === "audi" ? "Mein Audi" : t.title];
    v.innerHTML = { audi: vAudi, trips: vTrips, stat: vStat, fuel: vFuel }[route.name](); }
  $("#eyebrow").textContent = head[0]; $("#title").textContent = head[1];
  /* B3: Push nach rechts hinein, Pop nach links heraus. */
  v.classList.remove("push-rein", "push-raus");
  if (navRichtung && !gleicheAnsicht) {
    void v.offsetWidth;
    v.classList.add(navRichtung === "raus" ? "push-raus" : "push-rein");
  }
  navRichtung = null;
  ROOT.getElementById("overlay").innerHTML = sheetMarkup();
  /* D4: die Ringe sind nur auf den Tab-Ebenen sichtbar - auf Unterseiten steht
     dort der Zurück-Pfeil. */
  ROOT.getElementById("marke").style.display = istTab(route.name) ? "inline-flex" : "none";
  const sy = $("#sync");
  sy.style.display = route.name === "home" ? "flex" : "none";
  sy.querySelector("span").textContent = standAlterText();
  b.classList.toggle("on", !istTab(route.name));
  v.scrollTop = gleicheAnsicht ? merkeScroll : 0;
  letzteAnsicht = route.name + "|" + (route.id || "");
  if (MAP) { MAP.remove(); MAP = null; TILE = null; }
  if (route.name === "trip" || route.name === "fill") initMap().catch(() => {}); // ohne Internet bleibt die Kartenflaeche leer
  ROOT.querySelectorAll(".tab").forEach((el) => el.classList.toggle("on", el.dataset.tab === route.name));
}
const go = (name, id) => {
  // Ein unbestätigter Servicebuch-Entwurf (siehe data-sbneu) verlässt die
  // Anwendung nie ungespeichert - wird die Eintragsseite ohne Klick auf
  // "Speichern" verlassen, verwerfen wir ihn hier statt ihn liegen zu lassen
  // und beim nächsten unabhängigen Speichern versehentlich mit zu persistieren.
  if (entwurfIndex !== null && !(name === "sbuch" && id === String(entwurfIndex))) {
    CAR.service.buch.splice(entwurfIndex, 1);
    entwurfIndex = null;
  }
  sheet = null;
  /* B3: echter Verlauf statt statischer ZURUECK-Tabelle. Ein Tabwechsel setzt
     den Stapel zurück, weil Tabs Wurzeln sind, keine Ebenen. */
  if (istTab(name)) routeStack = [];
  else if (route.name !== name || route.id !== id) routeStack.push(route);
  navRichtung = istTab(name) ? null : "rein";
  route = { name, id }; render();
};
function zurueck() {
  sheet = null;
  const ziel = routeStack.pop() || { name: ZURUECK[route.name] || "home" };
  navRichtung = "raus";
  route = ziel; render();
}

/* B3: Randwischen zurück - dieselbe Bewegung wie ein Tipp auf den Pfeil. */
let randX0 = null, randY0 = null;
function randwischenVerdrahten() {
  const phone = ROOT.querySelector(".phone");
  phone.addEventListener("pointerdown", (e) => {
    randX0 = null;
    if (!e.isPrimary || sheet) return;
    const links = phone.getBoundingClientRect().left;
    if (e.clientX - links <= 26 && !istTab(route.name)) { randX0 = e.clientX; randY0 = e.clientY; }
  });
  phone.addEventListener("pointerup", (e) => {
    if (randX0 !== null && e.clientX - randX0 > 62 && Math.abs(e.clientY - randY0) < 60) zurueck();
    randX0 = null;
  });
  phone.addEventListener("pointercancel", () => { randX0 = null; });
}

/* -------------------------------------------------------------- Aktionen
   Schreibende Aktionen: lokale Anzeige sofort aktualisieren (render()),
   Persistenz über hass.callService im Hintergrund - dieselbe Reihenfolge
   wie im Prototyp, nur dass jetzt zusätzlich gespeichert wird. */
function serviceRufen(dienst, daten) {
  HASS.callService("pyscript", dienst, daten).catch((err) => console.error("audi_dashboard:", dienst, err));
}

function ereignisseVerdrahten() {
  ROOT.getElementById("tabbar").innerHTML = TABS.map((t) =>
    `<button class="tab" data-tab="${t.id}">
       <svg viewBox="0 0 24 24" width="21" height="21" aria-hidden="true">${ICONS[t.id]}</svg><span>${t.label}</span>
     </button>`).join("");
  ROOT.getElementById("tabbar").classList.toggle("ohne", !beschriftung);
  ROOT.getElementById("tabbar").addEventListener("click", (e) => { const b = e.target.closest(".tab"); if (b) go(b.dataset.tab); });
  ROOT.getElementById("back").addEventListener("click", () => zurueck());
  ROOT.getElementById("ringsbtn").addEventListener("click", () => go("einst"));
  ROOT.getElementById("theme").addEventListener("click", () => {
    themeSetzen(HOST.dataset.theme === "nacht" ? "tag" : "nacht");
    themeIcon();
  });
  // Wischgeste über Pointer Events (nicht Touch Events), damit dieselbe
  // Logik auch mit der Maus funktioniert (z. B. beim Testen im Browser).
  // touch-action:pan-y auf .swipe-content lässt vertikales Scrollen nativ
  // zu; erst wenn die Geste als horizontal erkannt ist, übernimmt hier
  // preventDefault() die Kontrolle.
  ROOT.addEventListener("pointerdown", (e) => {
    // Ziehen zum Aktualisieren (nur Übersicht, nur wenn ganz oben gescrollt) -
    // unabhängig von der Wischgeste unten, da hier keine .swipe-wrap nötig ist.
    if (e.isPrimary && route.name === "home" && !ptrLaedt) {
      const ansicht = $("#view");
      if (ansicht && ansicht.scrollTop <= 0) { ptrY0 = e.clientY; ptrDrag = 0; }
    }
    const wrap = e.target.closest(".swipe-wrap");
    if (!wrap || !e.isPrimary) return;
    swipeAktivWrap = wrap; swipeStartX = e.clientX; swipeStartY = e.clientY; swipeGeste = null; swipeDX = 0;
  });
  ROOT.addEventListener("pointermove", (e) => {
    if (ptrY0 !== null && e.isPrimary) {
      const ansicht = $("#view"), dy = e.clientY - ptrY0;
      if (dy > 0 && ansicht && ansicht.scrollTop <= 0) {
        e.preventDefault();
        ptrDrag = Math.min(90, dy * 0.5); // Widerstand, wie bei iOS
        const ptr = $("#ptr");
        if (ptr) {
          ptr.style.transition = "none";
          ptr.style.height = ptrDrag + "px";
          ptr.querySelector("span").textContent = ptrDrag >= 56 ? "Loslassen zum Aktualisieren" : "Ziehen zum Aktualisieren";
        }
      } else {
        ptrY0 = null; ptrDrag = 0;
        const ptr = $("#ptr");
        if (ptr) { ptr.style.transition = ""; ptr.style.height = "0"; }
      }
    }
    if (!swipeAktivWrap || !e.isPrimary) return;
    const dx = e.clientX - swipeStartX, dy = e.clientY - swipeStartY;
    if (swipeGeste === null) {
      if (Math.abs(dx) < 6 && Math.abs(dy) < 6) return;
      swipeGeste = Math.abs(dx) > Math.abs(dy) ? "x" : "y";
      if (swipeGeste === "y") { swipeAktivWrap = null; return; }
    }
    e.preventDefault();
    swipeWarGezogen = true;
    const basis = swipeAktivWrap.classList.contains("swiped") ? -SWIPE_BREITE : 0;
    swipeDX = Math.max(-SWIPE_BREITE - 20, Math.min(0, basis + dx));
    const inhalt = swipeAktivWrap.querySelector(".swipe-content");
    inhalt.style.transition = "none";
    inhalt.style.transform = `translateX(${swipeDX}px)`;
  }, { passive: false });
  function swipeBeenden() {
    if (!swipeAktivWrap) return;
    const inhalt = swipeAktivWrap.querySelector(".swipe-content");
    inhalt.style.transition = ""; inhalt.style.transform = "";
    if (swipeGeste === "x") {
      if (swipeDX <= -SWIPE_BREITE / 2) { schliesseAlleSwipes(); swipeAktivWrap.classList.add("swiped"); }
      else { swipeAktivWrap.classList.remove("swiped"); }
    }
    swipeAktivWrap = null; swipeGeste = null; swipeDX = 0;
    // Der nachlaufende Klick derselben Geste feuert synchron noch in diesem
    // Tick (Maus: immer: Touch: falls vom Browser nicht ohnehin unterdrückt) -
    // erst danach zurücksetzen, sonst würde ein echter, späterer Tipp (z. B.
    // auf den gerade erst aufgedeckten Löschen-Knopf) fälschlich verschluckt.
    if (swipeWarGezogen) setTimeout(() => { swipeWarGezogen = false; }, 0);
  }
  function ptrBeenden() {
    if (ptrY0 === null) return;
    ptrY0 = null;
    const ptr = $("#ptr"); if (!ptr) { ptrDrag = 0; return; }
    ptr.style.transition = "height .2s";
    if (ptrDrag >= 56) {
      ptrLaedt = true;
      ptr.style.height = "56px";
      ptr.classList.add("spin");
      ptr.querySelector("span").textContent = "Aktualisieren …";
      datenAktualisieren().finally(() => {
        ptrLaedt = false;
        const ptr2 = $("#ptr");
        if (ptr2) { ptr2.style.transition = "height .2s"; ptr2.style.height = "0"; ptr2.classList.remove("spin"); }
      });
    } else {
      ptr.style.height = "0";
    }
    ptrDrag = 0;
  }
  ROOT.addEventListener("pointerup", (e) => { swipeBeenden(); ptrBeenden(); });
  ROOT.addEventListener("pointercancel", (e) => { swipeBeenden(); ptrBeenden(); });

  // Batteriespannungs-Verlauf: Pinch-Zoom (2 Finger) / Ziehen zum Verschieben
  // (1 Finger) / Mausrad-Zoom, per pointerId in bvZeiger nachverfolgt statt
  // über die swipe*-Variablen oben, die für horizontales Wischen in Listen
  // gedacht sind und mit einer zweiten aktiven Berührung nichts anfangen
  // können.
  ROOT.addEventListener("pointerdown", (e) => {
    if (!e.target.closest("#bvChart") || !bvDomain) return;
    bvZeiger.set(e.pointerId, { x: e.clientX, y: e.clientY });
    if (bvZeiger.size === 1) {
      bvTapKandidat = { x: e.clientX, y: e.clientY };
      bvBewegtSeit = false;
      bvGestStart = { typ: "pan", x: e.clientX, domain: [...bvDomain] };
    } else if (bvZeiger.size === 2) {
      bvTapKandidat = null;
      const [p1, p2] = [...bvZeiger.values()];
      const svg = $("#bvSvg"), rect = svg.getBoundingClientRect(), rahmen = bvPlotRahmen(rect);
      const mitteX = (p1.x + p2.x) / 2;
      const ankerFrac = Math.max(0, Math.min(1, (mitteX - rahmen.linksPx) / rahmen.breitePx));
      bvGestStart = {
        typ: "pinch", dist: Math.hypot(p1.x - p2.x, p1.y - p2.y), domain: [...bvDomain],
        ankerFrac, ankerZeit: bvDomain[0] + ankerFrac * (bvDomain[1] - bvDomain[0]),
      };
    }
  });
  ROOT.addEventListener("pointermove", (e) => {
    if (!bvZeiger.has(e.pointerId) || !bvGestStart) return;
    bvZeiger.set(e.pointerId, { x: e.clientX, y: e.clientY });
    e.preventDefault();
    if (bvGestStart.typ === "pan" && bvZeiger.size === 1) {
      const dxPx = e.clientX - bvGestStart.x;
      if (Math.abs(dxPx) > 4) bvBewegtSeit = true;
      const svg = $("#bvSvg"), rect = svg.getBoundingClientRect();
      const breite = bvGestStart.domain[1] - bvGestStart.domain[0];
      const dt = -(dxPx / bvPlotRahmen(rect).breitePx) * breite;
      bvDomain = bvDomainKlemmen([bvGestStart.domain[0] + dt, bvGestStart.domain[1] + dt]);
      bvZeichnen();
    } else if (bvGestStart.typ === "pinch" && bvZeiger.size === 2) {
      const [p1, p2] = [...bvZeiger.values()];
      const dist = Math.hypot(p1.x - p2.x, p1.y - p2.y);
      const faktor = bvGestStart.dist / Math.max(dist, 1);
      const breite = (bvGestStart.domain[1] - bvGestStart.domain[0]) * faktor;
      const start = bvGestStart.ankerZeit - bvGestStart.ankerFrac * breite;
      bvDomain = bvDomainKlemmen([start, start + breite]);
      bvZeichnen();
    }
  }, { passive: false });
  function bvBeenden(e) {
    if (!bvZeiger.has(e.pointerId)) return;
    bvZeiger.delete(e.pointerId);
    if (bvZeiger.size === 0) {
      if (bvGestStart && bvGestStart.typ === "pan" && !bvBewegtSeit && bvTapKandidat) {
        bvAusgewaehlt = bvNaechsterPunkt(bvTapKandidat.x);
        bvZeichnen();
      }
      bvGestStart = null; bvTapKandidat = null;
    } else if (bvZeiger.size === 1) {
      // von Pinch (2 Finger) auf Pan (1 Finger verbleibend) wechseln, statt
      // die Geste einfach zu beenden - fühlt sich beim Loslassen eines
      // Fingers sonst wie ein Ruckler an
      const [rest] = [...bvZeiger.entries()];
      bvGestStart = { typ: "pan", x: rest[1].x, domain: [...bvDomain] };
      bvBewegtSeit = true;
    }
  }
  ROOT.addEventListener("pointerup", bvBeenden);
  ROOT.addEventListener("pointercancel", bvBeenden);
  ROOT.addEventListener("wheel", (e) => {
    if (!e.target.closest("#bvChart") || !bvDomain) return;
    e.preventDefault();
    const svg = $("#bvSvg"), rect = svg.getBoundingClientRect(), rahmen = bvPlotRahmen(rect);
    const frac = Math.max(0, Math.min(1, (e.clientX - rahmen.linksPx) / rahmen.breitePx));
    const ankerZeit = bvDomain[0] + frac * (bvDomain[1] - bvDomain[0]);
    const faktor = e.deltaY > 0 ? 1.2 : 1 / 1.2;
    const breite = (bvDomain[1] - bvDomain[0]) * faktor;
    const start = ankerZeit - frac * breite;
    bvDomain = bvDomainKlemmen([start, start + breite]);
    bvZeichnen();
  }, { passive: false });

  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; }
    if (e.target.closest("[data-sheet-ok]")) {
      const aktion = sheet && sheet.aktion;
      sheet = null; render();
      if (aktion) aktion();
      return;
    }
    if (sheet) return;
    const bvReset = e.target.closest("[data-bvreset]");
    if (bvReset) { bvDomain = [...bvVollDomain]; bvAusgewaehlt = null; bvZeichnen(); return; }
    // Nachlaufender Klick derselben Geste (Maus feuert ihn trotz Bewegung
    // weiterhin) - nicht als Navigation/Aktion werten.
    if (swipeWarGezogen) { swipeWarGezogen = false; return; }
    const loeschBtn = e.target.closest("[data-swipe-loeschen]");
    const offenerWrap = ROOT.querySelector(".swipe-wrap.swiped");
    if (offenerWrap && !loeschBtn) {
      // Erster Tipp bei offener Wischfläche schließt sie nur - wie bei iOS,
      // egal ob innerhalb der Zeile (statt zu navigieren) oder außerhalb.
      schliesseAlleSwipes();
      if (offenerWrap.contains(e.target)) return;
    }
    if (loeschBtn) {
      const [art, id] = loeschBtn.dataset.swipeLoeschen.split(":");
      bestaetigen(LOESCH_TEXT[art], LOESCH_HINWEIS[art], "Löschen", () => { eintragLoeschen(art, id); schliesseAlleSwipes(); });
      return;
    }
    const tripDel = e.target.closest("[data-tripdel]");
    if (tripDel) {
      const id = tripDel.dataset.tripdel;
      bestaetigen(LOESCH_TEXT.trip, LOESCH_HINWEIS.trip, "Löschen", () => { eintragLoeschen("trip", id); go("trips"); });
      return;
    }
    const fillDel = e.target.closest("[data-filldel]");
    if (fillDel) {
      const id = fillDel.dataset.filldel;
      bestaetigen(LOESCH_TEXT.fill, LOESCH_HINWEIS.fill, "Löschen", () => { eintragLoeschen("fill", id); go("fuel"); });
      return;
    }
    const sbDel = e.target.closest("[data-sbdel]");
    if (sbDel) {
      const id = sbDel.dataset.sbdel;
      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("pyscript", "audi_dashboard_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.
    const bildKlick = e.target.closest("[data-bildklick]");
    if (bildKlick) {
      const dateiname = bildKlick.dataset.bildklick;
      const box = bildKlick.querySelector(".bildbox");
      const container = bildKlick.closest(".tile, .bildslot");
      if (box && box.classList.contains("bild-fehlt")) {
        const input = container.querySelector(`[data-bildupload="${dateiname}"]`);
        if (input) input.click();
      } else {
        bildMenuOffen = dateiname; render();
      }
      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, .bildslot");
      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;
      if (!datum) return;
      CAR.smartdeal.aktiv = true; CAR.smartdeal.laeuftAb = datum;
      smartdealPopupOffen = false; profilSpeichern(); render();
      return;
    }
    if (e.target.id === "carimg") { bildWeiter(); return; }
    if (e.target.closest("[data-sbneu]")) {
      CAR.service.buch.unshift({ datum: dedat(heute()), km: CAR.odoBekannt ? CAR.odo : 0, art: SERVICE_ARTEN[0], werkstatt: CAR.service.autohaus.name, kosten: null, arbeiten: "" });
      entwurfIndex = 0; go("sbuch", "0"); return;
    }
    if (e.target.closest("[data-sbspeichern]")) {
      entwurfIndex = null; profilSpeichern(); go("service"); return;
    }
    if (e.target.closest("[data-nmauf]")) { nmOffen = !nmOffen; render(); return; }
    if (e.target.closest("[data-einrichten-auf]")) { einrichtenOffen = !einrichtenOffen; render(); return; }
    if (e.target.closest("[data-nmspeichern]")) {
      const wert = parseInt($("#nm").value, 10);
      if (!isNaN(wert) && wert > 0) { CAR.reifen.anzugsmoment = wert; profilSpeichern(); }
      nmOffen = false; render(); return;
    }
    if (e.target.closest("[data-neu]")) {
      neuOffen = !neuOffen;
      if (belegEntwurf && belegEntwurf.praefix === "neuTank") belegEntwurf = null;
      if (belegFehler && belegFehler.context === "neuTank") belegFehler = null;
      render(); return;
    }
    if (e.target.closest("[data-fahrt]")) { fahrtOffen = !fahrtOffen; render(); return; }
    if (e.target.closest("[data-fahrtspeichern]")) {
      const datum = $("#neuFahrtDatum").value, start = $("#neuFahrtStart").value, dauer = parseInt($("#neuFahrtDauer").value, 10) || 0;
      const tsStart = new Date(`${datum}T${start}:00`);
      const tsEnd = new Date(tsStart.getTime() + dauer * 60000);
      serviceRufen("audi_dashboard_fahrt_manuell_anlegen", { ts_start: tsStart.toISOString(), ts_end: tsEnd.toISOString(), art: "privat" });
      fahrtOffen = false; render(); return;
    }
    if (e.target.closest("[data-tankspeichern]")) {
      const w = tankFormularWerte("neuTank");
      if (w.liter > 0 && w.kosten > 0) {
        serviceRufen("audi_dashboard_tankvorgang_manuell", {
          ts: w.ts, liter: w.liter, kosten: w.kosten, km: w.km, ersparnis: w.ersparnis, station: w.station,
          distanz: w.distanz, kraftstoff: w.kraftstoff,
        });
      }
      neuOffen = false; belegEntwurf = null; belegFehler = null; render(); return;
    }
    if (e.target.closest("[data-tankspeichernbeleg]")) {
      const w = tankFormularWerte("neuTank");
      const beleg = belegEntwurf && belegEntwurf.praefix === "neuTank" ? belegEntwurf.daten : null;
      if (w.liter > 0 && w.kosten > 0) {
        serviceRufen("audi_dashboard_tankvorgang_manuell", {
          ts: w.ts, liter: w.liter, kosten: w.kosten, km: w.km, ersparnis: w.ersparnis, station: w.station,
          distanz: w.distanz, kraftstoff: w.kraftstoff,
          receipt_key: beleg ? beleg.receipt_key : null, receipt_file: beleg ? beleg.receipt_file : null,
        });
      }
      neuOffen = false; belegEntwurf = null; belegFehler = null; render(); return;
    }
    if (e.target.closest("[data-tankbearbeiten]")) {
      tankBearbeitenOffen = !tankBearbeitenOffen;
      if (belegEntwurf && belegEntwurf.praefix === "bearbTank") belegEntwurf = null;
      if (belegFehler && belegFehler.context === "bearbTank") belegFehler = null;
      render(); return;
    }
    const tankAkt = e.target.closest("[data-tankaktualisieren]");
    if (tankAkt) {
      const w = tankFormularWerte("bearbTank");
      const beleg = belegEntwurf && belegEntwurf.praefix === "bearbTank" ? belegEntwurf.daten : null;
      serviceRufen("audi_dashboard_tankvorgang_aktualisieren", {
        tank_id: tankAkt.dataset.tankaktualisieren, ts: w.ts, liter: w.liter, kosten: w.kosten,
        km: w.km, ersparnis: w.ersparnis, station: w.station,
        distanz: w.distanz, kraftstoff: w.kraftstoff,
        receipt_key: beleg ? beleg.receipt_key : null, receipt_file: beleg ? beleg.receipt_file : null,
      });
      tankBearbeitenOffen = false; belegEntwurf = null; belegFehler = null; render(); return;
    }
    const belegBtn = e.target.closest("[data-beleg]");
    if (belegBtn) {
      // "neu"/"bearb": nur parsen und den offenen Entwurf ausfüllen (siehe
      // belegEntwurf). Eine tank_id dagegen (Button in der Einzelbeleg-
      // Ansicht) trägt den Beleg direkt in den bereits gespeicherten
      // Tankvorgang nach - siehe Kopfkommentar in audi_dashboard_beleg_hochladen.
      const ziel = belegBtn.dataset.beleg;
      const nurParsen = ziel === "neu" || ziel === "bearb";
      const praefix = ziel === "neu" ? "neuTank" : ziel === "bearb" ? "bearbTank" : null;
      const tankId = nurParsen ? undefined : ziel;
      const kontext = nurParsen ? praefix : ziel;
      if (belegFehler && belegFehler.context === kontext) belegFehler = null;
      belegZielPraefix = praefix;
      const eingabe = document.createElement("input");
      eingabe.type = "file"; eingabe.accept = "application/pdf";
      eingabe.onchange = () => {
        const datei = eingabe.files[0]; if (!datei) return;
        const leser = new FileReader();
        leser.onload = () => {
          const base64 = leser.result.split(",")[1];
          serviceRufen("audi_dashboard_beleg_hochladen", { pdf_base64: base64, dateiname: datei.name, tank_id: tankId });
        };
        leser.readAsDataURL(datei);
      };
      eingabe.click();
      return;
    }
    const del = e.target.closest("[data-loeschen]");
    if (del) { delete CAR.service.vereinbart[del.dataset.loeschen]; profilSpeichern(); render(); return; }
    const oel = e.target.closest("[data-oel]");
    if (oel) { CAR.oel.modus = oel.dataset.oel; profilSpeichern(); render(); return; }
    const mode = e.target.closest("[data-mode]");
    if (mode) { themeSetzen(mode.dataset.mode); themeIcon(); render(); return; }
    const pr = e.target.closest("[data-profil]");
    if (pr && pr.dataset.profil === "export") {
      const a = document.createElement("a");
      a.href = "data:application/json;charset=utf-8," + encodeURIComponent(JSON.stringify(PROFIL_ROH, null, 2));
      a.download = "fahrzeugprofil.json"; document.body.appendChild(a); a.click(); a.remove();
      return;
    }
    const bk = e.target.closest("[data-backup]");
    if (bk && bk.dataset.backup === "jetzt") { serviceRufen("audi_dashboard_backup_jetzt", {}); return; }
    if (bk && bk.dataset.backup === "export") {
      const paket = { erstellt: new Date().toISOString(), profil: PROFIL_ROH, fahrten: TRIPS, tankvorgaenge: FILLS };
      const a = document.createElement("a");
      a.href = "data:application/json;charset=utf-8," + encodeURIComponent(JSON.stringify(paket, null, 2));
      a.download = `backup_${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); a.remove();
      return;
    }
    if (e.target.closest("[data-update-pruefen]")) {
      if (!updatePruefend) {
        updatePruefend = true; render();
        (async () => {
          try { await HASS.callService("pyscript", "audi_dashboard_update_pruefen", {}); }
          catch (err) { hinweis("Update-Prüfung fehlgeschlagen", err.message); }
          await updateStatusAbrufen();
          updatePruefend = false; render();
        })();
      }
      return;
    }
    if (e.target.closest("[data-update-installieren]")) {
      if (!updateInstallierend) bestaetigen(
        "Update installieren?",
        "Fahrzeugprofil, Fahrten, Tankvorgänge und eigene Fotos bleiben unangetastet. Die Seite lädt sich danach neu.",
        "Installieren",
        () => { updateInstallierend = true; render();
        (async () => {
          try {
            await HASS.callService("pyscript", "audi_dashboard_update_installieren", {});
            await updateStatusAbrufen();
            location.reload();
            return;
          } catch (err) { hinweis("Update-Installation fehlgeschlagen", err.message); }
          await updateStatusAbrufen();
          updateInstallierend = false; render();
        })();
      }, false);
      return;
    }
    const cs = e.target.closest("[data-csv]");
    if (cs) {
      const w = cs.dataset.csv;
      if (w === "fahrten") csvDatei("fahrten.csv", ["Start", "Ende", "km", "Art", "Status"],
        TRIPS.map((t) => [t.ts_start, t.ts_end, t.distance_km != null ? de(t.distance_km) : "", t.art, t.status]));
      else if (w === "tanken") csvDatei("tankvorgaenge.csv", ["Zeitpunkt", "Station", "Liter", "€/l", "Kosten €"],
        FILLS.map((f) => [f.ts, f.station_name || "", f.liters != null ? de(f.liters, 2) : "", f.liters ? de(f.fuel_total_eur / f.liters, 3) : "", f.fuel_total_eur != null ? eur(f.fuel_total_eur) : ""]));
      else csvDatei("servicebuch.csv", ["Datum", "km", "Art", "Werkstatt", "Kosten €"],
        CAR.service.buch.map((e2) => [e2.datum, de(e2.km), e2.art, e2.werkstatt, e2.kosten != null ? eur(e2.kosten) : ""]));
      return;
    }
    const satz = e.target.closest("[data-satz]");
    if (satz) { serviceRufen("audi_dashboard_reifen_wechseln", { satz: satz.dataset.satz.toLowerCase() }); CAR.reifen.aktiv = satz.dataset.satz; render(); return; }
    const k = e.target.closest("[data-ics]");
    if (k) {
      if (k.dataset.ics === "service") {
        const ts = termine(), t = ts[Math.min(svAuswahl, ts.length - 1)];
        const datum = $("#svDat").value;
        CAR.service.vereinbart[t.art] = datum;
        profilSpeichern();
        ics(`${CONFIG.fahrzeugtitel}: ${t.art}`, datum, CAR.service.autohaus.name,
          t.km !== null ? `Fällig bei ${de(t.km)} km · ${CAR.service.autohaus.adresse}` : CAR.service.autohaus.adresse);
        render();
      } else {
        const datum = $("#rfDat").value;
        CAR.reifen.wechsel.datum = datum;
        CAR.reifen.wechsel.vereinbart = $("#rfOk").checked;
        profilSpeichern();
        const ziel = CAR.reifen.aktiv === "Sommer" ? "Winter" : "Sommer";
        ics(`${CONFIG.fahrzeugtitel}: Reifenwechsel auf ${ziel}`, datum, CAR.service.autohaus.name, `Umstellung des Zählers auf den ${ziel}satz nicht vergessen`);
      }
      return;
    }
    const a = e.target.closest("[data-acc]");
    if (a) { const b = ROOT.getElementById(a.dataset.acc); a.setAttribute("aria-expanded", b.classList.toggle("on")); return; }
    const art = e.target.closest("[data-art]");
    if (art) { const tr = TRIPS.find((x) => x.trip_id === art.dataset.art); tr.art = tr.art === "arbeitsweg" ? "privat" : "arbeitsweg"; render(); return; }
    const g = e.target.closest("[data-go]");
    if (g) { const [n, i] = g.dataset.go.split(":"); go(n, i); }
  });

  ROOT.addEventListener("input", (e) => {
    const praefix = e.target.dataset.tankpreisfeld;
    if (!praefix) return;
    const liter = parseFloat((ROOT.getElementById(`${praefix}Liter`).value || "0").replace(",", "."));
    const kosten = parseFloat((ROOT.getElementById(`${praefix}Kosten`).value || "0").replace(",", "."));
    const anzeige = ROOT.getElementById(`${praefix}PreisAnzeige`);
    if (anzeige) anzeige.textContent = liter > 0 ? `Preis je Liter: ${de(kosten / liter, 3)} €` : "Preis je Liter: wird berechnet";
  });

  ROOT.addEventListener("change", (e) => {
    if (e.target.dataset.profil === "import") {
      const datei = e.target.files[0]; if (!datei) return;
      const leser = new FileReader();
      leser.onload = () => {
        try {
          const p = JSON.parse(leser.result);
          PROFIL_ROH = p; CONFIG = profilZuConfig(p); CAR = profilZuCar(p, { km: CAR.odo, tankprozent: CAR.tankPct, reichweite_km: CAR.reichweite, gesichert: CAR.gesichert });
          serviceRufen("audi_dashboard_profil_schreiben", { profil_json: JSON.stringify(p) });
          bild = 0; render();
        } catch (err) { hinweis("Die Datei ließ sich nicht lesen", err.message); }
      };
      leser.readAsText(datei);
      return;
    }
    if (e.target.dataset.backup === "import") {
      const datei = e.target.files[0]; if (!datei) return;
      const leser = new FileReader();
      leser.onload = () => {
        try {
          const paket = JSON.parse(leser.result);
          if (!paket.profil) throw new Error("Keine gültige Backup-Datei (Profil fehlt)");
          PROFIL_ROH = paket.profil;
          CONFIG = profilZuConfig(paket.profil);
          CAR = profilZuCar(paket.profil, { km: CAR.odo, tankprozent: CAR.tankPct, reichweite_km: CAR.reichweite, gesichert: CAR.gesichert });
          TRIPS = paket.fahrten || [];
          FILLS = paket.tankvorgaenge || [];
          serviceRufen("audi_dashboard_backup_wiederherstellen", {
            profil_json: JSON.stringify(paket.profil),
            fahrten_json: JSON.stringify(TRIPS),
            tankvorgaenge_json: JSON.stringify(FILLS),
          });
          bild = 0; render();
        } catch (err) { hinweis("Die Datei ließ sich nicht lesen", err.message); }
      };
      leser.readAsText(datei);
      return;
    }
    if (e.target.dataset.bildupload !== undefined) {
      const datei = e.target.files[0]; if (!datei) return;
      const dateiname = e.target.dataset.bildupload;
      const leser = new FileReader();
      leser.onload = async () => {
        const daten_base64 = String(leser.result).split(",")[1] || "";
        try {
          await HASS.callService("pyscript", "audi_dashboard_bild_hochladen", { dateiname, daten_base64 });
        } catch (err) { hinweis("Hochladen fehlgeschlagen", err.message); }
        bildMenuOffen = null; bildVersion = Date.now(); render();
      };
      leser.readAsDataURL(datei);
      e.target.value = "";
      return;
    }
    if (e.target.dataset.backupintervall !== undefined) {
      CONFIG.backupIntervall = e.target.value; profilSpeichern(); return;
    }
    const ein = e.target.dataset.einrichten;
    if (ein) {
      if (ein === "kennzeichen") CONFIG.kennzeichen = e.target.value;
      else if (ein === "ausfuehrung") CONFIG.ausfuehrung = e.target.value;
      else if (ein === "wlan") CONFIG.wlanName = e.target.value;
      else if (ein === "modell") CONFIG.fahrzeugtitel = e.target.value;
      else CAR[ein] = e.target.value;
      profilSpeichern(); return;
    }
    const vb = e.target.dataset.vereinbart;
    if (vb) { CAR.service.vereinbart[vb] = e.target.value; profilSpeichern(); render(); return; }
    const ah = e.target.dataset.ah;
    if (ah) {
      CAR.service.autohaus[ah] = e.target.value;
      profilSpeichern(); render(); return;
    }
    const sb = e.target.dataset.sb;
    if (sb) {
      const [i, feld] = sb.split("|");
      const eintrag = CAR.service.buch[+i];
      eintrag[feld] = feld === "km" ? (parseInt(e.target.value, 10) || 0) : feld === "kosten" ? (e.target.value === "" ? null : parseFloat(e.target.value)) : e.target.value;
      // Ein per "Eintrag hinzufügen" angelegter, noch nicht bestätigter Entwurf
      // wird erst beim Klick auf "Speichern" (data-sbspeichern) persistiert -
      // sonst würde schon das testweise Ausfüllen einzelner Felder auto-speichern.
      if (entwurfIndex !== +i) profilSpeichern();
      render(); return;
    }
    const rf = e.target.dataset.rf;
    if (rf) {
      const [k, schluessel] = rf.split("|");
      const wert = e.target.value;
      CAR.reifen[k][schluessel] = wert;
      profilSpeichern(); render(); return;
    }
    if (e.target.dataset.schrift !== undefined) {
      beschriftung = e.target.checked;
      try { localStorage.setItem(SCHRIFT_KEY, beschriftung ? "an" : "aus"); } catch (err) { /* Privatmodus */ }
      ROOT.getElementById("tabbar").classList.toggle("ohne", !beschriftung);
      return;
    }
    if (e.target.dataset.steuer !== undefined) { CAR.steuer.betrag = parseFloat(e.target.value) || 0; profilSpeichern(); render(); return; }
    if (e.target.dataset.steuerzeit !== undefined) { CAR.steuer.zeitraum = e.target.value; profilSpeichern(); render(); return; }
    if (e.target.dataset.steuerfaellig !== undefined) { CAR.steuer.faellig = e.target.value; profilSpeichern(); render(); return; }
    if (e.target.dataset.steuerlast !== undefined) { CAR.steuer.lastschrift = e.target.checked; profilSpeichern(); render(); return; }
    const vteil = e.target.dataset.vteil;
    if (vteil) {
      const [i, feld] = vteil.split("|");
      CAR.versicherung.teile[+i][feld] = feld === "betrag" ? (parseFloat(e.target.value) || 0) : e.target.value;
      CAR.versicherung.beitrag = sum(CAR.versicherung.teile, (t) => t.betrag || 0);
      profilSpeichern(); render(); return;
    }
    if (e.target.dataset.vgueltigab !== undefined) { CAR.versicherung.gueltigAb = e.target.value; profilSpeichern(); render(); return; }
    const vnotruf = e.target.dataset.vnotruf;
    if (vnotruf) { CAR.versicherung[vnotruf] = e.target.value; profilSpeichern(); render(); return; }
    if (e.target.dataset.pause !== undefined) { CONFIG.pauseMin = parseInt(e.target.value, 10) || 0; profilSpeichern(); return; }
    if (e.target.dataset.sd !== undefined) {
      if (e.target.checked) { smartdealPopupOffen = true; render(); }
      else { CAR.smartdeal.aktiv = false; profilSpeichern(); render(); }
      return;
    }
    if (e.target.dataset.oelkm !== undefined) { CAR.oel.km = parseInt(e.target.value, 10); profilSpeichern(); return; }
    if (e.target.dataset.oelm !== undefined) { CAR.oel.monate = parseInt(e.target.value, 10); profilSpeichern(); return; }
    if (e.target.dataset.boden !== undefined) { bodenAn = e.target.checked; render(); return; }
    if (e.target.dataset.startbild !== undefined) { CONFIG.startbild = e.target.value; profilSpeichern(); render(); return; }
    if (e.target.dataset.svsel !== undefined) { svAuswahl = parseInt(e.target.value, 10); render(); return; }
  });
}

/* ------------------------------------------------------------ Datenladen */
function zustandsDaten(entityId) {
  const st = HASS && HASS.states[entityId];
  return st && st.attributes ? st.attributes.daten : undefined;
}

let letzterProfilState = null, letzterFahrtenState = null, letzterTankState = null, letzterStatusState = null, letzterBelegState = null, letzterBattState = null;

/* Ergebnis eines Beleg-Uploads verarbeiten (siehe belegEntwurf/belegFehler
   weiter oben) - kommt über dieselbe Zustand-statt-Rückgabewert-Route wie
   Profil/Fahrten/Tankvorgänge, siehe Kopfkommentar in
   frontend_veroeffentlichung.py. */
function belegErgebnisVerarbeiten(daten) {
  if (!daten) return;
  if (daten.fehler) {
    const kontext = daten.tank_id || belegZielPraefix;
    belegFehler = { context: kontext, meldung: daten.fehler };
    if (!daten.tank_id) belegEntwurf = null;
  } else if (daten.tank_id) {
    // Direkt übernommener Beleg (Beleg nachtragen/ersetzen in der
    // Einzelbeleg-Ansicht) - die neuen Werte kommen bereits über
    // tankvorgaenge_veroeffentlichen(), hier nur eine evtl. noch offene
    // Fehleranzeige für diesen Tankvorgang löschen.
    if (belegFehler && belegFehler.context === daten.tank_id) belegFehler = null;
  } else if (belegZielPraefix) {
    belegEntwurf = { praefix: belegZielPraefix, daten };
    belegFehler = null;
  }
  render();
}

/* Nachlade-Fallback: panel_custom schiebt einem bereits gemounteten Element
   frische hass-Objekte nicht zuverlässig nach - an der Testinstanz auch
   Minuten nach dem Start und nach SPA-interner Navigation weg und zurück
   beobachtet, dass HASS auf dem allerersten, noch unvollständigen Stand
   hängen blieb (der übliche Fall: das Backend hat unsere vier Zustände beim
   allerersten hass-Push noch nicht veröffentlicht, z. B. kurz nach einem
   HA-Neustart). Deshalb hier aktiv per WebSocket nachfragen, statt passiv
   auf den nächsten hass-Push zu warten. Ergebnisse landen in einem eigenen
   Zwischenspeicher statt in HASS.states selbst - das gehört Home Assistant,
   nicht uns.

   Ein früherer Stand brach nach 15 Versuchen (rund 30s) endgültig ab - real
   zu kurz beobachtet: startet der Container komplett neu (HA + HACS + die
   VAG-Integration mit eigenen Netzwerkaufrufen), kann es deutlich länger
   dauern, bis pyscript überhaupt geladen ist und sein @time_trigger("startup")
   feuert. Sichtbares Symptom war genau das gemeldete Verhalten: die App
   blieb dauerhaft auf "Lädt …" hängen (und die Tableiste zeigte dadurch auch
   kein aktives Symbol, weil render() den harten Rücksprung bei
   !DATEN_GELADEN nie verlässt) - erst ein Panelwechsel weg und zurück half,
   weil das einen frischen connectedCallback()/datenLaden(false) auslöst, zu
   dem Zeitpunkt oft schon nach Ablauf der wirklichen Backend-Startzeit.
   Deshalb jetzt ohne festes Abbruchlimit: nach der anfänglichen schnellen
   Serie (siehe NACHLADE_INTERVALL_MS) läuft es mit einem größeren, aber
   endlosen Intervall weiter, bis Daten da sind oder das Element abgehängt
   wird (disconnectedCallback stoppt hier nichts explizit, aber ein
   getimeter Aufruf ohne HASS/callWS bricht oben ohnehin ab). */
const NACHGELADEN = {};
let nachladeTimer = null;
let nachladeVersuche = 0;
const NACHLADE_INTERVALL_MS = 2000;
const NACHLADE_LANGSAM_MS = 10000;
const NACHLADE_SCHNELLE_VERSUCHE = 15;

function nachladeAnstossen() {
  if (nachladeTimer || !HASS || !HASS.callWS) return;
  nachladeVersuche++;
  // Erster Versuch fast sofort statt erst nach vollem Intervall - der
  // Wettlauf mit pyscripts beim_start() (siehe Kopfkommentar) ist meist
  // schon nach wenigen hundert Millisekunden entschieden, nicht erst nach
  // den vollen zwei Sekunden. Nach den ersten Versuchen (Backend vermutlich
  // noch nicht bereit) auf ein ruhigeres Intervall wechseln, statt für immer
  // im schnellen Takt weiterzufragen.
  const verzoegerung = nachladeVersuche === 1 ? 200
    : nachladeVersuche <= NACHLADE_SCHNELLE_VERSUCHE ? NACHLADE_INTERVALL_MS
    : NACHLADE_LANGSAM_MS;
  nachladeTimer = setTimeout(async () => {
    nachladeTimer = null;
    try {
      const states = await HASS.callWS({ type: "get_states" });
      const finden = (id) => states.find((s) => s.entity_id === id);
      NACHGELADEN.profil = finden("pyscript.audi_dashboard_profil") || NACHGELADEN.profil;
      NACHGELADEN.fahrten = finden("pyscript.audi_dashboard_fahrten") || NACHGELADEN.fahrten;
      NACHGELADEN.tank = finden("pyscript.audi_dashboard_tankvorgaenge") || NACHGELADEN.tank;
      NACHGELADEN.status = finden("pyscript.audi_dashboard_fahrzeugstatus") || NACHGELADEN.status;
      NACHGELADEN.batt = finden("pyscript.audi_dashboard_batterieverlauf") || NACHGELADEN.batt;
    } catch (e) {
      // wird beim naechsten Versuch erneut probiert
    }
    datenLaden(true);
  }, verzoegerung);
}

async function datenLaden(erzwingen) {
  if (!HASS) return;
  const profilState = HASS.states["pyscript.audi_dashboard_profil"] || NACHGELADEN.profil;
  const fahrtenState = HASS.states["pyscript.audi_dashboard_fahrten"] || NACHGELADEN.fahrten;
  const tankState = HASS.states["pyscript.audi_dashboard_tankvorgaenge"] || NACHGELADEN.tank;
  const statusState = HASS.states["pyscript.audi_dashboard_fahrzeugstatus"] || NACHGELADEN.status;
  const battState = HASS.states["pyscript.audi_dashboard_batterieverlauf"] || NACHGELADEN.batt;
  const belegState = HASS.states["pyscript.audi_dashboard_beleg_ergebnis"];

  if (belegState && belegState !== letzterBelegState) {
    letzterBelegState = belegState;
    belegErgebnisVerarbeiten(belegState.attributes && belegState.attributes.daten);
  }

  const geaendert = erzwingen
    || profilState !== letzterProfilState || fahrtenState !== letzterFahrtenState
    || tankState !== letzterTankState || statusState !== letzterStatusState || battState !== letzterBattState;
  if (geaendert) {
    letzterProfilState = profilState; letzterFahrtenState = fahrtenState;
    letzterTankState = tankState; letzterStatusState = statusState; letzterBattState = battState;

    const profil = profilState && profilState.attributes ? profilState.attributes.daten : null;
    if (profil) {
      const status = (statusState && statusState.attributes && statusState.attributes.daten) || {};
      LETZTES_UPDATE_TS = (status.km != null && statusState && statusState.last_updated)
        ? new Date(statusState.last_updated).getTime() : null;
      PROFIL_ROH = profil;
      CONFIG = profilZuConfig(profil);
      CAR = profilZuCar(profil, status);
      TRIPS = (fahrtenState && fahrtenState.attributes && fahrtenState.attributes.daten) || [];
      FILLS = (tankState && tankState.attributes && tankState.attributes.daten) || [];
      BATTERIEVERLAUF = (battState && battState.attributes && battState.attributes.daten) || [];
      DATEN_GELADEN = true;
      nachladeVersuche = 0;
      if (!UPDATE_STATUS_GEHOLT) { UPDATE_STATUS_GEHOLT = true; updateStatusAbrufen().then(render); }
      render();
      return;
    }
  }

  if (!DATEN_GELADEN) nachladeAnstossen(); // Backend noch nicht bereit (erster Tick nach Neustart)
}

/* -------------------------------------------------------------- Element */
class AudiDashboardPanel extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });

    /* ha-panel-custom erzeugt das Element teils schon, bevor unser eigenes
       Modul (dieses hier) mit customElements.define() fertig registriert
       ist - beobachtet als Zusammenspiel mit dessen fehlendem Cache für
       module_url-Panels (siehe Kopfkommentar in audi-dashboard-panel.js):
       _createPanel() kann mehrfach anlaufen, und beim ersten, noch zu
       frühen document.createElement(tag) ist "audi-dashboard-panel" für
       den Browser noch ein unbekanntes Element. Die anschließende
       Zuweisung el.hass = ... trifft dann noch keinen Setter auf dem
       Prototyp und legt stattdessen eine gewöhnliche eigene Eigenschaft
       auf der Instanz an. Sobald customElements.define() greift und der
       Browser das Element "aufwertet" (führt genau diesen Konstruktor
       nach), holt die Plattform das NICHT automatisch nach - das gilt nur
       für über observedAttributes beobachtete HTML-Attribute, nicht für
       beliebige JS-Eigenschaften. Ohne dieses Nachholen bleibt set hass()
       dauerhaft unerreichbar: _aufbauen() lief zwar (über
       connectedCallback(), einen echten Lifecycle-Hook), aber
       datenLaden() nie - sichtbares Symptom war ein dauerhaft
       hängendes "Lädt …" trotz vorhandener Backend-Daten. */
    for (const eigenschaft of ["hass", "narrow", "panel"]) {
      if (Object.prototype.hasOwnProperty.call(this, eigenschaft)) {
        const wert = this[eigenschaft];
        delete this[eigenschaft];
        this[eigenschaft] = wert;
      }
    }
  }

  set hass(hass) {
    const erster = !HASS;
    HASS = hass;
    if (erster) {
      this._aufbauen();
    }
    datenLaden(false);
  }
  get hass() { return HASS; }

  set narrow(v) { this._narrow = v; }
  set panel(v) { this._panel = v; }

  connectedCallback() {
    HOST = this;
    if (!this.shadowRoot.querySelector(".phone")) this._aufbauen();
    // Absicherung fuer den Fall, dass panel_custom das Element neu verbindet
    // (z. B. weg- und zurueckgeklickt), ohne set hass() dabei erneut
    // auszuloesen - ohne das hier bliebe ein noch nicht geladener Stand
    // haengen, bis irgendwann zufaellig ein globaler hass-Push durchkommt.
    if (!DATEN_GELADEN) datenLaden(false);
  }

  disconnectedCallback() {
    if (MAP) { MAP.remove(); MAP = null; }
  }

  _aufbauen() {
    // Idempotenz-Sperre: sowohl connectedCallback() als auch set hass()
    // können _aufbauen() auslösen, je nachdem in welcher Reihenfolge
    // panel_custom das Element einfügt und mit hass versorgt. Ohne diese
    // Sperre lief der Aufbau zweimal, verdrahtete jeden Knopf doppelt und
    // ein Klick auf Tag/Nacht schaltete zweimal um - macht sich gegenseitig
    // zunichte, sichtbar erst beim tatsächlichen Testen, nicht im Code.
    if (this.shadowRoot.querySelector(".phone")) return;
    ROOT = this.shadowRoot;
    HOST = this;
    /* 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();

    const link = document.createElement("link");
    link.rel = "stylesheet";
    link.href = `/local/audi-dashboard.css?v=${CSS_VERSION}`;
    ROOT.appendChild(link);

    const wrapper = document.createElement("div");
    wrapper.innerHTML = `
    <div class="phone">
      <div class="topbar">
        <button class="back" id="back" aria-label="Zurück"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="m10.21 12 5.12-5.08-.7-.71L8.79 12l5.84 5.8.7-.72L10.22 12Z"/></svg></button>        <div class="rings" id="marke" aria-label="Audi"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 289 100" fill="currentColor" aria-hidden="true"><defs><clipPath id="audiRingsClip" clipPathUnits="userSpaceOnUse"><path d="M0 100h289V0H0z"/></clipPath></defs><g clip-path="url(#audiRingsClip)" transform="matrix(1 0 0 -1 0 100.00040017)"><path d="M239 11.1914c-8.885 0-17.075 3.008-23.624 8.05 6.646 8.489 10.624 19.165 10.624 30.759 0 11.593-3.978 22.269-10.624 30.759 6.549 5.041 14.739 8.05 23.624 8.05 21.399 0 38.809-17.41 38.809-38.809 0-21.4-17.41-38.809-38.809-38.809m-86.625 8.05c6.647 8.489 10.625 19.165 10.625 30.759 0 11.593-3.978 22.269-10.624 30.759 6.548 5.041 14.74 8.05 23.624 8.05 8.885 0 17.075-3.009 23.624-8.05-6.646-8.49-10.624-19.166-10.624-30.759 0-11.594 3.978-22.27 10.624-30.759-6.549-5.042-14.739-8.05-23.624-8.05-8.884 0-17.076 3.008-23.625 8.05m-63 0C96.022 27.7304 100 38.4064 100 50.0004c0 11.593-3.978 22.269-10.624 30.759 6.548 5.041 14.74 8.05 23.624 8.05 8.884 0 17.076-3.009 23.624-8.05-6.646-8.49-10.624-19.166-10.624-30.759 0-11.594 3.978-22.27 10.624-30.759-6.548-5.042-14.74-8.05-23.624-8.05-8.884 0-17.076 3.008-23.625 8.05m-78.184 30.759c0 21.399 17.41 38.809 38.809 38.809 8.884 0 17.076-3.009 23.624-8.05C66.978 72.2694 63 61.5934 63 50.0004c0-11.594 3.978-22.27 10.624-30.759-6.548-5.042-14.74-8.05-23.624-8.05-21.399 0-38.809 17.409-38.809 38.809M81.5 27.3734c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m63 0c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m63 0c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m31.5 72.627c-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.568 11.207-31.5 11.207-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.569 11.207-31.5 11.207-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.569 11.207-31.5 11.207-27.57 0-50-22.43-50-50 0-27.571 22.43-50 50-50 11.931 0 22.896 4.206 31.5 11.207C90.104 4.2064 101.068.0004 113 .0004c11.931 0 22.896 4.206 31.5 11.207 8.604-7.001 19.568-11.207 31.5-11.207 11.932 0 22.896 4.206 31.5 11.207 8.604-7.001 19.568-11.207 31.5-11.207 27.57 0 50 22.429 50 50 0 27.57-22.43 50-50 50"/></g></svg></div>
        <div class="head"><div class="eyebrow" id="eyebrow"></div><div class="title" id="title">Übersicht</div>
          <div class="sync" id="sync"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="m18.65 4.95 2.56 2.55-2.56 2.55-.7-.7L19.29 8H7.2A5.24 5.24 0 0 0 2 13.38V14H1v-.6A6.24 6.24 0 0 1 7.2 7h12.1l-1.34-1.35.7-.7ZM23 11.59A6.24 6.24 0 0 1 16.8 18H4.7l1.35 1.35-.7.7-2.56-2.55 2.56-2.55.7.7L4.71 17H16.8a5.24 5.24 0 0 0 5.2-5.38V11h1v.6Z"/></svg><span></span></div></div>
        <button class="themebtn" id="theme" hidden aria-hidden="true"><svg id="themeicon" viewBox="0 0 20 20" width="15" height="15"></svg></button>
        <button class="profilbtn" id="ringsbtn" aria-label="Einstellungen"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.6 1.6 0 0 0 .33 1.76l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.6 1.6 0 0 0-1.76-.33 1.6 1.6 0 0 0-1 1.47V21a2 2 0 1 1-4 0v-.1a1.6 1.6 0 0 0-1-1.47 1.6 1.6 0 0 0-1.77.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.6 1.6 0 0 0 4.6 15a1.6 1.6 0 0 0-1.47-1H3a2 2 0 1 1 0-4h.1a1.6 1.6 0 0 0 1.47-1 1.6 1.6 0 0 0-.33-1.77l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.6 1.6 0 0 0 9 4.6 1.6 1.6 0 0 0 10 3.13V3a2 2 0 1 1 4 0v.1a1.6 1.6 0 0 0 1 1.47 1.6 1.6 0 0 0 1.77-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.6 1.6 0 0 0 19.4 9v.04a1.6 1.6 0 0 0 1.47 1H21a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1Z"/></svg></button>
      </div>
      <div class="ptr" id="ptr"><svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M20.06 8.36c-1.21-3.33-4.3-6.46-8.5-6.46-3.69 0-6.97 2-8.55 5.28a9.47 9.47 0 0 0 4.23 12.77A9.56 9.56 0 0 0 20 15.72l-.89-.44a8.56 8.56 0 0 1-11.43 3.77A8.47 8.47 0 0 1 3.9 7.62a8.38 8.38 0 0 1 7.65-4.72c3.8 0 6.64 2.94 7.66 6.1h-5.66v1h7.5V2.5h-1v5.86Z"/></svg><span>Ziehen zum Aktualisieren</span></div>
      <main id="view"><div class="tile" style="margin-top:20px"><span class="label">Lädt …</span></div></main>
      <div id="overlay"></div>
      <nav class="tabbar" id="tabbar"></nav>
    </div>`;
    ROOT.appendChild(wrapper);

    ereignisseVerdrahten();
    randwischenVerdrahten();
    themeIcon();
    setInterval(standAlterTicken, 1000);
  }
}

if (!customElements.get("audi-dashboard-panel")) {
  customElements.define("audi-dashboard-panel", AudiDashboardPanel);
}

