diff --git a/custom_components/houseplan/frontend/houseplan-card.js b/custom_components/houseplan/frontend/houseplan-card.js
index 0578536..1e13acf 100755
--- a/custom_components/houseplan/frontend/houseplan-card.js
+++ b/custom_components/houseplan/frontend/houseplan-card.js
@@ -1294,13 +1294,20 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
}
.vactrail polyline {
fill: none;
- stroke: var(--hp-accent);
- stroke-opacity: 0.35;
- stroke-width: 3;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
+ /* dark halo + light core: neutral, and one of the two always contrasts
+ with whatever fill is underneath */
+ .vactrail .case {
+ stroke: rgba(0, 0, 0, 0.4);
+ stroke-width: 4.5;
+ }
+ .vactrail .core {
+ stroke: rgba(255, 255, 255, 0.82);
+ stroke-width: 1.8;
+ }
.vacbox .vacbtns { display: flex; gap: 8px; margin: 6px 0; flex-wrap: wrap; }
.vaccalbar {
position: fixed;
@@ -2130,7 +2137,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
${a.length?B`
`}_vacMapId(t,e){if("default"!==e.mapId)return e.mapId;const i=this._vacEntity(t),s=i?this.hass?.states[i]?.attributes?.selected_map:null;return s?String(s):"default"}_vacSaveMatrix(t,e,i,s){const o=this._serverCfg,n=o?.markers?.find(e=>e.id===t);if(!n)return;const r={...n.vacuum||{}};r.source=e,r.calibration={...r.calibration||{},[i]:s.map(t=>Number(t.toFixed(6)))},n.vacuum=r,this._regSignature="",this._saveConfig(),this.requestUpdate()}_vacAutoCalibrate(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;if(!e||!i||i.rooms.length<3)return void this._showToast(this._t("vac.autocal_no_rooms"));const s=this._spaceModel(t.space),o=(s?.rooms||[]).filter(t=>t.name&&t.poly?.length>=3).map(t=>{const e=Ae(t.poly);return{name:t.name,cx:e[0],cy:e[1]}}),n=function(t,e){const i=new Map(e.map(t=>[Ai(t.name),t])),s=[],o=[];for(const e of t){const t=i.get(Ai(e.name));t&&(s.push([[e.cx,e.cy],[t.cx,t.cy]]),o.push(e.name))}if(s.length<3)return null;const n=Di(s);return n?{matrix:n,matched:o,residual:Ti(n,s)}:null}(i.rooms,o);n?(n.residual>50&&this._showToast(ti(this._t("vac.autocal_res_warn"),{rooms:String(n.matched.length)})),this._vacSaveMatrix(t.id,e,this._vacMapId(t,i),n.matrix),this._showToast(ti(this._t("vac.autocal_done"),{rooms:String(n.matched.length)}))):this._showToast(this._t("vac.autocal_no_match"))}_vacStartWizard(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;e&&i?.pos?(this._markerDialog=null,this._vacCal={markerId:t.id,source:e,mapId:this._vacMapId(t,i),pairs:[]}):this._showToast(this._t("vac.cal_need_pos"))}_vacCalClick(t){const e=this._vacCal;if(!e)return;const i=Pi(this.hass?.states[e.source]?.attributes);if(!i?.pos)return void this._showToast(this._t("vac.cal_need_pos"));const s=this._svgPoint(t),o=[...e.pairs,[[i.pos.x,i.pos.y],[s[0],s[1]]]];if(o.length<3)return void(this._vacCal={...e,pairs:o});const n=Di(o);return n?Ti(n,o)>50&&o.length<4?(this._vacCal={...e,pairs:o},void this._showToast(this._t("vac.cal_res_warn"))):(this._vacSaveMatrix(e.markerId,e.source,e.mapId,n),this._vacCal=null,void this._showToast(this._t("vac.cal_done"))):(this._vacCal={...e,pairs:o.slice(0,-1)},void this._showToast(this._t("vac.cal_degenerate")))}_renderVacuums(t,e){if(this._markup||"decor"===this._mode)return G;const i=[],s=[];for(const o of t){if(o.hidden||!this._isVacDev(o))continue;const t=this._vacSource(o);if(!t)continue;const n=Pi(this.hass?.states[t]?.attributes);if(!n)continue;const r=o.marker?.vacuum?.calibration?.[this._vacMapId(o,n)];if(!r||6!==r.length)continue;const a=this._vacRt.get(o.id),l=a?.moving??!1,c=!l&&a?.endedTs&&Date.now()-a.endedTs<6e5;if(!1!==o.marker?.vacuum?.trail&&(l||c)){const t=n.path||a?.trail||[];if(t.length>1){const e=t.map(([t,e])=>{const[i,s]=Ci(r,t,e);return i.toFixed(1)+","+s.toFixed(1)}).join(" ");s.push(j`
`)}}if(!l||!n.pos)continue;const[h,d]=Ci(r,n.pos.x,n.pos.y),p=(h-e.x)/e.w*100,u=(d-e.y)/e.h*100,_=a&&a.lastTs>0&&Date.now()-a.lastTs>6e4,m=o.marker?.icon||o.icon||"mdi:robot-vacuum";i.push(B`
{
const trail = sr().querySelector('.vactrail polyline');
o.trailDrawn = !!trail && trail.getAttribute('points').split(' ').length >= 3;
o.trailScaledByMatrix = !!trail && trail.getAttribute('points').startsWith('300.0,400.0');
+ // casing pair: dark halo + light core with identical geometry — the trail
+ // must survive any room fill underneath (owner 2026-07-31)
+ const tcase = sr().querySelector('.vactrail .case');
+ const tcore = sr().querySelector('.vactrail .core');
+ o.trailCasing = !!tcase && !!tcore
+ && tcase.getAttribute('points') === tcore.getAttribute('points')
+ && parseFloat(getComputedStyle(tcase).strokeWidth) > parseFloat(getComputedStyle(tcore).strokeWidth)
+ && getComputedStyle(tcase).stroke.includes('0, 0, 0')
+ && getComputedStyle(tcore).stroke.includes('255, 255, 255');
// trail off via marker option
@@ -128,7 +137,7 @@ checkAll(out, {
dockedNoPuck: true, baseMarkerThere: true, puckAppears: true, puckRound: true,
puckPlateMatchesDev: true, puck80pct: true,
noWedge: true, iconCentred: true, baseStaysDuringCleaning: true, trailDrawn: true,
- trailScaledByMatrix: true, trailToggleOff: true,
+ trailScaledByMatrix: true, trailCasing: true, trailToggleOff: true,
puckGoneWhenDocked: true, trailLingers: true, hiddenNoPuck: true,
unknownMapNoPuck: true,
wizardDevFound: true, wizardBanner: true, wizardSavedMatrix: true,
diff --git a/demo/srv/assets/houseplan-card.js b/demo/srv/assets/houseplan-card.js
index 0578536..1e13acf 100755
--- a/demo/srv/assets/houseplan-card.js
+++ b/demo/srv/assets/houseplan-card.js
@@ -1294,13 +1294,20 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
}
.vactrail polyline {
fill: none;
- stroke: var(--hp-accent);
- stroke-opacity: 0.35;
- stroke-width: 3;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
+ /* dark halo + light core: neutral, and one of the two always contrasts
+ with whatever fill is underneath */
+ .vactrail .case {
+ stroke: rgba(0, 0, 0, 0.4);
+ stroke-width: 4.5;
+ }
+ .vactrail .core {
+ stroke: rgba(255, 255, 255, 0.82);
+ stroke-width: 1.8;
+ }
.vacbox .vacbtns { display: flex; gap: 8px; margin: 6px 0; flex-wrap: wrap; }
.vaccalbar {
position: fixed;
@@ -2130,7 +2137,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
${a.length?B`
${ti(this._t("vac.cal_maps"),{maps:a.join(", ")})}
`:G}
`:G}
-
`}_vacMapId(t,e){if("default"!==e.mapId)return e.mapId;const i=this._vacEntity(t),s=i?this.hass?.states[i]?.attributes?.selected_map:null;return s?String(s):"default"}_vacSaveMatrix(t,e,i,s){const o=this._serverCfg,n=o?.markers?.find(e=>e.id===t);if(!n)return;const r={...n.vacuum||{}};r.source=e,r.calibration={...r.calibration||{},[i]:s.map(t=>Number(t.toFixed(6)))},n.vacuum=r,this._regSignature="",this._saveConfig(),this.requestUpdate()}_vacAutoCalibrate(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;if(!e||!i||i.rooms.length<3)return void this._showToast(this._t("vac.autocal_no_rooms"));const s=this._spaceModel(t.space),o=(s?.rooms||[]).filter(t=>t.name&&t.poly?.length>=3).map(t=>{const e=Ae(t.poly);return{name:t.name,cx:e[0],cy:e[1]}}),n=function(t,e){const i=new Map(e.map(t=>[Ai(t.name),t])),s=[],o=[];for(const e of t){const t=i.get(Ai(e.name));t&&(s.push([[e.cx,e.cy],[t.cx,t.cy]]),o.push(e.name))}if(s.length<3)return null;const n=Di(s);return n?{matrix:n,matched:o,residual:Ti(n,s)}:null}(i.rooms,o);n?(n.residual>50&&this._showToast(ti(this._t("vac.autocal_res_warn"),{rooms:String(n.matched.length)})),this._vacSaveMatrix(t.id,e,this._vacMapId(t,i),n.matrix),this._showToast(ti(this._t("vac.autocal_done"),{rooms:String(n.matched.length)}))):this._showToast(this._t("vac.autocal_no_match"))}_vacStartWizard(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;e&&i?.pos?(this._markerDialog=null,this._vacCal={markerId:t.id,source:e,mapId:this._vacMapId(t,i),pairs:[]}):this._showToast(this._t("vac.cal_need_pos"))}_vacCalClick(t){const e=this._vacCal;if(!e)return;const i=Pi(this.hass?.states[e.source]?.attributes);if(!i?.pos)return void this._showToast(this._t("vac.cal_need_pos"));const s=this._svgPoint(t),o=[...e.pairs,[[i.pos.x,i.pos.y],[s[0],s[1]]]];if(o.length<3)return void(this._vacCal={...e,pairs:o});const n=Di(o);return n?Ti(n,o)>50&&o.length<4?(this._vacCal={...e,pairs:o},void this._showToast(this._t("vac.cal_res_warn"))):(this._vacSaveMatrix(e.markerId,e.source,e.mapId,n),this._vacCal=null,void this._showToast(this._t("vac.cal_done"))):(this._vacCal={...e,pairs:o.slice(0,-1)},void this._showToast(this._t("vac.cal_degenerate")))}_renderVacuums(t,e){if(this._markup||"decor"===this._mode)return G;const i=[],s=[];for(const o of t){if(o.hidden||!this._isVacDev(o))continue;const t=this._vacSource(o);if(!t)continue;const n=Pi(this.hass?.states[t]?.attributes);if(!n)continue;const r=o.marker?.vacuum?.calibration?.[this._vacMapId(o,n)];if(!r||6!==r.length)continue;const a=this._vacRt.get(o.id),l=a?.moving??!1,c=!l&&a?.endedTs&&Date.now()-a.endedTs<6e5;if(!1!==o.marker?.vacuum?.trail&&(l||c)){const t=n.path||a?.trail||[];if(t.length>1){const e=t.map(([t,e])=>{const[i,s]=Ci(r,t,e);return i.toFixed(1)+","+s.toFixed(1)}).join(" ");s.push(j`
`)}}if(!l||!n.pos)continue;const[h,d]=Ci(r,n.pos.x,n.pos.y),p=(h-e.x)/e.w*100,u=(d-e.y)/e.h*100,_=a&&a.lastTs>0&&Date.now()-a.lastTs>6e4,m=o.marker?.icon||o.icon||"mdi:robot-vacuum";i.push(B`
`}_vacMapId(t,e){if("default"!==e.mapId)return e.mapId;const i=this._vacEntity(t),s=i?this.hass?.states[i]?.attributes?.selected_map:null;return s?String(s):"default"}_vacSaveMatrix(t,e,i,s){const o=this._serverCfg,n=o?.markers?.find(e=>e.id===t);if(!n)return;const r={...n.vacuum||{}};r.source=e,r.calibration={...r.calibration||{},[i]:s.map(t=>Number(t.toFixed(6)))},n.vacuum=r,this._regSignature="",this._saveConfig(),this.requestUpdate()}_vacAutoCalibrate(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;if(!e||!i||i.rooms.length<3)return void this._showToast(this._t("vac.autocal_no_rooms"));const s=this._spaceModel(t.space),o=(s?.rooms||[]).filter(t=>t.name&&t.poly?.length>=3).map(t=>{const e=Ae(t.poly);return{name:t.name,cx:e[0],cy:e[1]}}),n=function(t,e){const i=new Map(e.map(t=>[Ai(t.name),t])),s=[],o=[];for(const e of t){const t=i.get(Ai(e.name));t&&(s.push([[e.cx,e.cy],[t.cx,t.cy]]),o.push(e.name))}if(s.length<3)return null;const n=Di(s);return n?{matrix:n,matched:o,residual:Ti(n,s)}:null}(i.rooms,o);n?(n.residual>50&&this._showToast(ti(this._t("vac.autocal_res_warn"),{rooms:String(n.matched.length)})),this._vacSaveMatrix(t.id,e,this._vacMapId(t,i),n.matrix),this._showToast(ti(this._t("vac.autocal_done"),{rooms:String(n.matched.length)}))):this._showToast(this._t("vac.autocal_no_match"))}_vacStartWizard(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;e&&i?.pos?(this._markerDialog=null,this._vacCal={markerId:t.id,source:e,mapId:this._vacMapId(t,i),pairs:[]}):this._showToast(this._t("vac.cal_need_pos"))}_vacCalClick(t){const e=this._vacCal;if(!e)return;const i=Pi(this.hass?.states[e.source]?.attributes);if(!i?.pos)return void this._showToast(this._t("vac.cal_need_pos"));const s=this._svgPoint(t),o=[...e.pairs,[[i.pos.x,i.pos.y],[s[0],s[1]]]];if(o.length<3)return void(this._vacCal={...e,pairs:o});const n=Di(o);return n?Ti(n,o)>50&&o.length<4?(this._vacCal={...e,pairs:o},void this._showToast(this._t("vac.cal_res_warn"))):(this._vacSaveMatrix(e.markerId,e.source,e.mapId,n),this._vacCal=null,void this._showToast(this._t("vac.cal_done"))):(this._vacCal={...e,pairs:o.slice(0,-1)},void this._showToast(this._t("vac.cal_degenerate")))}_renderVacuums(t,e){if(this._markup||"decor"===this._mode)return G;const i=[],s=[];for(const o of t){if(o.hidden||!this._isVacDev(o))continue;const t=this._vacSource(o);if(!t)continue;const n=Pi(this.hass?.states[t]?.attributes);if(!n)continue;const r=o.marker?.vacuum?.calibration?.[this._vacMapId(o,n)];if(!r||6!==r.length)continue;const a=this._vacRt.get(o.id),l=a?.moving??!1,c=!l&&a?.endedTs&&Date.now()-a.endedTs<6e5;if(!1!==o.marker?.vacuum?.trail&&(l||c)){const t=n.path||a?.trail||[];if(t.length>1){const e=t.map(([t,e])=>{const[i,s]=Ci(r,t,e);return i.toFixed(1)+","+s.toFixed(1)}).join(" ");s.push(j`
`)}}if(!l||!n.pos)continue;const[h,d]=Ci(r,n.pos.x,n.pos.y),p=(h-e.x)/e.w*100,u=(d-e.y)/e.h*100,_=a&&a.lastTs>0&&Date.now()-a.lastTs>6e4,m=o.marker?.icon||o.icon||"mdi:robot-vacuum";i.push(B`
${a.length?B`
${ti(this._t("vac.cal_maps"),{maps:a.join(", ")})}
`:G}
`:G}
-
`}_vacMapId(t,e){if("default"!==e.mapId)return e.mapId;const i=this._vacEntity(t),s=i?this.hass?.states[i]?.attributes?.selected_map:null;return s?String(s):"default"}_vacSaveMatrix(t,e,i,s){const o=this._serverCfg,n=o?.markers?.find(e=>e.id===t);if(!n)return;const r={...n.vacuum||{}};r.source=e,r.calibration={...r.calibration||{},[i]:s.map(t=>Number(t.toFixed(6)))},n.vacuum=r,this._regSignature="",this._saveConfig(),this.requestUpdate()}_vacAutoCalibrate(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;if(!e||!i||i.rooms.length<3)return void this._showToast(this._t("vac.autocal_no_rooms"));const s=this._spaceModel(t.space),o=(s?.rooms||[]).filter(t=>t.name&&t.poly?.length>=3).map(t=>{const e=Ae(t.poly);return{name:t.name,cx:e[0],cy:e[1]}}),n=function(t,e){const i=new Map(e.map(t=>[Ai(t.name),t])),s=[],o=[];for(const e of t){const t=i.get(Ai(e.name));t&&(s.push([[e.cx,e.cy],[t.cx,t.cy]]),o.push(e.name))}if(s.length<3)return null;const n=Di(s);return n?{matrix:n,matched:o,residual:Ti(n,s)}:null}(i.rooms,o);n?(n.residual>50&&this._showToast(ti(this._t("vac.autocal_res_warn"),{rooms:String(n.matched.length)})),this._vacSaveMatrix(t.id,e,this._vacMapId(t,i),n.matrix),this._showToast(ti(this._t("vac.autocal_done"),{rooms:String(n.matched.length)}))):this._showToast(this._t("vac.autocal_no_match"))}_vacStartWizard(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;e&&i?.pos?(this._markerDialog=null,this._vacCal={markerId:t.id,source:e,mapId:this._vacMapId(t,i),pairs:[]}):this._showToast(this._t("vac.cal_need_pos"))}_vacCalClick(t){const e=this._vacCal;if(!e)return;const i=Pi(this.hass?.states[e.source]?.attributes);if(!i?.pos)return void this._showToast(this._t("vac.cal_need_pos"));const s=this._svgPoint(t),o=[...e.pairs,[[i.pos.x,i.pos.y],[s[0],s[1]]]];if(o.length<3)return void(this._vacCal={...e,pairs:o});const n=Di(o);return n?Ti(n,o)>50&&o.length<4?(this._vacCal={...e,pairs:o},void this._showToast(this._t("vac.cal_res_warn"))):(this._vacSaveMatrix(e.markerId,e.source,e.mapId,n),this._vacCal=null,void this._showToast(this._t("vac.cal_done"))):(this._vacCal={...e,pairs:o.slice(0,-1)},void this._showToast(this._t("vac.cal_degenerate")))}_renderVacuums(t,e){if(this._markup||"decor"===this._mode)return G;const i=[],s=[];for(const o of t){if(o.hidden||!this._isVacDev(o))continue;const t=this._vacSource(o);if(!t)continue;const n=Pi(this.hass?.states[t]?.attributes);if(!n)continue;const r=o.marker?.vacuum?.calibration?.[this._vacMapId(o,n)];if(!r||6!==r.length)continue;const a=this._vacRt.get(o.id),l=a?.moving??!1,c=!l&&a?.endedTs&&Date.now()-a.endedTs<6e5;if(!1!==o.marker?.vacuum?.trail&&(l||c)){const t=n.path||a?.trail||[];if(t.length>1){const e=t.map(([t,e])=>{const[i,s]=Ci(r,t,e);return i.toFixed(1)+","+s.toFixed(1)}).join(" ");s.push(j`
`)}}if(!l||!n.pos)continue;const[h,d]=Ci(r,n.pos.x,n.pos.y),p=(h-e.x)/e.w*100,u=(d-e.y)/e.h*100,_=a&&a.lastTs>0&&Date.now()-a.lastTs>6e4,m=o.marker?.icon||o.icon||"mdi:robot-vacuum";i.push(B`
`}_vacMapId(t,e){if("default"!==e.mapId)return e.mapId;const i=this._vacEntity(t),s=i?this.hass?.states[i]?.attributes?.selected_map:null;return s?String(s):"default"}_vacSaveMatrix(t,e,i,s){const o=this._serverCfg,n=o?.markers?.find(e=>e.id===t);if(!n)return;const r={...n.vacuum||{}};r.source=e,r.calibration={...r.calibration||{},[i]:s.map(t=>Number(t.toFixed(6)))},n.vacuum=r,this._regSignature="",this._saveConfig(),this.requestUpdate()}_vacAutoCalibrate(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;if(!e||!i||i.rooms.length<3)return void this._showToast(this._t("vac.autocal_no_rooms"));const s=this._spaceModel(t.space),o=(s?.rooms||[]).filter(t=>t.name&&t.poly?.length>=3).map(t=>{const e=Ae(t.poly);return{name:t.name,cx:e[0],cy:e[1]}}),n=function(t,e){const i=new Map(e.map(t=>[Ai(t.name),t])),s=[],o=[];for(const e of t){const t=i.get(Ai(e.name));t&&(s.push([[e.cx,e.cy],[t.cx,t.cy]]),o.push(e.name))}if(s.length<3)return null;const n=Di(s);return n?{matrix:n,matched:o,residual:Ti(n,s)}:null}(i.rooms,o);n?(n.residual>50&&this._showToast(ti(this._t("vac.autocal_res_warn"),{rooms:String(n.matched.length)})),this._vacSaveMatrix(t.id,e,this._vacMapId(t,i),n.matrix),this._showToast(ti(this._t("vac.autocal_done"),{rooms:String(n.matched.length)}))):this._showToast(this._t("vac.autocal_no_match"))}_vacStartWizard(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;e&&i?.pos?(this._markerDialog=null,this._vacCal={markerId:t.id,source:e,mapId:this._vacMapId(t,i),pairs:[]}):this._showToast(this._t("vac.cal_need_pos"))}_vacCalClick(t){const e=this._vacCal;if(!e)return;const i=Pi(this.hass?.states[e.source]?.attributes);if(!i?.pos)return void this._showToast(this._t("vac.cal_need_pos"));const s=this._svgPoint(t),o=[...e.pairs,[[i.pos.x,i.pos.y],[s[0],s[1]]]];if(o.length<3)return void(this._vacCal={...e,pairs:o});const n=Di(o);return n?Ti(n,o)>50&&o.length<4?(this._vacCal={...e,pairs:o},void this._showToast(this._t("vac.cal_res_warn"))):(this._vacSaveMatrix(e.markerId,e.source,e.mapId,n),this._vacCal=null,void this._showToast(this._t("vac.cal_done"))):(this._vacCal={...e,pairs:o.slice(0,-1)},void this._showToast(this._t("vac.cal_degenerate")))}_renderVacuums(t,e){if(this._markup||"decor"===this._mode)return G;const i=[],s=[];for(const o of t){if(o.hidden||!this._isVacDev(o))continue;const t=this._vacSource(o);if(!t)continue;const n=Pi(this.hass?.states[t]?.attributes);if(!n)continue;const r=o.marker?.vacuum?.calibration?.[this._vacMapId(o,n)];if(!r||6!==r.length)continue;const a=this._vacRt.get(o.id),l=a?.moving??!1,c=!l&&a?.endedTs&&Date.now()-a.endedTs<6e5;if(!1!==o.marker?.vacuum?.trail&&(l||c)){const t=n.path||a?.trail||[];if(t.length>1){const e=t.map(([t,e])=>{const[i,s]=Ci(r,t,e);return i.toFixed(1)+","+s.toFixed(1)}).join(" ");s.push(j`
`)}}if(!l||!n.pos)continue;const[h,d]=Ci(r,n.pos.x,n.pos.y),p=(h-e.x)/e.w*100,u=(d-e.y)/e.h*100,_=a&&a.lastTs>0&&Date.now()-a.lastTs>6e4,m=o.marker?.icon||o.icon||"mdi:robot-vacuum";i.push(B`
`);
+ // cartography casing: a dark halo under a light core. Neutral and
+ // visible over ANY room fill — blend modes all have a blind
+ // luminance where the line vanishes (owner request 2026-07-31).
+ trails.push(svg`
`);
}
}
if (!moving || !tele.pos) continue;
diff --git a/src/styles.ts b/src/styles.ts
index 1a40a84..a06a5ba 100644
--- a/src/styles.ts
+++ b/src/styles.ts
@@ -1291,13 +1291,20 @@ export const cardStyles = css`
}
.vactrail polyline {
fill: none;
- stroke: var(--hp-accent);
- stroke-opacity: 0.35;
- stroke-width: 3;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
+ /* dark halo + light core: neutral, and one of the two always contrasts
+ with whatever fill is underneath */
+ .vactrail .case {
+ stroke: rgba(0, 0, 0, 0.4);
+ stroke-width: 4.5;
+ }
+ .vactrail .core {
+ stroke: rgba(255, 255, 255, 0.82);
+ stroke-width: 1.8;
+ }
.vacbox .vacbtns { display: flex; gap: 8px; margin: 6px 0; flex-wrap: wrap; }
.vaccalbar {
position: fixed;