feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+36 -36
View File
@@ -35,23 +35,23 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text'
#city-name{font-size:24px;font-weight:900;letter-spacing:-0.8px;color:var(--text);line-height:1}
#city-region{font-size:11px;color:var(--t2);margin-top:3px;font-weight:500}
#city-stats{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:12px}
.cstat{background:var(--s2);border:1px solid var(--border);border-radius:7px;padding:8px 10px}
.cstat{background:var(--s2);border:1px solid var(--border);border-radius: var(--radius-sm);padding:8px 10px}
.cstat-val{font-size:16px;font-weight:800;color:var(--text);letter-spacing:-0.3px}
.cstat-lbl{font-size:9px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase;color:var(--t3);margin-top:1px}
#profile-block{padding:12px 16px;border-bottom:1px solid var(--border)}
#profile-tabs{display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;background:rgba(0,0,0,0.06);border-radius:10px;padding:2px}
.ptab{padding:6px 4px;border-radius:8px;border:none;background:transparent;cursor:pointer;font-size:10px;font-weight:600;color:var(--t2);transition:all 0.15s}
#profile-tabs{display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;background:rgba(0,0,0,0.06);border-radius: var(--radius-md);padding:2px}
.ptab{padding:6px 4px;border-radius: var(--radius-md);border:none;background:transparent;cursor:pointer;font-size:10px;font-weight:600;color:var(--t2);transition:all 0.15s}
.ptab:hover{background:var(--s2)}
.ptab.active{background:#fff;color:var(--text);box-shadow:0 1px 4px rgba(0,0,0,0.12)}
#district-block{padding:12px 16px}
#district-lbl{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);margin-bottom:8px}
.dcard{border-radius:12px;background:var(--s2);padding:11px 13px;margin-bottom:6px;cursor:pointer;transition:all 0.12s}
.dcard{border-radius: var(--radius-md);background:var(--s2);padding:11px 13px;margin-bottom:6px;cursor:pointer;transition:all 0.12s}
.dcard:hover,.dcard.active{background:var(--s3)}
.dcard-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:3px}
.dcard-name{font-size:13px;font-weight:700;color:var(--text)}
.dcard-score{font-size:11px;font-weight:800;padding:1px 7px;border-radius:4px;font-family:var(--mono)}
.dcard-score{font-size:11px;font-weight:800;padding:1px 7px;border-radius: var(--radius-sm);font-family:var(--mono)}
.dcard-desc{font-size:12px;color:var(--t2);line-height:1.4;margin-bottom:5px}
.dcard-contra{display:inline-flex;align-items:center;gap:4px;font-size:9px;font-weight:700;border-radius:4px;padding:2px 7px}
.dcard-contra{display:inline-flex;align-items:center;gap:4px;font-size:9px;font-weight:700;border-radius: var(--radius-sm);padding:2px 7px}
.dc-low{background:rgba(22,163,74,0.12);color:#166534}
.dc-med{background:rgba(217,119,6,0.12);color:#92400e}
.dc-high{background:rgba(220,38,38,0.12);color:#991b1b}
@@ -61,7 +61,7 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text'
.fresh-row:last-child{border-bottom:none}
.fresh-source{font-size:10px;color:var(--t2);font-weight:500}
.fresh-val{font-size:9px;font-weight:700;font-family:var(--mono);display:flex;align-items:center;gap:4px}
.fresh-dot{width:5px;height:5px;border-radius:50%}
.fresh-dot{width:5px;height:5px;border-radius: var(--radius-full)}
.fresh-dot.live{background:#22c55e;animation:pulse-ring 1.5s ease-in-out infinite}
.fresh-dot.recent{background:#fbbf24}
#map-wrap{position:relative;overflow:hidden;background:var(--bg)}
@@ -71,13 +71,13 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text'
#map-logo em{color:var(--accent3);font-style:normal}
.map-sep{width:1px;height:16px;background:rgba(0,0,0,0.12)}
#map-city-label{font-size:12px;font-weight:700;color:var(--text);flex:1}
#map-profile-chip{font-size:10px;font-weight:700;border-radius:4px;padding:3px 10px;border:1px solid}
#map-profile-chip{font-size:10px;font-weight:700;border-radius: var(--radius-sm);padding:3px 10px;border:1px solid}
#layer-bar{position:absolute;top:44px;left:0;right:0;z-index:999;display:flex;align-items:center;gap:4px;padding:5px 8px;background:#fff;border-bottom:1px solid rgba(0,0,0,0.1);overflow-x:auto;scrollbar-width:none}
.ltoggle{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:600;color:var(--t2);background:rgba(0,0,0,0.05);border:none;border-radius:20px;padding:5px 12px;cursor:pointer;white-space:nowrap;user-select:none;flex-shrink:0}
.ltoggle{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:600;color:var(--t2);background:rgba(0,0,0,0.05);border:none;border-radius: var(--radius-xl);padding:5px 12px;cursor:pointer;white-space:nowrap;user-select:none;flex-shrink:0}
.ltoggle.on{background:var(--accent);color: #1d1d1f}
.ltoggle-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
.ltoggle-dot{width:6px;height:6px;border-radius: var(--radius-full);flex-shrink:0}
.leaflet-control-zoom{margin-top:96px!important;margin-left:10px!important}
.leaflet-popup-content-wrapper{background:#fff!important;border:none!important;border-radius:16px!important;box-shadow:0 8px 32px rgba(0,0,0,0.15)!important;padding:0!important}
.leaflet-popup-content-wrapper{background:#fff!important;border:none!important;border-radius: 16px!important;box-shadow:0 8px 32px rgba(0,0,0,0.15)!important;padding:0!important}
.leaflet-popup-content{margin:0!important;width:260px!important}
.leaflet-popup-tip{background:#fff!important}
.mpop{padding:14px 15px}
@@ -88,10 +88,10 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text'
.mpop-bars{display:flex;flex-direction:column;gap:5px;margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid var(--border)}
.mpop-bar-row{display:flex;align-items:center;gap:6px}
.mpop-bar-lbl{font-size:9px;font-weight:600;text-transform:uppercase;color:var(--t3);width:72px;flex-shrink:0}
.mpop-bar-track{flex:1;height:3px;border-radius:2px;background:rgba(0,0,0,0.06)}
.mpop-bar-fill{height:100%;border-radius:2px}
.mpop-bar-track{flex:1;height:3px;border-radius: var(--radius-sm);background:rgba(0,0,0,0.06)}
.mpop-bar-fill{height:100%;border-radius: var(--radius-sm)}
.mpop-bar-val{font-size:10px;font-weight:700;font-family:var(--mono);width:24px;text-align:right}
.mpop-contra{font-size:10px;border-radius:6px;padding:6px 9px;line-height:1.4;margin-bottom:8px}
.mpop-contra{font-size:10px;border-radius: var(--radius-sm);padding:6px 9px;line-height:1.4;margin-bottom:8px}
.mpop-contra.low{background:rgba(22,163,74,0.08);color:#166534;border:1px solid rgba(22,163,74,0.15)}
.mpop-contra.medium{background:rgba(217,119,6,0.08);color:#92400e;border:1px solid rgba(217,119,6,0.15)}
.mpop-contra.high{background:rgba(220,38,38,0.08);color:#991b1b;border:1px solid rgba(220,38,38,0.15)}
@@ -102,7 +102,7 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text'
#right-profile-label{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);margin-bottom:4px}
#right-title{font-size:17px;font-weight:700;color:var(--text)}
#kpi-grid{padding:10px 16px 8px;display:grid;grid-template-columns:1fr 1fr;gap:6px;border-bottom:1px solid var(--border)}
.kpi{background:var(--s2);border-radius:12px;padding:12px 14px}
.kpi{background:var(--s2);border-radius: var(--radius-md);padding:12px 14px}
.kpi-label{font-size:10px;font-weight:700;letter-spacing:0.5px;text-transform:uppercase;margin-bottom:4px}
.kpi-value{font-size:22px;font-weight:800;font-family:var(--mono);line-height:1}
.kpi-sub{font-size:11px;color:var(--t2);margin-top:3px}
@@ -117,31 +117,31 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text'
#contra-block{padding:12px 16px;border-bottom:1px solid var(--border)}
#contra-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
#contra-title{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);display:flex;align-items:center;gap:5px}
.contra-live-dot{width:5px;height:5px;border-radius:50%;background:#f87171;animation:pulse-ring 1.8s ease-in-out infinite}
.contra-count{font-size:9px;font-weight:700;background:rgba(220,38,38,0.12);color:#991b1b;padding:1px 7px;border-radius:10px}
.contra-card{border-radius:7px;border:1px solid var(--border);background:var(--s2);padding:9px 11px;margin-bottom:5px}
.contra-live-dot{width:5px;height:5px;border-radius: var(--radius-full);background:#f87171;animation:pulse-ring 1.8s ease-in-out infinite}
.contra-count{font-size:9px;font-weight:700;background:rgba(220,38,38,0.12);color:#991b1b;padding:1px 7px;border-radius: var(--radius-md)}
.contra-card{border-radius: var(--radius-sm);border:1px solid var(--border);background:var(--s2);padding:9px 11px;margin-bottom:5px}
.contra-card.c-medium{border-left:3px solid rgba(217,119,6,0.5)}
.contra-card.c-high{border-left:3px solid rgba(220,38,38,0.6)}
.contra-card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:3px}
.contra-card-name{font-size:11px;font-weight:700;color:var(--text)}
.contra-card-badge{font-size:8px;font-weight:700;text-transform:uppercase;padding:1px 6px;border-radius:3px}
.contra-card-badge{font-size:8px;font-weight:700;text-transform:uppercase;padding:1px 6px;border-radius: var(--radius-sm)}
.contra-card-badge.medium{background:rgba(217,119,6,0.12);color:#92400e}
.contra-card-badge.high{background:rgba(220,38,38,0.12);color:#991b1b}
.contra-card-desc{font-size:10px;color:var(--t2);line-height:1.4}
#right-footer{padding:10px 16px;font-size:9px;color:var(--t3);line-height:1.4;border-top:1px solid var(--border)}
#bottom-sheet{display:none;position:fixed;left:0;right:0;bottom:0;z-index:9999;background:#fff;border-top:1px solid rgba(0,0,0,0.1);border-radius:16px 16px 0 0;box-shadow:0 -4px 24px rgba(0,0,0,0.12);max-height:72vh;overflow-y:auto;transform:translateY(100%);transition:transform 0.28s cubic-bezier(0.32,0.72,0,1)}
#bottom-sheet{display:none;position:fixed;left:0;right:0;bottom:0;z-index:9999;background:#fff;border-top:1px solid rgba(0,0,0,0.1);border-radius: var(--radius-lg) 16px 0 0;box-shadow:0 -4px 24px rgba(0,0,0,0.12);max-height:72vh;overflow-y:auto;transform:translateY(100%);transition:transform 0.28s cubic-bezier(0.32,0.72,0,1)}
#bottom-sheet.open{display:block;transform:translateY(0)}
#bottom-sheet-handle{width:36px;height:4px;border-radius:2px;background:rgba(0,0,0,0.12);margin:10px auto 0}
#bottom-sheet-close{position:absolute;top:12px;right:14px;background:rgba(0,0,0,0.06);border:none;color:var(--text);font-size:16px;width:28px;height:28px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center}
#bottom-sheet-handle{width:36px;height:4px;border-radius: var(--radius-sm);background:rgba(0,0,0,0.12);margin:10px auto 0}
#bottom-sheet-close{position:absolute;top:12px;right:14px;background:rgba(0,0,0,0.06);border:none;color:var(--text);font-size:16px;width:28px;height:28px;border-radius: var(--radius-full);cursor:pointer;display:flex;align-items:center;justify-content:center}
#bottom-sheet-content{padding:8px 0 env(safe-area-inset-bottom,16px)}
#bottom-sheet-backdrop{display:none;position:fixed;inset:0;z-index:9998;background:rgba(0,0,0,0.4)}
#bottom-sheet-backdrop.open{display:block}
#map-legend{position:absolute;bottom:60px;right:10px;z-index:800;background:rgba(0,0,0,0.92);backdrop-filter:blur(12px);border:1px solid rgba(0,0,0,0.08);border-radius:10px;padding:10px 12px}
#map-legend{position:absolute;bottom:60px;right:10px;z-index:800;background:rgba(0,0,0,0.92);backdrop-filter:blur(12px);border:1px solid rgba(0,0,0,0.08);border-radius: var(--radius-md);padding:10px 12px}
@keyframes pulse-ring{0%{opacity:1;transform:scale(1)}50%{opacity:0.6;transform:scale(1.2)}100%{opacity:1;transform:scale(1)}}
@keyframes pulse-wave{0%{transform:scale(1);opacity:0.7}100%{transform:scale(3.2);opacity:0}}
.pulse-marker{pointer-events:none}
.pulse-outer{position:absolute;border-radius:50%;animation:pulse-wave 2.2s ease-out infinite}
.pulse-inner{position:absolute;border-radius:50%;transform:translate(-50%,-50%)}
.pulse-outer{position:absolute;border-radius: var(--radius-full);animation:pulse-wave 2.2s ease-out infinite}
.pulse-inner{position:absolute;border-radius: var(--radius-full);transform:translate(-50%,-50%)}
@media(max-width:900px){
html,body{height:auto;overflow-y:auto;overflow-x:hidden}
#app{display:flex;flex-direction:column;height:auto;min-height:100dvh}
@@ -320,9 +320,9 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text'
<div id="map-city-label">Malmö — <span id="map-profile-label">Business</span></div>
<div id="map-profile-chip" style="background:rgba(0,122,255,0.12);color:#0066cc;border-color:rgba(0,122,255,0.25)">Business</div>
<div style="display:flex;gap:4px;margin-left:auto">
<button onclick="map.setView([55.605,13.004],12)" style="background:rgba(0,0,0,0.06);border:none;border-radius:6px;padding:4px 8px;font-size:10px;font-weight:600;color:rgba(28,28,30,0.6);cursor:pointer">MMA</button>
<button onclick="map.setView([55.7,13.2],9)" style="background:rgba(0,0,0,0.06);border:none;border-radius:6px;padding:4px 8px;font-size:10px;font-weight:600;color:rgba(28,28,30,0.6);cursor:pointer">Skåne</button>
<button onclick="window.location='/intelligence'" style="background:rgba(0,0,0,0.06);border:none;border-radius:6px;padding:4px 8px;font-size:10px;font-weight:600;color:rgba(28,28,30,0.6);cursor:pointer">All cities</button>
<button onclick="map.setView([55.605,13.004],12)" style="background:rgba(0,0,0,0.06);border:none;border-radius: var(--radius-sm);padding:4px 8px;font-size:10px;font-weight:600;color:rgba(28,28,30,0.6);cursor:pointer">MMA</button>
<button onclick="map.setView([55.7,13.2],9)" style="background:rgba(0,0,0,0.06);border:none;border-radius: var(--radius-sm);padding:4px 8px;font-size:10px;font-weight:600;color:rgba(28,28,30,0.6);cursor:pointer">Skåne</button>
<button onclick="window.location='/intelligence'" style="background:rgba(0,0,0,0.06);border:none;border-radius: var(--radius-sm);padding:4px 8px;font-size:10px;font-weight:600;color:rgba(28,28,30,0.6);cursor:pointer">All cities</button>
</div>
</div>
<div id="layer-bar">
@@ -343,10 +343,10 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text'
<div id="map-legend">
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.45);margin-bottom:7px">Intelligence Score</div>
<div style="display:flex;flex-direction:column;gap:5px">
<div style="display:flex;align-items:center;gap:7px;font-size:11px;color:rgba(28,28,30,0.75)"><div style="width:12px;height:12px;border-radius:50%;background:#22c55e;flex-shrink:0"></div>Strong (810)</div>
<div style="display:flex;align-items:center;gap:7px;font-size:11px;color:rgba(28,28,30,0.75)"><div style="width:12px;height:12px;border-radius:50%;background:#eab308;flex-shrink:0"></div>Moderate (68)</div>
<div style="display:flex;align-items:center;gap:7px;font-size:11px;color:rgba(28,28,30,0.75)"><div style="width:12px;height:12px;border-radius:50%;background:#f97316;flex-shrink:0"></div>Weak (46)</div>
<div style="display:flex;align-items:center;gap:7px;font-size:11px;color:rgba(28,28,30,0.75)"><div style="width:12px;height:12px;border-radius:50%;background:#ef4444;flex-shrink:0"></div>Poor (&lt;4)</div>
<div style="display:flex;align-items:center;gap:7px;font-size:11px;color:rgba(28,28,30,0.75)"><div style="width:12px;height:12px;border-radius: var(--radius-full);background:#22c55e;flex-shrink:0"></div>Strong (810)</div>
<div style="display:flex;align-items:center;gap:7px;font-size:11px;color:rgba(28,28,30,0.75)"><div style="width:12px;height:12px;border-radius: var(--radius-full);background:#eab308;flex-shrink:0"></div>Moderate (68)</div>
<div style="display:flex;align-items:center;gap:7px;font-size:11px;color:rgba(28,28,30,0.75)"><div style="width:12px;height:12px;border-radius: var(--radius-full);background:#f97316;flex-shrink:0"></div>Weak (46)</div>
<div style="display:flex;align-items:center;gap:7px;font-size:11px;color:rgba(28,28,30,0.75)"><div style="width:12px;height:12px;border-radius: var(--radius-full);background:#ef4444;flex-shrink:0"></div>Poor (&lt;4)</div>
</div>
<div style="font-size:9px;color:rgba(28,28,30,0.35);margin-top:6px">Tap any area for details</div>
</div>
@@ -379,7 +379,7 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text'
<div id="police-block" style="padding:12px 16px;border-bottom:1px solid var(--border)">
<div style="font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);margin-bottom:8px;display:flex;align-items:center;justify-content:space-between">
Police Events · Live
<span style="font-size:9px;font-weight:700;background:rgba(239,68,68,0.12);color:#991b1b;border-radius:4px;padding:1px 7px" id="police-count">Loading…</span>
<span style="font-size:9px;font-weight:700;background:rgba(239,68,68,0.12);color:#991b1b;border-radius: var(--radius-sm);padding:1px 7px" id="police-count">Loading…</span>
</div>
<div id="police-list" style="font-size:10px;color:var(--t3)">Loading…</div>
</div>
@@ -509,7 +509,7 @@ function buildList(p){
document.getElementById('district-list').innerHTML=sorted.map(d=>{
const v=d.s[p],col=sc(v);
const cClass=d.c==='high'?'dc-high':d.c==='medium'?'dc-med':'dc-low';
const cTxt=d.c==='high'?' HIGH CONTRA':d.c==='medium'?'· MEDIUM CONTRA':' LOW CONTRA';
const cTxt=d.c==='high'?' HIGH CONTRA':d.c==='medium'?'· MEDIUM CONTRA':'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> LOW CONTRA';
return `<div class="dcard" id="dc-${d.id}" onclick="focusDist('${d.id}')"><div class="dcard-head"><div class="dcard-name">${d.name}</div><div class="dcard-score" style="background:${col}18;color:${col}">${v.toFixed(1)}</div></div><div class="dcard-desc">${d.ct.length>80?d.ct.substring(0,80)+'…':d.ct}</div><div class="dcard-contra ${cClass}">${cTxt}</div></div>`;
}).join('');
}
@@ -573,7 +573,7 @@ function buildAlertMarkers(){
alertMarkers.forEach(m=>m.remove());alertMarkers=[];
D.filter(d=>d.c==='high'||d.c==='medium').forEach(d=>{
const col=d.c==='high'?'#f87171':'#fbbf24';
const icon=L.divIcon({className:'',html:`<div style="width:18px;height:18px;border-radius:3px;background:rgba(0,0,0,0.95);border:2px solid ${col};display:flex;align-items:center;justify-content:center;font-size:10px;box-shadow:0 0 10px ${col}66;cursor:pointer">${d.c==='high'?'':'!'}</div>`,iconSize:[18,18],iconAnchor:[9,9]});
const icon=L.divIcon({className:'',html:`<div style="width:18px;height:18px;border-radius: var(--radius-sm);background:rgba(0,0,0,0.95);border:2px solid ${col};display:flex;align-items:center;justify-content:center;font-size:10px;box-shadow:0 0 10px ${col}66;cursor:pointer">${d.c==='high'?'':'!'}</div>`,iconSize:[18,18],iconAnchor:[9,9]});
const ca=CONTRA_ALERTS.find(x=>x.distId===d.id);
const marker=L.marker([d.lat,d.lng],{icon,zIndexOffset:500});
if(ca)marker.bindPopup(`<div style="padding:10px 12px;min-width:200px"><div style="font-size:9px;font-weight:700;color:${col};text-transform:uppercase;margin-bottom:4px">CONTRADICTION ${d.c.toUpperCase()}</div><div style="font-size:11px;font-weight:600;margin-bottom:4px">${d.name}</div><div style="font-size:10px;color:rgba(28,28,30,0.7);line-height:1.4">${ca.desc}</div></div>`,{maxWidth:240});
@@ -633,7 +633,7 @@ window.addEventListener('DOMContentLoaded',()=>{
poly.bindPopup(buildPopup(d,'business'),{maxWidth:260,minWidth:250});
poly.on('click',()=>{if(isMobile()){setTimeout(()=>{map.closePopup();openBottomSheet(buildPopup(d,profile));},10);}});
poly.addTo(map);polys[d.id]=poly;
if(map.getZoom()>=11){L.marker([d.lat,d.lng],{icon:L.divIcon({className:'label-marker',html:`<div style="font-size:11px;font-weight:700;color:#1c1c1e;white-space:nowrap;pointer-events:none;background:rgba(0,0,0,0.75);padding:2px 6px;border-radius:4px">${d.name}</div>`,iconAnchor:[0,0]}),interactive:false}).addTo(map);}
if(map.getZoom()>=11){L.marker([d.lat,d.lng],{icon:L.divIcon({className:'label-marker',html:`<div style="font-size:11px;font-weight:700;color:#1c1c1e;white-space:nowrap;pointer-events:none;background:rgba(0,0,0,0.75);padding:2px 6px;border-radius: 4px">${d.name}</div>`,iconAnchor:[0,0]}),interactive:false}).addTo(map);}
});
document.getElementById('district-count').textContent=D.length;
buildPulseMarkers();buildAlertMarkers();buildList('business');buildRight('business');