Fix broken map rendering, split GPS cleanup, receipt parser and design backlog

Leaflet's stylesheet was appended to document.head, so it never reached the
panel's shadow root: .leaflet-tile{position:absolute} never applied, tiles laid
out as in-flow images (~1040px inside a 190px box) and the marker pane ended up
far below the visible area. That is the real cause of the long-standing
"fragmented Leaflet rendering" finding and of the invisible vehicle pin - every
tile request had actually succeeded. The stylesheet now goes into the shadow
root and is awaited before the map is built.

Also in this round:
- Map tiles are always light (Google-Maps-style), no dark variant at night.
- Vehicle marker uses the real CI poi-car icons (poi-car-l >=34px, poi-car-s
  below), with a white halo so the outline stays readable on tiles.
- Removed the obsolete combined STANDORT_TRACKER field; only the split
  lat/lon sensors remain.
- Receipt upload: widened the try block so base64/save failures surface, and
  the frontend call site now reports a rejected service call.
- Generic receipt parser: total detection is line-based (letter-spaced
  headings, no more matching the SUMME-EUR column header, tax lines excluded),
  address heuristic handles 4-digit postcodes and single-line address blocks,
  and "Preis/L" matches without a spelled-out "Liter".
- Design backlog: red hairline frame on list rows (delete button bled through
  at fractional row heights) and select fields now use the grey background box.

Shell 10-receipt regression suite still passes; all changes verified live in
the audi_ha_test container.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 12:46:48 +02:00
parent 25d1cebd12
commit 39da68cd87
11 changed files with 328 additions and 133 deletions
+83 -33
View File
@@ -386,31 +386,57 @@ function ring(pct) {
/* ------------------------------------------------------------------ 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",
};
/* Bewusst themeunabhaengig hell: Kartendienste (Google Maps & Co.) zeigen die
Standard-Strassenkarte immer im hellen Stil, weil Strassen, Beschriftungen
und Wegfarben darauf lesbar bleiben. Die frueher bei Nacht eingeblendeten
dark_all-Kacheln liessen die Karte im Panel schwarz wirken. */
const TILE_URL = "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png";
const ATTR = "&copy; OpenStreetMap &middot; CARTO";
/* Satellitenansicht der Standort-Vollbildkarte (Layer-Knopf) - wie die
CARTO-Straßenkacheln oben ohne eigenen Schlüssel nutzbar. */
const SAT_TILE_URL = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}";
const SAT_ATTR = "Tiles &copy; Esri";
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);
const LEAFLET_CSS_URL = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css";
const LEAFLET_JS_URL = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js";
/* Das Leaflet-Stylesheet muss in den Shadow-Root, nicht in document.head:
Dokument-Styles greifen nicht in ein Shadow-DOM hinein. Lag es im Head,
fehlte drinnen u. a. .leaflet-tile{position:absolute} - die Kacheln flossen
dann als gewoehnliche Bilder untereinander (bei 8 Kacheln ueber 1000 px
hoch), von der Karte war nur ein schmaler Streifen zu sehen und der
Fahrzeugmarker landete weit unterhalb des sichtbaren Ausschnitts. Das JS
gehoert dagegen weiterhin in den Head, damit window.L global bleibt. */
function leafletCssEinhaengen() {
if (!ROOT || ROOT.querySelector("link[data-leaflet-css]")) return Promise.resolve();
return new Promise((fertig) => {
const css = document.createElement("link");
css.rel = "stylesheet";
css.href = LEAFLET_CSS_URL;
css.dataset.leafletCss = "1";
/* Auch bei Ladefehler weitermachen statt haengen zu bleiben - dann sieht
die Karte zwar wieder falsch aus, aber die Ansicht baut sich auf. */
css.onload = fertig;
css.onerror = fertig;
ROOT.appendChild(css);
});
return LEAFLET_LADEND;
}
function leafletLaden() {
/* Bewusst bei jedem Aufruf: haengt HA das Panel neu ein, entsteht ein neuer
Shadow-Root ohne das Stylesheet, waehrend window.L laengst gesetzt ist. */
const css = leafletCssEinhaengen();
if (window.L) return css;
if (!LEAFLET_LADEND) {
LEAFLET_LADEND = new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = LEAFLET_JS_URL;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
return Promise.all([css, LEAFLET_LADEND]);
}
function mapBox() { return `<div class="mapbox"><div id="map"></div></div>`; }
@@ -437,7 +463,7 @@ function fakeTrack(a, b, n = 13) {
}
function tiles() {
if (!MAP || !window.L) return;
const url = TILES[HOST.dataset.theme];
const url = TILE_URL;
if (TILE) MAP.removeLayer(TILE);
TILE = window.L.tileLayer(url, { maxZoom: 19, attribution: ATTR }).addTo(MAP);
}
@@ -498,10 +524,15 @@ const STANDORT_PEEK_PX = 96; // sichtbarer Anteil im geschlossenen Zustand, muss
function standortBekannt() { return CAR.standortLat != null && CAR.standortLon != null; }
/* Kartenmarkierung: poi-car aus dem Audi-CI-Satz (assets/icons/poi-car.svg).
Die Spitze sitzt unten, deshalb ankert Leaflet am unteren Rand. */
/* Kartenmarkierung: poi-car aus dem CI-Satz, in den beiden gelieferten
Groessenvarianten - poi-car-l (48er-Raster, mehr Detail) ab 34 px, darunter
poi-car-s (24er-Raster). Die Nadelspitze sitzt unten, deshalb ankert Leaflet
am unteren Rand (siehe iconAnchor an den Aufrufstellen: Spitze bei y=45.25
im 48er- bzw. y=23.3 im 24er-viewBox). */
function fahrzeugMarkerSVG(px = 34) {
return `<svg viewBox="0 0 24 24" width="${px}" height="${px}" fill="none" aria-hidden="true">${CI.poiCar}</svg>`;
const gross = px >= 34;
const box = gross ? 48 : 24;
return `<svg viewBox="0 0 ${box} ${box}" width="${px}" height="${px}" fill="none" aria-hidden="true">${gross ? CI.poiCarL : CI.poiCar}</svg>`;
}
/* Fahrzustand fürs Menü: "fährt" solange die aktuelle Fahrt noch offen ist
@@ -576,7 +607,7 @@ function standortErfassenFallsNoetig() {
}
/* Reverse-Geocoding über die öffentliche Nominatim-API (OpenStreetMap) -
wie die Kartenkacheln oben (TILES) ohne eigenen Schlüssel; bei diesem
wie die Kartenkacheln oben (TILE_URL) ohne eigenen Schlüssel; bei diesem
Aufrufvolumen (ein Fahrzeug, nur bei geöffneter Standortansicht) im Rahmen
der Nominatim-Nutzungsbedingungen unproblematisch, genau wie das bereits
dokumentierte Leaflet-über-CDN-Vorgehen (siehe AGENTS.md). Cache-Schlüssel
@@ -639,14 +670,14 @@ function standortTiles() {
if (SMAP_TILE) SMAP.removeLayer(SMAP_TILE);
SMAP_TILE = SMAP_STIL === "satellit"
? window.L.tileLayer(SAT_TILE_URL, { maxZoom: 19, attribution: SAT_ATTR }).addTo(SMAP)
: window.L.tileLayer(TILES[HOST.dataset.theme], { maxZoom: 19, attribution: ATTR }).addTo(SMAP);
: window.L.tileLayer(TILE_URL, { maxZoom: 19, attribution: ATTR }).addTo(SMAP);
}
function standortMarkerZeichnen() {
if (!SMAP || !window.L) return;
if (FAHRZEUG_MARKER) { SMAP.removeLayer(FAHRZEUG_MARKER); FAHRZEUG_MARKER = null; }
if (USER_MARKER) { SMAP.removeLayer(USER_MARKER); USER_MARKER = null; }
if (standortBekannt()) {
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${fahrzeugMarkerSVG(38)}</div>`, iconSize: [38, 38], iconAnchor: [18, 36] });
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${fahrzeugMarkerSVG(38)}</div>`, iconSize: [38, 38], iconAnchor: [19, 36] });
FAHRZEUG_MARKER = window.L.marker([CAR.standortLat, CAR.standortLon], { icon }).addTo(SMAP);
FAHRZEUG_MARKER.on("click", () => standortMenuOeffnen());
}
@@ -693,10 +724,10 @@ async function initVorschauMap() {
zoomControl: false, attributionControl: false, dragging: false,
scrollWheelZoom: false, doubleClickZoom: false, boxZoom: false, keyboard: false, touchZoom: false,
});
TMAP_TILE = window.L.tileLayer(TILES[HOST.dataset.theme], { maxZoom: 19, attribution: ATTR }).addTo(TMAP);
TMAP_TILE = window.L.tileLayer(TILE_URL, { maxZoom: 19, attribution: ATTR }).addTo(TMAP);
if (standortBekannt()) {
TMAP.setView([CAR.standortLat, CAR.standortLon], 14);
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${fahrzeugMarkerSVG(32)}</div>`, iconSize: [32, 32], iconAnchor: [15, 30] });
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${fahrzeugMarkerSVG(32)}</div>`, iconSize: [32, 32], iconAnchor: [15, 31] });
window.L.marker([CAR.standortLat, CAR.standortLon], { icon }).addTo(TMAP);
} else {
TMAP.setView(kartenMittelpunkt(), 11);
@@ -781,6 +812,9 @@ const CI = {
carSearch: '<path fill="currentColor" d="M1 7.8a6.16 6.16 0 0 0 6.1 6.1h.4A5.7 5.7 0 0 0 13 7.91V7.9a6.1 6.1 0 0 0-5.91-6A5.93 5.93 0 0 0 1 7.8m-1 0C0 3.92 3.23.8 7.11.9a7.1 7.1 0 0 1 6.89 7c.06 2.33-1 4.44-2.73 5.71L14.8 19H20v1.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5v-7.8l-1.9-1.9-.04-.06a55 55 0 0 0-2.84-4.71c-1.42-.1-2.49-.17-3.2-.2l.04-1c.77.04 1.93.1 3.48.22l.24.02.14.2a55 55 0 0 1 2.99 4.93L24 12.3v8.2c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5V20h-3.55l1.67 2.54-.84.54L14.26 20 5 19.96v.54c0 .83-.67 1.5-1.5 1.5h-2A1.5 1.5 0 0 1 0 20.5v-7.08h1v7.08c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5v-1.54l9.6.02-3.17-4.83c-.87.45-1.85.72-2.92.75H7.1c-3.88 0-7-3.13-7.1-7.09zm5.47-2.75c1.86-.12 3.37-.2 4.52-.24l.03 1c-1.08.03-2.5.1-4.24.22L4.51 8c-.27.43-.6 1-1 1.7l-.87-.48c.4-.72.74-1.3 1.03-1.76q.6-.96 1.41-2.19l.14-.2z"/>',
gps: '<path fill="currentColor" d="M12.5 3.51V6.5h-1V3.51a8.46 8.46 0 0 0-7.99 7.99H6.5v1H3.51a8.46 8.46 0 0 0 7.99 7.99V17.5h1v2.99a8.46 8.46 0 0 0 7.99-7.99H17.5v-1h2.99a8.46 8.46 0 0 0-7.99-7.99m0-1a9.46 9.46 0 0 1 8.99 8.99h2.01v1h-2.01a9.46 9.46 0 0 1-8.99 8.99v2.01h-1v-2.01a9.46 9.46 0 0 1-8.99-8.99H.5v-1h2.01a9.46 9.46 0 0 1 8.99-8.99V.5h1z"/>',
poiCar: '<path fill="currentColor" d="M3.8 8.6C3.8 4.33 7.32.8 11.5.8s7.7 3.53 7.7 7.8c0 1.11-.5 2.58-1.17 4.1a48 48 0 0 1-2.5 4.7 90 90 0 0 1-3.54 5.47l-.06.09-.02.02V23l-.41-.29-.4.29h-.01l-.02-.03-.06-.09a70 70 0 0 1-1.05-1.53 90 90 0 0 1-2.49-3.93 48 48 0 0 1-2.5-4.72C4.29 11.18 3.8 9.71 3.8 8.6m7.7 14.1-.4.29.4.57.4-.57zm0-.87a88 88 0 0 0 3.17-4.94 47 47 0 0 0 2.45-4.6A10.3 10.3 0 0 0 18.2 8.6c0-3.73-3.08-6.8-6.7-6.8a6.82 6.82 0 0 0-6.7 6.8c0 .89.4 2.18 1.08 3.69.67 1.48 1.55 3.1 2.45 4.6a89 89 0 0 0 3.17 4.94M8.77 4h5.47L17 7.35V11a1 1 0 0 1-1 1h-1.5a1 1 0 0 1-1-1h-4a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7.32zm.46 1L7 7.68V8h2v1H7v2h1.5v-1h6v1H16V9h-2V8h2v-.29L13.76 5z"/>',
/* poi-car-l - grosse Variante desselben CI-Symbols (48er-Raster), fuer die
Fahrzeugnadel auf der Vollbildkarte. Siehe fahrzeugMarkerSVG(). */
poiCarL: '<path fill="currentColor" d="M9 17.5C9 9.52 15.52 3 23.5 3S38 9.52 38 17.5c0 2.04-.92 4.78-2.23 7.67a90 90 0 0 1-4.84 8.98 170 170 0 0 1-6.98 10.59l-.03.04-.01.01-.41-.29-.4.3-.02-.02-.03-.04a66 66 0 0 1-.56-.8 166 166 0 0 1-6.42-9.8 90 90 0 0 1-4.84-8.97C9.92 22.27 9 19.54 9 17.5m14.5 27-.4.3.4.55.4-.56zm0-.86a132 132 0 0 0 1.77-2.56 170 170 0 0 0 4.8-7.45 89 89 0 0 0 4.79-8.87c1.3-2.89 2.14-5.45 2.14-7.26C37 10.08 30.92 4 23.5 4S10 10.08 10 17.5c0 1.8.83 4.37 2.14 7.26a89 89 0 0 0 4.79 8.87 170 170 0 0 0 6.57 10.01M17.96 7.98A63.3 63.3 0 0 1 29.05 8l.2.02.13.16c.6.7 1.1 1.36 1.46 1.94l1.31 2.08L34 14.31v7.19c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5V21H18v.5c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5v-7.19l1.85-2.11 1.3-2.08c.37-.58.86-1.24 1.47-1.96l.13-.16.2-.02ZM15.48 13 14 14.69V15h3v1h-3v5.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5V20h13v1.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5V16h-3v-1h3v-.31L31.52 13zm15.36-1L30 10.65a16 16 0 0 0-1.25-1.67 62.4 62.4 0 0 0-10.5-.02 17 17 0 0 0-1.25 1.7L16.16 12zM19.3 15h8.42l1.4 1.4-.16.31c-.34.74-.65 1.32-.92 1.72-.14.2-.28.38-.42.5a.92.92 0 0 1-.61.27h-7a.92.92 0 0 1-.6-.27 2.4 2.4 0 0 1-.43-.5c-.27-.4-.58-.98-.92-1.72l-.15-.32zm.42 1-.6.6c.26.57.5.99.69 1.27.11.17.2.27.26.32l.01.01h6.86v-.01c.07-.05.16-.15.27-.32.2-.28.43-.7.7-1.27l-.6-.6h-7.6Z"/>',
selectAll: '<path fill="currentColor" d="M2.8 2H5v1H3v2H2V2.8a.76.76 0 0 1 .43-.71.9.9 0 0 1 .35-.09zv.5zM9 3H7V2h2zm4 0h-2V2h2zm4 0h-2V2h2zm4 0h-2V2h2.2v.5V2h.02a1 1 0 0 1 .1.01c.07.01.16.03.25.08a.76.76 0 0 1 .43.71V5h-1zM6 6h12v12H6zm1 1v10h10V7zM3 7v2H2V7zm19 0v2h-1V7zM3 11v2H2v-2zm19 0v2h-1v-2zM3 15v2H2v-2zm19 0v2h-1v-2zM3 21v-2H2v2.2a.76.76 0 0 0 .43.71.9.9 0 0 0 .35.09h.01l.01-.5v.5H5v-1zm19-2v2.2a.76.76 0 0 1-.43.71.9.9 0 0 1-.35.09h-.01l-.01-.49V22H19v-1h2v-2zM9 22H7v-1h2zm4 0h-2v-1h2zm4 0h-2v-1h2z"/>',
planRoute: '<path fill="currentColor" d="m18.3 7-3.65-3.65.7-.7 4.86 4.85-4.86 4.85-.7-.7L18.29 8H9.5C8.67 8 8 8.67 8 9.5V21H7V9.5A2.5 2.5 0 0 1 9.5 7z"/>',
upload: '<path fill="currentColor" d="M13 20h-1V9.7l-3.65 3.65-.7-.7 4.85-4.86 4.85 4.86-.7.7L13 9.71zM6 5V4h13v1z"/>',
@@ -3103,11 +3137,9 @@ function ereignisseVerdrahten() {
ROOT.getElementById("theme").addEventListener("click", () => {
themeSetzen(HOST.dataset.theme === "nacht" ? "tag" : "nacht");
themeIcon();
/* Neu zeichnen, damit auch die Leaflet-Kacheln den Stil wechseln: die
Kachel-URL wird beim Aufbau aus TILES[theme] gewaehlt und nicht
nachtraeglich aktualisiert. Ohne dies blieb die Karte bis zum naechsten
Backend-Update im alten Stil - seit der Dauerkarte auf der Uebersicht
deutlich sichtbar. */
/* Neu zeichnen, damit theme-abhaengige, in JS gesetzte Werte (z. B. die
aus --red gelesene Routenfarbe in initMap) mitziehen. Die Kacheln selbst
sind seit TILE_URL themeunabhaengig hell und brauchen das nicht mehr. */
render();
});
// Wischgeste über Pointer Events (nicht Touch Events), damit dieselbe
@@ -3153,6 +3185,9 @@ function ereignisseVerdrahten() {
}
e.preventDefault();
swipeWarGezogen = true;
// Loeschknopf erst waehrend der Geste einblenden (siehe .swipe-delete in
// audi-dashboard.css) - sonst blitzt er dauerhaft an den Zeilenraendern durch.
swipeAktivWrap.classList.add("wischt");
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");
@@ -3163,6 +3198,7 @@ function ereignisseVerdrahten() {
if (!swipeAktivWrap) return;
const inhalt = swipeAktivWrap.querySelector(".swipe-content");
inhalt.style.transition = ""; inhalt.style.transform = "";
swipeAktivWrap.classList.remove("wischt");
if (swipeGeste === "x") {
if (swipeDX <= -SWIPE_BREITE / 2) { schliesseAlleSwipes(); swipeAktivWrap.classList.add("swiped"); }
else { swipeAktivWrap.classList.remove("swiped"); }
@@ -3510,7 +3546,21 @@ function ereignisseVerdrahten() {
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 });
// Kein serviceRufen() hier: dessen catch loggt nur in die Browser-
// Konsole (bei allen anderen Aufrufen ok, die reagieren auf
// datenLaden() statt auf den Rückgabewert) - ein rejizierter
// Service-Aufruf (Backend nicht erreichbar, Timeout, ...) blieb
// für den Beleg-Upload sonst komplett unsichtbar in der
// Oberfläche, obwohl der Backend-seitige Parser-Fehlerpfad selbst
// schon einen Fehler veröffentlicht (siehe belegverarbeitung.py).
HASS.callService("pyscript", "audi_dashboard_beleg_hochladen",
{ pdf_base64: base64, dateiname: datei.name, tank_id: tankId }
).catch((err) => {
console.error("audi_dashboard: audi_dashboard_beleg_hochladen", err);
belegFehler = { context: kontext, meldung: "Beleg konnte nicht hochgeladen werden" };
if (!tankId) belegEntwurf = null;
render();
});
};
leser.readAsDataURL(datei);
};
+5 -4
View File
@@ -140,10 +140,11 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
padding:8px 4px;color:var(--fg2)}
/* Design-Audit 2026-08-13: ein <select> (z.B. Oelwechsel-Intervall "10.000 km"/
"1 Jahr") sah wie reiner, nicht editierbarer Text aus, seit hier Rahmen/
Flaeche entfernt wurden - anders als bei freien Texteingaben zeigt iOS bei
Picker-Feldern wie diesem ueblicherweise die Akzentfarbe statt Fliesstext-
Grau als Hinweis "hier gibt es eine Auswahl". */
.feld select{color:var(--ios-tint)}
Flaeche entfernt wurden. Der Hinweis "hier gibt es eine Auswahl" laeuft
deshalb ueber dieselbe graue Flaeche wie bei .mitEinheit input (Feld
"Pause bis [15] Minuten") - ein Kasten liest sich als bedienbar, waehrend
eingefaerbter Text auch bloss Hervorhebung sein koennte. */
.feld select{background:var(--ios-fill);padding:8px 10px}
.feld input:focus,.feld select:focus{background:var(--ios-fill);color:var(--fg)}
.mitEinheit input{background:var(--ios-fill);padding:8px 10px}
.notiz{background:var(--ios-fill);border:0;border-radius:12px;font-size:16px}
@@ -1 +1 @@
{"version": 1786752000}
{"version": 1786765000}
+17 -5
View File
@@ -667,7 +667,15 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
/* -------------------------------------------------------------- Wischen */
.swipe-wrap { position: relative; overflow: hidden; }
.swipe-delete { position: absolute; inset: 0 0 0 auto; width: 84px; display: flex; }
/* Der rote Loeschknopf liegt dauerhaft hinter der Zeile. Zeilenhoehen sind
gebrochen (z. B. 66,28 px), dadurch fiel die Kante von .swipe-content nicht
aufs Geraetepixelraster und der rote Knopf blitzte als duenne Linie an den
Zeilenraendern durch - sichtbar als roter Teilrahmen um jede Listenzeile.
Er wird deshalb erst sichtbar, sobald wirklich gewischt wird ("wischt"
waehrend der Geste, "swiped" fuer die offen stehende Zeile). */
.swipe-delete { position: absolute; inset: 0 0 0 auto; width: 84px; display: flex; visibility: hidden; }
.swipe-wrap.wischt .swipe-delete,
.swipe-wrap.swiped .swipe-delete { visibility: visible; }
.swipe-loeschen {
width: 100%;
background: var(--bad); color: #fff; border: none;
@@ -702,12 +710,16 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
}
.fahrzeug-pin-wrap { background: none; border: none; }
/* poi-car aus dem CI-Satz - rotes Symbol, weiss abgesetzt statt in einem
Kasten, damit die Spitze exakt auf der Position steht. */
/* poi-car aus dem CI-Satz ist eine reine Konturform in currentColor - auf
Kartenkacheln waere sie ohne Trennung zum Untergrund kaum lesbar. Die
gestapelten weissen drop-shadows ziehen deshalb einen Halo um die Kontur,
der letzte Schatten gibt Tiefe. Feste Farbe statt Theme-Variable, damit die
Nadel in Tag- und Nachtansicht gleich aussieht. */
.fahrzeug-pin {
color: var(--red);
display: flex; align-items: center; justify-content: center;
filter: drop-shadow(0 0 1px #fff) drop-shadow(0 0 2px #fff) drop-shadow(0 2px 5px rgba(0,0,0,.45));
color: #182634;
filter: drop-shadow(0 0 1px #fff) drop-shadow(0 0 1px #fff)
drop-shadow(0 0 2px #fff) drop-shadow(0 2px 4px rgba(0,0,0,.4));
}
.user-pin-wrap { background: none; border: none; }
.user-pin {