Files
boc/backups/2026-07-12-1023/landvex-prod/intelligence-map.html
T
Bernt 58ca4e68db feat(boc): Complete Business Operations Center v1.0
- Go backend API with full CRUD for all modules (CRM, Sales, Finance, HR, Legal, Marketing, Support, Purchase, Inventory, Projects, Automation, Analytics)
- Rust analytics service with parallel report generation
- C runtime with POSIX shared memory IPC
- PostgreSQL schema with 30+ tables, full migrations
- Redis cache, sessions, pub/sub
- Kafka event streaming with Zookeeper
- WebSocket hub for real-time updates
- Automation engine with cron jobs, workflows, event triggers
- JWT authentication, multi-tenant from start
- Docker Compose with all services
- Nginx reverse proxy with rate limiting
- Integration tests passing
- Feature gap analysis against Fortnox/Odoo/Visma

Refs: BOC-001
2026-07-12 12:41:35 +00:00

889 lines
77 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
<title>Landvex Intelligence Map — Urban Intelligence Worldwide</title>
<meta name="description" content="Landvex global intelligence map. City scores, contradiction index and urban intelligence for 100+ cities. Free historical data, live intelligence on demand."/>
<link rel="stylesheet" href="/vendor/leaflet.css"/>
<script src="/vendor/leaflet.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#070c14;--surface:#0d1520;--surface2:#111e2e;--surface3:#162438;--border: rgba(0,0,0,0.08);--border2:rgba(0,0,0,0.12);--text:#e8eef5;--text2:rgba(232,238,245,0.55);--text3:rgba(232,238,245,0.32);--accent:#0ea5e9;--accent2:#38bdf8;--green:#22c55e;--yellow:#eab308;--orange:#f97316;--red:#ef4444;--gold:#f59e0b;--safe-top:env(safe-area-inset-top,0px);--safe-bottom:env(safe-area-inset-bottom,0px)}
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,sans-serif;background:var(--bg);color:var(--text);overflow:hidden}
#map{position:fixed;inset:0;z-index:1}
.leaflet-control-zoom{margin-top:calc(var(--safe-top) + 72px) !important;margin-left:16px !important}
.leaflet-control-zoom a{background:var(--surface) !important;color:var(--text) !important;border-color:var(--border2) !important;width:34px !important;height:34px !important;line-height:34px !important;border-radius: var(--radius-md) !important;font-size:18px !important}
#hdr{position:fixed;top:0;left:0;right:0;z-index:1000;background:rgba(7,12,20,0.96);backdrop-filter:blur(24px) saturate(200%);-webkit-backdrop-filter:blur(24px) saturate(200%);border-bottom:1px solid var(--border);padding:calc(var(--safe-top) + 8px) 20px 8px}
#hdr-i{max-width:1400px;margin:0 auto;display:flex;align-items:center;gap:12px;flex-wrap:nowrap;overflow:hidden}
#logo{font-size:16px;font-weight:800;letter-spacing:-0.5px;color:var(--text);text-decoration:none;white-space:nowrap}
#logo em{color:var(--accent);font-style:normal}
.hdiv{width:1px;height:20px;background:var(--border2)}
#hdr-title{font-size:13px;font-weight:600;color:var(--text2);flex:1}
.hpill{font-size:10px;font-weight:700;letter-spacing:0.6px;text-transform:uppercase;border-radius: var(--radius-xl);padding:3px 10px;white-space:nowrap}
.pill-free{background:rgba(34,197,94,0.12);color:#4ade80;border:1px solid rgba(34,197,94,0.25)}
.pill-live{background:rgba(14,165,233,0.15);color:var(--accent2);border:1px solid rgba(14,165,233,0.3)}
#hdr-cta{background:var(--accent);color:white;border:none;border-radius: var(--radius-md);padding:7px 16px;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap;text-decoration:none;display:inline-block;transition:opacity 0.2s}
#hdr-cta:hover{opacity:0.85}
#sidebar{position:fixed;top:calc(var(--safe-top) + 54px);left:16px;z-index:900;width:220px;display:flex;flex-direction:column;gap:6px;pointer-events:none}
.sbc{background:rgba(13,21,32,0.93);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid var(--border);border-radius: var(--radius-md);padding:12px 14px;pointer-events:all}
.sbl{font-size:10px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:var(--text2);margin-bottom:8px}
.lr{display:flex;align-items:center;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border);cursor:pointer}
.lr:last-child{border-bottom:none}
.ll{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:500;color:var(--text)}
.ld2{width:9px;height:9px;border-radius: var(--radius-full);flex-shrink:0}
.tog{width:32px;height:18px;border-radius: var(--radius-md);position:relative;transition:background 0.2s;flex-shrink:0}
.tog.on{background:var(--accent)}
.tog.off{background:var(--surface3)}
.tog::after{content:'';position:absolute;top:3px;left:3px;width:12px;height:12px;border-radius: var(--radius-full);background:white;transition:transform 0.2s}
.tog.on::after{transform:translateX(14px)}
.ig{display:flex;flex-direction:column;gap:3px}
.ir{display:flex;align-items:center;justify-content:space-between;padding:5px 6px;border-radius: var(--radius-sm);cursor:pointer;transition:background 0.15s}
.ir:hover{background:var(--surface3)}
.ir.active{background:rgba(14,165,233,0.12);border:1px solid rgba(14,165,233,0.2)}
.in{font-size:11px;color:var(--text);font-weight:500}
.ib{font-size:10px;font-weight:700;border-radius: var(--radius-sm);padding:2px 6px}
.ib-free{background:rgba(34,197,94,0.12);color:#4ade80}
.ib-paid{background:rgba(245,158,11,0.12);color:#fbbf24}
#bot{position:fixed;bottom:0;left:0;right:0;z-index:900;background:rgba(7,12,20,0.96);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-top:1px solid var(--border);padding:8px 20px calc(var(--safe-bottom) + 8px)}
#bot-i{max-width:1400px;margin:0 auto;display:flex;align-items:center;gap:12px;flex-wrap:nowrap;overflow:hidden}
#leg{display:flex;gap:10px;flex-wrap:wrap;flex:1}
.li{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--text2)}
.ld{width:10px;height:10px;border-radius: var(--radius-sm);flex-shrink:0}
#disc{font-size:10px;color:var(--text3);line-height:1.4;max-width:380px}
.leaflet-popup-pane{z-index:2000 !important}
.leaflet-popup-content-wrapper{background:#ffffff !important;border:none !important;border-radius: var(--radius-lg) !important;box-shadow:0 8px 40px rgba(0,0,0,0.18) !important;padding:0 !important;max-height:65vh !important;overflow-y:auto !important}
.leaflet-popup-content{margin:0 !important;width:280px !important;color:#0A0A0A !important;max-height:60vh !important;overflow-y:auto !important}
.leaflet-popup-tip{background:#ffffff !important}
.leaflet-popup-close-button{color:rgba(28,28,30,0.45) !important;top:10px !important;right:12px !important;font-size:18px !important}
.zpop{padding:16px 18px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
.zcity{font-size:15px;font-weight:800;color:#0A0A0A;letter-spacing:-0.3px}
.zcountry{font-size:12px;color:rgba(28,28,30,0.55);font-weight:400}
.zconf{font-size:10px;font-weight:600;background:#f2f2f7;border-radius: var(--radius-sm);padding:2px 7px;color:rgba(28,28,30,0.6);display:inline-block;margin:4px 0 10px}
.zsect{font-size:10px;font-weight:700;letter-spacing:0.7px;text-transform:uppercase;color:var(--text2);margin:8px 0 5px}
.zindices{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:8px}
.zidx{background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px}
.zil{font-size:10px;color:rgba(28,28,30,0.55);font-weight:500;margin-bottom:3px}
.zbar{height:3px;border-radius: var(--radius-sm);background:rgba(0,0,0,0.08);margin-bottom:3px}
.zfill{height:100%;border-radius: var(--radius-sm)}
.zval{font-size:13px;font-weight:800;color:#0A0A0A}
.zcontra{border-radius: var(--radius-md);padding:8px 11px;font-size:11px;line-height:1.5;margin-bottom:8px}
.zcontra.low{background:rgba(34,197,94,0.1);color:#86efac;border:1px solid rgba(34,197,94,0.18)}
.zcontra.medium{background:rgba(249,115,22,0.08);color:#92400e;border:1px solid rgba(249,115,22,0.2)}
.zcontra.high{background:rgba(239,68,68,0.08);color:#991b1b;border:1px solid rgba(239,68,68,0.2)}
.zcl{font-weight:800;display:block;margin-bottom:2px;font-size:11px;color:inherit}
.zage{font-size:11px;color:#16a34a;font-weight:600;margin-bottom:10px}
.lock-wrap{position:relative;margin-bottom:10px}
.lock-blur{filter:blur(3px);pointer-events:none;opacity:0.5}
.lock-over{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;background:rgba(7,12,20,0.75);border-radius: var(--radius-md);border:1px solid var(--border2)}
.lock-over span:first-child{font-size:16px}
.lock-over span:last-child{font-size:11px;color:var(--text2);font-weight:600}
.zcta{display:block;width:100%;background:#007aff;color:white;border:none;border-radius: var(--radius-md);padding:10px;font-size:12px;font-weight:700;cursor:pointer;text-align:center;text-decoration:none;transition:opacity 0.2s;margin-top:2px}
.zcta:hover{opacity:0.85}
.zdisc{font-size:10px;color:rgba(28,28,30,0.35);margin-top:8px;text-align:center;line-height:1.4}
.zdisc a{color:#007aff;text-decoration:none}
@media(max-width:640px){#sidebar{display:none}.hdiv,#hdr-title{display:none}#hdr-i{gap:6px;flex-wrap:nowrap;overflow:hidden}#hdr-cta{padding:6px 12px;font-size:11px;flex-shrink:0}.hpill{font-size:9px;padding:2px 7px}.pill-live{display:none}.cta-long{display:none}.cta-short{display:inline}}
@media(min-width:641px){.cta-short{display:none}.cta-long{display:inline}}
@media(max-width:640px){.leaflet-popup{display:none !important}}
/* Bottom sheet for mobile */
#bsheet{
display:none;position:fixed;left:0;right:0;bottom:0;z-index:9999;
background:#ffffff;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.15);
max-height:72vh;overflow-y:auto;
-webkit-overflow-scrolling:touch;
transform:translateY(100%);
transition:transform 0.28s cubic-bezier(0.32,0.72,0,1);
}
#bsheet.open{display:block;transform:translateY(0)}
#bsheet-handle{width:36px;height:4px;border-radius: var(--radius-sm);background:rgba(0,0,0,0.15);margin:10px auto 0}
#bsheet-close{position:absolute;top:10px;right:12px;background:rgba(0,0,0,0.06);border:none;color:#0A0A0A;font-size:16px;width:28px;height:28px;border-radius: var(--radius-full);cursor:pointer;display:flex;align-items:center;justify-content:center}
#bsheet-content{padding:8px 0 env(safe-area-inset-bottom,16px)}
#bsheet-backdrop{display:none;position:fixed;inset:0;z-index:9998;background:rgba(0,0,0,0.3)}
#bsheet-backdrop.open{display:block}
@media(min-width:640px){
.leaflet-popup-pane{z-index:2000 !important}
}
#bsheet-content .zpop{padding:14px 16px}
#bsheet-content .zcity{color:#0A0A0A !important}
#bsheet-content .zcountry{color:rgba(28,28,30,0.55) !important}
#bsheet-content .zconf{background:#f2f2f7 !important;color:rgba(28,28,30,0.6) !important}
#bsheet-content .zsect{color:rgba(28,28,30,0.45) !important}
#bsheet-content .zidx{background:#f2f2f7 !important}
#bsheet-content .zil{color:rgba(28,28,30,0.55) !important}
#bsheet-content .zval{color:#0A0A0A !important}
#bsheet-content .zbar{background:rgba(0,0,0,0.08) !important}
#bsheet-content .lock-over{background:rgba(0,0,0,0.6) !important}
#bsheet-content .lock-over span:last-child{color:rgba(28,28,30,0.65) !important}
#bsheet-content .zdisc{color:rgba(28,28,30,0.35) !important}
#bsheet-content .zdisc a{color:#007aff !important}
#bsheet-content .zcta{background:#007aff !important;color: #1d1d1ffff !important}
</style>
<style>
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
</head>
<body>
<div id="map"></div>
<div id="hdr"><div id="hdr-i">
<a href="https://www.landvex.com" id="logo">Land<em>vex</em></a>
<div class="hdiv"></div>
<div id="hdr-title">Intelligence Map</div>
<div class="hpill pill-free">Free historical data</div>
<div class="hpill pill-live">Live on demand</div>
<a href="https://www.landvex.com/enterprise" id="hdr-cta"><span class="cta-long">Request live intelligence</span><span class="cta-short">Go live →</span></a>
</div></div>
<div id="sidebar">
<div class="sbc">
<div class="sbl">Map layers</div>
<div class="lr" onclick="toggleLayer('zones')">
<div class="ll"><div class="ld2" style="background:#0ea5e9"></div>Intelligence Zones</div>
<div class="tog on" id="tog-zones"></div>
</div>
<div class="lr" onclick="toggleLayer('contra')">
<div class="ll"><div class="ld2" style="background:#f59e0b"></div>Contradiction Index</div>
<div class="tog on" id="tog-contra"></div>
</div>
</div>
<div class="sbc">
<div class="sbl">Indices</div>
<div class="ig">
<div class="ir active" onclick="selIdx(this,'infra')"><span class="in">Infrastructure Risk</span><span class="ib ib-free">Free</span></div>
<div class="ir" onclick="selIdx(this,'commercial')"><span class="in">Commercial Activity</span><span class="ib ib-free">Free</span></div>
<div class="ir" onclick="selIdx(this,'safety')"><span class="in">Safety Index</span><span class="ib ib-free">Free</span></div>
<div class="ir" onclick="selIdx(this,'climate')"><span class="in">Climate Risk</span><span class="ib ib-free">Free</span></div>
<div class="ir" onclick="selIdx(this,'growth')"><span class="in">Urban Growth</span><span class="ib ib-paid">Live</span></div>
<div class="ir" onclick="selIdx(this,'crowd')"><span class="in">Crowd Density</span><span class="ib ib-paid">Live</span></div>
<div class="ir" onclick="selIdx(this,'decay')"><span class="in">Decay Index</span><span class="ib ib-paid">Live</span></div>
<div class="ir" onclick="selIdx(this,'property')"><span class="in">Property Development</span><span class="ib ib-paid">Live</span></div>
</div>
</div>
</div>
<div id="bsheet-backdrop" onclick="closeBsheet()"></div>
<div id="bsheet"><div id="bsheet-handle"></div><button id="bsheet-close" onclick="closeBsheet()">&#215;</button><div id="bsheet-content"></div></div>
<div id="bot"><div id="bot-i">
<div id="leg">
<div class="li"><div class="ld" style="background:#22c55e"></div>Low operational risk</div>
<div class="li"><div class="ld" style="background:#eab308"></div>Moderate</div>
<div class="li"><div class="ld" style="background:#f97316"></div>Elevated</div>
<div class="li"><div class="ld" style="background:#ef4444"></div>High risk</div>
<div class="li"><div class="ld" style="background:rgba(245,158,11,0.5);border:1px dashed #f59e0b"></div>Contradiction detected</div>
</div>
<div id="disc">Landvex Assessment — official sources + quiXzoom observations. Free: 30+ day historical data. Live intelligence requires subscription.</div>
</div></div>
<script>
const map=L.map('map',{center:[30,15],zoom:3,zoomControl:true,attributionControl:false,
autoPanPaddingTopLeft:[10,80],
autoPanPaddingBottomRight:[10,80]
});
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',{maxZoom:19,subdomains:'abcd',attribution:''}).addTo(map);
const CITIES=[
// Sweden
{name:'Stockholm',country:'SE',conf:94,a:'green',c:'low',ct:'Strong alignment. Top-performing Nordic capital.',age:'2025-2026',idx:{infra:9.1,commercial:8.4,safety:8.7,climate:7.2},lat:59.334,lng:18.063,r:25000},
{name:'Göteborg',country:'SE',conf:88,a:'green',c:'low',ct:'Second city. Strong industrial and commercial base.',age:'2025-2026',idx:{infra:8.4,commercial:8.1,safety:7.8,climate:6.8},lat:57.706,lng:11.967,r:20000},
{name:'Malmö',country:'SE',conf:79,a:'yellow',c:'medium',ct:'Night safety deviates from official. Port area higher activity.',age:'2025-2026',idx:{infra:7.5,commercial:6.8,safety:6.4,climate:6.1},lat:55.604,lng:13.003,r:18000},
{name:'Uppsala',country:'SE',conf:84,a:'green',c:'low',ct:'University city. Strong R&D cluster.',age:'2025-2026',idx:{infra:8.4,commercial:7.8,safety:8.6,climate:7.1},lat:59.858,lng:17.645,r:15000},
{name:'Linköping',country:'SE',conf:82,a:'green',c:'low',ct:'Tech/defence capital. Saab. Strong alignment.',age:'2025-2026',idx:{infra:8.2,commercial:7.9,safety:8.8,climate:7.0},lat:58.411,lng:15.621,r:15000},
{name:'Luleå',country:'SE',conf:78,a:'green',c:'low',ct:'Data center capital Europe. Green steel transformation.',age:'2025-2026',idx:{infra:7.4,commercial:7.2,safety:9.4,climate:5.2},lat:65.584,lng:22.157,r:14000},
{name:'Umeå',country:'SE',conf:76,a:'green',c:'low',ct:'Fastest growing northern city.',age:'2025-2026',idx:{infra:7.6,commercial:7.1,safety:9.2,climate:5.8},lat:63.826,lng:20.263,r:14000},
{name:'Kiruna',country:'SE',conf:71,a:'orange',c:'high',ct:'CONTRADICTION: City physically moving 3km. Official maps show old location.',age:'2025-2026',idx:{infra:6.8,commercial:6.2,safety:9.6,climate:4.1},lat:67.856,lng:20.225,r:12000},
{name:'Visby',country:'SE',conf:80,a:'green',c:'low',ct:'UNESCO medieval city. Summer peak.',age:'2025-2026',idx:{infra:5.8,commercial:6.8,safety:9.6,climate:7.2},lat:57.638,lng:18.295,r:10000},
// UK
{name:'London',country:'UK',conf:92,a:'green',c:'low',ct:'Global financial centre. Strong alignment.',age:'2025-2026',idx:{infra:9.0,commercial:9.2,safety:8.1,climate:6.8},lat:51.507,lng:-0.127,r:20000},
{name:'Manchester',country:'UK',conf:84,a:'yellow',c:'medium',ct:'Northern powerhouse. Some areas deviation.',age:'2025-2026',idx:{infra:7.8,commercial:8.4,safety:7.2,climate:6.4},lat:53.480,lng:-2.242,r:15000},
// Netherlands
{name:'Amsterdam',country:'NL',conf:88,a:'green',c:'medium',ct:'Overtourism creates livability contradictions.',age:'2025-2026',idx:{infra:8.8,commercial:8.6,safety:8.3,climate:5.9},lat:52.370,lng:4.895,r:15000},
// Germany
{name:'Berlin',country:'DE',conf:86,a:'green',c:'low',ct:'Strong alignment. Tech hub transformation.',age:'2025-2026',idx:{infra:8.4,commercial:8.2,safety:7.8,climate:6.8},lat:52.520,lng:13.404,r:20000},
{name:'Hamburg',country:'DE',conf:84,a:'green',c:'low',ct:'Port city. HafenCity transformation.',age:'2025-2026',idx:{infra:8.6,commercial:8.4,safety:8.2,climate:6.4},lat:53.550,lng:10.000,r:18000},
{name:'Munich',country:'DE',conf:89,a:'green',c:'low',ct:'Premium German city. High alignment.',age:'2025-2026',idx:{infra:9.1,commercial:8.8,safety:8.9,climate:6.6},lat:48.137,lng:11.575,r:18000},
// France
{name:'Paris',country:'FR',conf:87,a:'yellow',c:'medium',ct:'Tourist pressure creates commercial contradictions.',age:'2025-2026',idx:{infra:8.6,commercial:9.1,safety:7.4,climate:6.8},lat:48.856,lng:2.352,r:20000},
// USA — full coverage
{name:'New York',country:'US',conf:89,a:'yellow',c:'medium',ct:'Infrastructure aging vs premium economic activity. Subway maintenance gap significant.',age:'2025-2026',idx:{infra:7.8,commercial:9.1,safety:7.2,climate:6.1},lat:40.712,lng:-74.005,r:28000},
{name:'Houston',country:'US',conf:82,a:'green',c:'low',ct:'Landvex HQ city. Energy capital. Strong commercial alignment.',age:'2025-2026',idx:{infra:7.4,commercial:8.8,safety:7.1,climate:5.8},lat:29.760,lng:-95.369,r:28000},
{name:'Los Angeles',country:'US',conf:80,a:'orange',c:'high',ct:'Homelessness crisis creates visible contradiction vs official livability data.',age:'2025-2026',idx:{infra:6.8,commercial:8.6,safety:6.4,climate:7.2},lat:34.052,lng:-118.243,r:28000},
{name:'Chicago',country:'US',conf:82,a:'yellow',c:'medium',ct:'Neighborhood inequality significant. Loop vs South Side divergence.',age:'2025-2026',idx:{infra:7.4,commercial:8.4,safety:6.2,climate:5.8},lat:41.878,lng:-87.629,r:26000},
{name:'Dallas',country:'US',conf:80,a:'yellow',c:'medium',ct:'Rapid growth outpacing infrastructure. Sprawl contradiction.',age:'2025-2026',idx:{infra:7.1,commercial:8.6,safety:7.1,climate:6.2},lat:32.779,lng:-96.808,r:26000},
{name:'Phoenix',country:'US',conf:76,a:'yellow',c:'medium',ct:'Climate risk underrepresented. Water scarcity growing concern.',age:'2025-2026',idx:{infra:7.2,commercial:7.8,safety:7.4,climate:4.2},lat:33.448,lng:-112.073,r:28000},
{name:'Philadelphia',country:'US',conf:78,a:'orange',c:'high',ct:'Infrastructure decay significant. Kensington area extreme contradiction.',age:'2025-2026',idx:{infra:6.4,commercial:7.6,safety:5.8,climate:6.4},lat:39.952,lng:-75.165,r:22000},
{name:'San Antonio',country:'US',conf:77,a:'green',c:'low',ct:'Military-driven economy. Stable alignment.',age:'2025-2026',idx:{infra:7.1,commercial:7.4,safety:7.6,climate:6.1},lat:29.424,lng:-98.493,r:24000},
{name:'San Diego',country:'US',conf:82,a:'green',c:'low',ct:'Military and biotech hub. Strong alignment.',age:'2025-2026',idx:{infra:8.1,commercial:8.2,safety:7.8,climate:9.2},lat:32.715,lng:-117.161,r:22000},
{name:'San Jose',country:'US',conf:84,a:'green',c:'low',ct:'Silicon Valley core. High confidence tech hub.',age:'2025-2026',idx:{infra:8.4,commercial:9.4,safety:7.6,climate:8.6},lat:37.338,lng:-121.886,r:20000},
{name:'San Francisco',country:'US',conf:80,a:'orange',c:'high',ct:'Homelessness and crime contradiction vs premium tech economy. Visible divergence.',age:'2025-2026',idx:{infra:7.8,commercial:9.2,safety:6.1,climate:8.4},lat:37.774,lng:-122.419,r:16000},
{name:'Austin',country:'US',conf:82,a:'green',c:'low',ct:'Tech migration boom. Infrastructure strain but strong growth.',age:'2025-2026',idx:{infra:7.4,commercial:8.8,safety:7.8,climate:6.8},lat:30.267,lng:-97.743,r:22000},
{name:'Seattle',country:'US',conf:82,a:'yellow',c:'medium',ct:'Amazon HQ2 effect. Some homeless contradiction.',age:'2025-2026',idx:{infra:8.1,commercial:8.8,safety:7.1,climate:7.4},lat:47.606,lng:-122.332,r:20000},
{name:'Denver',country:'US',conf:80,a:'yellow',c:'medium',ct:'Rapid growth. Climate and water risk growing.',age:'2025-2026',idx:{infra:7.6,commercial:8.2,safety:7.4,climate:6.4},lat:39.739,lng:-104.984,r:20000},
{name:'Washington DC',country:'US',conf:84,a:'yellow',c:'medium',ct:'Federal capital. Some neighbourhood inequality.',age:'2025-2026',idx:{infra:8.4,commercial:8.8,safety:7.2,climate:6.8},lat:38.907,lng:-77.036,r:18000},
{name:'Boston',country:'US',conf:86,a:'green',c:'low',ct:'Education and biotech hub. Strong alignment.',age:'2025-2026',idx:{infra:8.2,commercial:8.6,safety:7.8,climate:6.4},lat:42.360,lng:-71.058,r:16000},
{name:'Atlanta',country:'US',conf:78,a:'yellow',c:'medium',ct:'Major logistics hub. Inequality between zones.',age:'2025-2026',idx:{infra:7.4,commercial:8.4,safety:6.8,climate:6.8},lat:33.748,lng:-84.387,r:22000},
{name:'Miami',country:'US',conf:80,a:'yellow',c:'medium',ct:'Latin American gateway. Climate risk underclassified.',age:'2025-2026',idx:{infra:7.6,commercial:8.6,safety:7.1,climate:5.4},lat:25.761,lng:-80.191,r:18000},
{name:'Las Vegas',country:'US',conf:74,a:'yellow',c:'medium',ct:'Tourism economy. Water scarcity growing.',age:'2025-2026',idx:{infra:7.2,commercial:8.4,safety:6.8,climate:4.1},lat:36.174,lng:-115.136,r:20000},
{name:'Minneapolis',country:'US',conf:80,a:'yellow',c:'medium',ct:'Some neighbourhood inequality. Winter resilience strong.',age:'2025-2026',idx:{infra:7.8,commercial:7.8,safety:7.1,climate:5.8},lat:44.977,lng:-93.265,r:18000},
{name:'Portland',country:'US',conf:74,a:'orange',c:'high',ct:'Homelessness contradiction significant. Official data optimistic.',age:'2025-2026',idx:{infra:7.4,commercial:7.6,safety:5.8,climate:7.8},lat:45.523,lng:-122.676,r:16000},
{name:'Nashville',country:'US',conf:78,a:'green',c:'low',ct:'Music city boom. Strong commercial growth.',age:'2025-2026',idx:{infra:7.2,commercial:8.2,safety:7.4,climate:7.1},lat:36.162,lng:-86.781,r:18000},
{name:'Charlotte',country:'US',conf:78,a:'green',c:'low',ct:'Financial hub. Consistent growth trajectory.',age:'2025-2026',idx:{infra:7.4,commercial:8.1,safety:7.6,climate:7.2},lat:35.227,lng:-80.843,r:18000},
{name:'Detroit',country:'US',conf:68,a:'orange',c:'high',ct:'Post-industrial contradiction. Vacancy and decay significant.',age:'2025-2026',idx:{infra:5.8,commercial:6.4,safety:5.2,climate:5.8},lat:42.331,lng:-83.045,r:18000},
{name:'New Orleans',country:'US',conf:68,a:'orange',c:'high',ct:'Climate risk extreme. Infrastructure vulnerability high.',age:'2025-2026',idx:{infra:5.4,commercial:7.2,safety:5.4,climate:3.8},lat:29.951,lng:-90.071,r:14000},
// UAE
{name:'Dubai',country:'UAE',conf:83,a:'green',c:'low',ct:'World-class infrastructure. Strong alignment.',age:'2025-2026',idx:{infra:9.4,commercial:9.1,safety:8.8,climate:4.2},lat:25.197,lng:55.274,r:18000},
{name:'Abu Dhabi',country:'UAE',conf:81,a:'green',c:'low',ct:'Capital region. Premium development.',age:'2025-2026',idx:{infra:9.2,commercial:8.6,safety:9.1,climate:4.0},lat:24.453,lng:54.377,r:18000},
// Singapore
{name:'Singapore',country:'SG',conf:91,a:'green',c:'low',ct:'Near-perfect city-state alignment.',age:'2025-2026',idx:{infra:9.6,commercial:9.3,safety:9.4,climate:5.8},lat:1.352,lng:103.820,r:12000},
// Thailand
{name:'Bangkok',country:'TH',conf:71,a:'orange',c:'high',ct:'Field verified: cable disorder, infrastructure decay. Contradiction HIGH.',age:'2025-2026',idx:{infra:6.2,commercial:7.8,safety:5.9,climate:3.8},lat:13.756,lng:100.502,r:20000},
// Japan
{name:'Tokyo',country:'JP',conf:92,a:'green',c:'low',ct:'World-class maintenance culture. Perfect alignment.',age:'2025-2026',idx:{infra:9.9,commercial:9.4,safety:9.7,climate:6.2},lat:35.689,lng:139.692,r:25000},
// China
{name:'Shanghai',country:'CN',conf:74,a:'yellow',c:'medium',ct:'Rapid development creates some data lag.',age:'2025-2026',idx:{infra:8.8,commercial:9.2,safety:7.8,climate:5.4},lat:31.230,lng:121.474,r:25000},
// India
{name:'Mumbai',country:'IN',conf:68,a:'orange',c:'high',ct:'Infrastructure significantly below official classification in outer zones.',age:'2025-2026',idx:{infra:5.8,commercial:7.8,safety:6.4,climate:4.8},lat:19.076,lng:72.877,r:20000},
// Brazil
{name:'São Paulo',country:'BR',conf:68,a:'orange',c:'high',ct:'Safety underrepresented. Infrastructure gap outer zones.',age:'2025-2026',idx:{infra:5.8,commercial:7.2,safety:4.1,climate:5.5},lat:-23.550,lng:-46.633,r:25000},
{name:'Rio de Janeiro',country:'BR',conf:64,a:'orange',c:'high',ct:'Tourism vs safety contradiction significant.',age:'2025-2026',idx:{infra:5.2,commercial:6.8,safety:4.4,climate:6.1},lat:-22.906,lng:-43.172,r:20000},
// Africa
{name:'Lagos',country:'NG',conf:44,a:'red',c:'high',ct:'Low confidence. Major deviation infrastructure vs official.',age:'2025-2026',idx:{infra:3.8,commercial:6.1,safety:3.2,climate:4.1},lat:6.524,lng:3.379,r:20000},
{name:'Nairobi',country:'KE',conf:52,a:'orange',c:'high',ct:'Infrastructure gaps significant. Rapid urbanisation outpaces data.',age:'2025-2026',idx:{infra:4.8,commercial:6.4,safety:4.8,climate:5.6},lat:-1.286,lng:36.817,r:15000},
// Australia
{name:'Sydney',country:'AU',conf:87,a:'green',c:'low',ct:'High alignment. Premium Pacific city.',age:'2025-2026',idx:{infra:8.8,commercial:8.6,safety:8.6,climate:7.8},lat:-33.868,lng:151.209,r:20000},
// Canada
{name:'Toronto',country:'CA',conf:86,a:'green',c:'low',ct:'Strong alignment. Diverse economic base.',age:'2025-2026',idx:{infra:8.4,commercial:8.8,safety:8.2,climate:6.2},lat:43.653,lng:-79.383,r:18000},
// South Africa
{name:'Cape Town',country:'ZA',conf:58,a:'orange',c:'high',ct:'Extreme inequality visible. Tourist vs local reality gap.',age:'2025-2026',idx:{infra:5.4,commercial:6.8,safety:4.2,climate:7.4},lat:-33.924,lng:18.424,r:15000},
// Morocco
{name:'Casablanca',country:'MA',conf:62,a:'orange',c:'high',ct:'Rapid informal urbanisation creates significant contradiction between official infrastructure data and observed reality.',age:'2025-2026',idx:{infra:5.8,commercial:7.4,safety:6.1,climate:6.8},lat:33.589,lng:-7.603,r:18000},
{name:'Marrakech',country:'MA',conf:68,a:'yellow',c:'medium',ct:'Tourism economy creates dual reality. Medina vs modern city diverge significantly.',age:'2025-2026',idx:{infra:5.4,commercial:7.8,safety:6.8,climate:7.1},lat:31.628,lng:-7.987,r:14000},
{name:'Rabat',country:'MA',conf:65,a:'yellow',c:'medium',ct:'Capital. Official data more reliable than other Moroccan cities.',age:'2025-2026',idx:{infra:6.2,commercial:6.4,safety:7.2,climate:7.0},lat:34.020,lng:-6.841,r:13000},
// China
{name:'Beijing',country:'CN',conf:72,a:'yellow',c:'medium',ct:'Official data well-maintained but selective. Some commercial contradiction.',age:'2025-2026',idx:{infra:9.1,commercial:8.8,safety:8.4,climate:4.8},lat:39.904,lng:116.407,r:30000},
{name:'Shenzhen',country:'CN',conf:76,a:'green',c:'low',ct:'Tech megacity. Fastest development pace globally. Strong alignment.',age:'2025-2026',idx:{infra:9.2,commercial:9.4,safety:8.6,climate:5.8},lat:22.543,lng:114.057,r:22000},
{name:'Guangzhou',country:'CN',conf:71,a:'yellow',c:'medium',ct:'Pearl River Delta hub. Manufacturing transition to services.',age:'2025-2026',idx:{infra:8.8,commercial:8.6,safety:7.8,climate:5.4},lat:23.129,lng:113.264,r:22000},
{name:'Chengdu',country:'CN',conf:68,a:'yellow',c:'medium',ct:'Western China hub. Rapid growth outpacing official classification.',age:'2025-2026',idx:{infra:7.8,commercial:7.6,safety:7.9,climate:5.6},lat:30.572,lng:104.066,r:20000},
// Spain
{name:'Madrid',country:'ES',conf:84,a:'green',c:'low',ct:'Capital. Strong alignment. Premium Southern European market.',age:'2025-2026',idx:{infra:8.6,commercial:8.8,safety:8.2,climate:7.4},lat:40.416,lng:-3.703,r:20000},
{name:'Barcelona',country:'ES',conf:82,a:'yellow',c:'medium',ct:'Overtourism contradiction. Commercial activity vs livability diverging.',age:'2025-2026',idx:{infra:8.4,commercial:8.6,safety:7.4,climate:8.1},lat:41.385,lng:2.173,r:16000},
{name:'Valencia',country:'ES',conf:78,a:'green',c:'low',ct:'Growing tech hub. Strong alignment. Emerging market.',age:'2025-2026',idx:{infra:7.8,commercial:7.6,safety:8.4,climate:8.4},lat:39.470,lng:-0.376,r:14000},
{name:'Seville',country:'ES',conf:76,a:'green',c:'low',ct:'Tourism and industry balance. Strong historical alignment.',age:'2025-2026',idx:{infra:7.4,commercial:7.2,safety:8.2,climate:7.8},lat:37.389,lng:-5.984,r:13000},
{name:'Málaga',country:'ES',conf:74,a:'green',c:'low',ct:'Costa del Sol boom. Tech hub emerging. Tourism drives commercial.',age:'2025-2026',idx:{infra:7.6,commercial:7.8,safety:8.6,climate:9.1},lat:36.721,lng:-4.421,r:12000},
// Italy
{name:'Milan',country:'IT',conf:83,a:'green',c:'low',ct:'Fashion and finance capital. Strong commercial alignment.',age:'2025-2026',idx:{infra:8.4,commercial:9.1,safety:7.8,climate:7.2},lat:45.464,lng:9.189,r:18000},
{name:'Rome',country:'IT',conf:78,a:'yellow',c:'medium',ct:'Tourism pressure. Infrastructure aging vs heritage premium.',age:'2025-2026',idx:{infra:6.8,commercial:8.2,safety:7.2,climate:8.1},lat:41.902,lng:12.496,r:20000},
{name:'Naples',country:'IT',conf:64,a:'orange',c:'high',ct:'Significant informal economy not reflected in official data. Infrastructure contradiction high.',age:'2025-2026',idx:{infra:5.2,commercial:6.8,safety:5.4,climate:8.4},lat:40.853,lng:14.268,r:16000},
// Poland
{name:'Warsaw',country:'PL',conf:80,a:'green',c:'low',ct:'Fastest growing EU capital. Strong investment environment.',age:'2025-2026',idx:{infra:8.2,commercial:8.4,safety:8.4,climate:6.4},lat:52.229,lng:21.012,r:18000},
{name:'Krakow',country:'PL',conf:76,a:'green',c:'low',ct:'Tourism and tech hub. Strong alignment.',age:'2025-2026',idx:{infra:7.8,commercial:7.6,safety:8.6,climate:6.8},lat:50.061,lng:19.937,r:14000},
// Portugal
{name:'Lisbon',country:'PT',conf:78,a:'yellow',c:'medium',ct:'Tourism boom creating affordability contradiction.',age:'2025-2026',idx:{infra:7.6,commercial:8.2,safety:8.2,climate:9.2},lat:38.717,lng:-9.139,r:14000},
{name:'Porto',country:'PT',conf:76,a:'green',c:'low',ct:'Gentrification active. Tech and tourism growing.',age:'2025-2026',idx:{infra:7.4,commercial:7.8,safety:8.4,climate:8.8},lat:41.157,lng:-8.629,r:12000},
// Greece
{name:'Athens',country:'GR',conf:70,a:'yellow',c:'medium',ct:'Recovery from crisis. Infrastructure gap vs tourism premium.',age:'2025-2026',idx:{infra:6.4,commercial:7.2,safety:7.4,climate:8.6},lat:37.983,lng:23.727,r:16000},
// Turkey
{name:'Istanbul',country:'TR',conf:68,a:'orange',c:'high',ct:'Rapid informal development. Official data significantly lags observed reality.',age:'2025-2026',idx:{infra:6.8,commercial:8.4,safety:6.4,climate:7.2},lat:41.015,lng:28.979,r:22000},
// Saudi Arabia
{name:'Riyadh',country:'SA',conf:74,a:'green',c:'low',ct:'Vision 2030 transformation. Rapid premium development.',age:'2025-2026',idx:{infra:8.8,commercial:8.6,safety:8.4,climate:3.8},lat:24.688,lng:46.721,r:22000},
{name:'NEOM',country:'SA',conf:52,a:'yellow',c:'medium',ct:'Megaproject. Official projections vs observed construction pace diverge.',age:'2025-2026',idx:{infra:7.2,commercial:4.8,safety:8.8,climate:4.2},lat:28.150,lng:35.050,r:25000},
// India
{name:'Delhi',country:'IN',conf:62,a:'orange',c:'high',ct:'Air quality and infrastructure contradiction extremely high.',age:'2025-2026',idx:{infra:5.4,commercial:7.8,safety:5.8,climate:2.8},lat:28.613,lng:77.209,r:28000},
{name:'Bangalore',country:'IN',conf:68,a:'orange',c:'high',ct:'Tech hub but infrastructure severely lagging. Traffic contradiction extreme.',age:'2025-2026',idx:{infra:4.8,commercial:8.6,safety:7.1,climate:7.4},lat:12.971,lng:77.594,r:20000},
{name:'Hyderabad',country:'IN',conf:66,a:'yellow',c:'medium',ct:'HITEC City tech boom. Infrastructure improving rapidly.',age:'2025-2026',idx:{infra:6.2,commercial:8.1,safety:7.4,climate:6.8},lat:17.385,lng:78.486,r:18000},
// South Korea
{name:'Seoul',country:'KR',conf:88,a:'green',c:'low',ct:'World-class infrastructure. Near-perfect data alignment.',age:'2025-2026',idx:{infra:9.4,commercial:9.1,safety:9.2,climate:6.8},lat:37.566,lng:126.977,r:22000},
{name:'Busan',country:'KR',conf:82,a:'green',c:'low',ct:'Port city transformation. Strong tech and tourism.',age:'2025-2026',idx:{infra:8.6,commercial:7.8,safety:8.8,climate:7.2},lat:35.179,lng:129.075,r:16000},
// Indonesia
{name:'Jakarta',country:'ID',conf:58,a:'orange',c:'high',ct:'Sinking city. Infrastructure vs population density extreme contradiction.',age:'2025-2026',idx:{infra:5.2,commercial:7.4,safety:6.1,climate:3.4},lat:-6.208,lng:106.845,r:25000},
// Vietnam
{name:'Ho Chi Minh City',country:'VN',conf:62,a:'orange',c:'high',ct:'Rapid informal development. Official data significantly lags.',age:'2025-2026',idx:{infra:5.8,commercial:8.2,safety:7.1,climate:4.8},lat:10.762,lng:106.660,r:20000},
{name:'Hanoi',country:'VN',conf:60,a:'yellow',c:'medium',ct:'Capital. More stable but development pressure high.',age:'2025-2026',idx:{infra:6.2,commercial:7.6,safety:7.4,climate:5.4},lat:21.027,lng:105.834,r:16000},
// Mexico
{name:'Mexico City',country:'MX',conf:64,a:'orange',c:'high',ct:'Inequality extreme. Infrastructure aging in outer zones.',age:'2025-2026',idx:{infra:5.8,commercial:7.8,safety:5.4,climate:5.8},lat:19.432,lng:-99.133,r:28000},
{name:'Monterrey',country:'MX',conf:68,a:'yellow',c:'medium',ct:'Industrial hub. US nearshoring boom.',age:'2025-2026',idx:{infra:7.1,commercial:8.2,safety:6.4,climate:6.1},lat:25.686,lng:-100.316,r:18000},
// Colombia
{name:'Bogotá',country:'CO',conf:60,a:'orange',c:'high',ct:'Altitude, inequality and infrastructure gaps significant.',age:'2025-2026',idx:{infra:5.6,commercial:7.2,safety:5.2,climate:6.8},lat:4.711,lng:-74.072,r:22000},
{name:'Medellín',country:'CO',conf:62,a:'yellow',c:'medium',ct:'Remarkable transformation. Innovation city from crisis.',age:'2025-2026',idx:{infra:6.8,commercial:7.4,safety:6.8,climate:8.4},lat:6.244,lng:-75.574,r:16000},
// Argentina
{name:'Buenos Aires',country:'AR',conf:62,a:'orange',c:'high',ct:'Economic crisis visible in infrastructure decay. Official vs observed diverge.',age:'2025-2026',idx:{infra:5.4,commercial:7.1,safety:6.1,climate:7.4},lat:-34.603,lng:-58.381,r:22000},
// Chile
{name:'Santiago',country:'CL',conf:70,a:'yellow',c:'medium',ct:'Most developed Latin American capital. Some inequality.',age:'2025-2026',idx:{infra:7.4,commercial:7.8,safety:7.1,climate:7.6},lat:-33.456,lng:-70.648,r:20000},
// Egypt
{name:'Cairo',country:'EG',conf:54,a:'orange',c:'high',ct:'Mega-city pressure. New Cairo vs old city extreme contradiction.',age:'2025-2026',idx:{infra:5.1,commercial:6.8,safety:6.2,climate:5.8},lat:30.044,lng:31.235,r:26000},
// Ethiopia
{name:'Addis Ababa',country:'ET',conf:46,a:'red',c:'high',ct:'Rapid development but data very limited. Low confidence.',age:'2025-2026',idx:{infra:4.4,commercial:5.8,safety:5.4,climate:6.2},lat:9.025,lng:38.747,r:18000},
// Ghana
{name:'Accra',country:'GH',conf:52,a:'orange',c:'high',ct:'West Africa hub. Infrastructure significantly below classification.',age:'2025-2026',idx:{infra:4.8,commercial:6.4,safety:6.1,climate:5.8},lat:5.603,lng:-0.187,r:16000},
// New Zealand
{name:'Auckland',country:'NZ',conf:82,a:'green',c:'low',ct:'Pacific hub. Strong alignment.',age:'2025-2026',idx:{infra:8.2,commercial:7.8,safety:8.8,climate:8.4},lat:-36.848,lng:174.763,r:16000},
// Philippines
{name:'Manila',country:'PH',conf:52,a:'orange',c:'high',ct:'Infrastructure severely below official classification. Traffic extreme.',age:'2025-2026',idx:{infra:4.4,commercial:6.8,safety:5.4,climate:4.4},lat:14.599,lng:120.984,r:18000},
// Malaysia
{name:'Kuala Lumpur',country:'MY',conf:72,a:'yellow',c:'medium',ct:'Rapid development. Some informal areas create contradiction.',age:'2025-2026',idx:{infra:7.8,commercial:8.1,safety:7.6,climate:5.8},lat:3.139,lng:101.687,r:16000},
];
const AS={green:{color:'#22c55e',fo:0.10,w:1.5,d:null},yellow:{color:'#eab308',fo:0.13,w:1.5,d:'6 4'},orange:{color:'#f97316',fo:0.18,w:2,d:'5 4'},red:{color:'#ef4444',fo:0.22,w:2,d:'4 3'}};
const CL={low:'Contradiction Index: Low',medium:'Contradiction Index: Medium',high:'Contradiction Index: HIGH <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'};
// Practical Intelligence — loaded from API
let PRACTICAL = {};
let PRACTICAL_META = {};
async function loadPracticalData() {
try {
const r = await fetch('/data/practical.json?v=' + Date.now());
const d = await r.json();
PRACTICAL = {};
for (const [city, data] of Object.entries(d.cities)) {
PRACTICAL[city] = data;
}
PRACTICAL_META = d.meta;
console.log('[Landvex] Practical data loaded:', Object.keys(PRACTICAL).length, 'cities, generated:', d.meta.generated);
} catch(e) {
console.warn('[Landvex] Practical data unavailable, using cached:', e.message);
}
}
function getPractical(cityName) {
return PRACTICAL[cityName] || null;
}
function foodPopup(p) {
const f = p.food;
const l = p.lcs;
if (!f) return '';
const upfCol = f.ultra_processed_pct < 30 ? '#16a34a' :
f.ultra_processed_pct < 45 ? '#ca8a04' : '#dc2626';
const obeseCol = f.obesity_pct < 12 ? '#16a34a' :
f.obesity_pct < 22 ? '#ca8a04' : '#dc2626';
const foodIncCol = f.food_income_pct < 15 ? '#16a34a' :
f.food_income_pct < 28 ? '#ca8a04' : '#dc2626';
const scoreCol = f.food_environment_score >= 75 ? '#16a34a' :
f.food_environment_score >= 55 ? '#ca8a04' : '#dc2626';
const heeScore = l && l.hee_score ? l.hee_score : null;
return `<div style="border-top:1px solid rgba(0,0,0,0.08);margin-top:10px;padding-top:10px">
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.4);margin-bottom:8px">FOOD ENVIRONMENT & HUMAN ENERGY</div>
${heeScore ? `<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:10px 12px;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between">
<div>
<div style="font-size:9px;color:rgba(28,28,30,0.45);margin-bottom:2px">Human Energy Efficiency</div>
<div style="font-size:9px;color:rgba(28,28,30,0.35)">LCS adjusted for food environment</div>
</div>
<div style="font-size:32px;font-weight:900;color:${heeScore>=78?'#16a34a':heeScore>=62?'#ca8a04':'#dc2626'};font-family:monospace">${heeScore}</div>
</div>` : ''}
<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:8px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:2px">Food env. score</div>
<div style="font-size:15px;font-weight:800;color:${scoreCol}">${f.food_environment_score}/100</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:2px">Ultra-processed</div>
<div style="font-size:15px;font-weight:800;color:${upfCol}">${f.ultra_processed_pct}%</div>
<div style="font-size:8px;color:rgba(28,28,30,0.35)">of calories</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:2px">Obesity rate</div>
<div style="font-size:15px;font-weight:800;color:${obeseCol}">${f.obesity_pct}%</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:2px">Food/income</div>
<div style="font-size:15px;font-weight:800;color:${foodIncCol}">${f.food_income_pct}%</div>
</div>
</div>
${f.contradiction ? `<div style="background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.2);border-radius: var(--radius-sm);padding:7px 9px;font-size:10px;color:#92400e;line-height:1.4;margin-bottom:6px"><span style="font-weight:800">Food contradiction: </span>${f.contradiction}</div>` : ''}
<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.6);line-height:1.5;margin-bottom:5px">${f.note}</div>
<div style="font-size:9px;color:rgba(28,28,30,0.3)">Source: ${f.source||'WHO / NOVA 2024'}</div>
</div>`;
}
function lcsPopup(p) {
const l = p.lcs;
if (!l) return '';
const scoreCol = l.lcs_score >= 80 ? '#16a34a' : l.lcs_score >= 65 ? '#ca8a04' :
l.lcs_score >= 50 ? '#f97316' : '#dc2626';
const dimBar = (val, label, col) => `
<div style="display:flex;align-items:center;gap:6px;margin-bottom:5px">
<span style="width:72px;font-size:10px;color:rgba(28,28,30,0.55);flex-shrink:0">${label}</span>
<div style="flex:1;height:6px;background:rgba(0,0,0,0.08);border-radius: 3px">
<div style="width:${val}%;height:100%;background:${col};border-radius: 3px"></div>
</div>
<span style="font-size:11px;font-weight:800;color:${col};width:28px;text-align:right">${val}</span>
</div>`;
const statRow = (label, val, unit='', col='#1c1c1e') =>
`<div style="display:flex;justify-content:space-between;font-size:10px;padding:3px 0;border-bottom:1px solid rgba(0,0,0,0.04)">
<span style="color:rgba(28,28,30,0.55)">${label}</span>
<span style="font-weight:700;color:${col}">${val}${unit}</span>
</div>`;
const fertCol = l.fertility_rate < 1.2 ? '#dc2626' : l.fertility_rate < 1.6 ? '#f97316' : '#16a34a';
const airCol = l.air_quality_aqi < 30 ? '#16a34a' : l.air_quality_aqi < 60 ? '#ca8a04' : '#dc2626';
const avoidCol = l.avoidable_deaths_per100k < 150 ? '#16a34a' : l.avoidable_deaths_per100k < 300 ? '#ca8a04' : '#dc2626';
return `<div style="border-top:1px solid rgba(0,0,0,0.08);margin-top:10px;padding-top:10px">
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.4);margin-bottom:8px">LANDVEX CIVILIZATIONAL SCORE</div>
<!-- Main score -->
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:12px;margin-bottom:8px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<div>
<div style="font-size:36px;font-weight:900;color:${scoreCol};line-height:1;font-family:monospace">${l.lcs_score}</div>
<div style="font-size:9px;color:rgba(28,28,30,0.4)">out of 100</div>
</div>
<div style="text-align:right;max-width:160px">
<div style="font-size:10px;color:rgba(28,28,30,0.55);line-height:1.4;font-style:italic">${l.highlight}</div>
</div>
</div>
${dimBar(l.vitality, 'Vitality', '#16a34a')}
${dimBar(l.prosperity, 'Prosperity', '#3b82f6')}
${dimBar(l.friction, 'Low friction', '#a855f7')}
</div>
<!-- Vitality stats -->
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:8px 10px;margin-bottom:6px">
<div style="font-size:9px;font-weight:700;color:rgba(28,28,30,0.4);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:5px">Vitality</div>
${statRow('Life expectancy', l.life_expectancy, ' yrs')}
${statRow('Healthy years', l.healthy_years, ' yrs')}
${statRow('Avoidable deaths', l.avoidable_deaths_per100k, '/100k', avoidCol)}
${statRow('Infant mortality', l.infant_mortality_per1000, '/1000')}
${statRow('Dental health', l.dental_health_score + '/10')}
${statRow('Air quality (AQI)', l.air_quality_aqi, '', airCol)}
</div>
<!-- Demography -->
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:8px 10px;margin-bottom:6px">
<div style="font-size:9px;font-weight:700;color:rgba(28,28,30,0.4);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:5px">Demographics</div>
${statRow('Fertility rate', l.fertility_rate, ' children/woman', fertCol)}
${statRow('Median age', l.median_age, ' yrs')}
${l.fertility_rate < 1.4 ? `<div style="background:rgba(220,38,38,0.08);border-radius: var(--radius-sm);padding:4px 7px;font-size:9px;color:#991b1b;font-weight:600;margin-top:4px">Demographic warning: below replacement level</div>` : ''}
</div>
<!-- Society -->
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:8px 10px;margin-bottom:6px">
<div style="font-size:9px;font-weight:700;color:rgba(28,28,30,0.4);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:5px">Society</div>
${statRow('Homicide rate', l.homicide_per100k, '/100k', l.homicide_per100k < 1 ? '#16a34a' : l.homicide_per100k < 3 ? '#ca8a04' : '#dc2626')}
${statRow('Prison rate', l.prison_per100k, '/100k')}
${statRow('Press freedom', l.press_freedom + '/100')}
${statRow('Social mobility', l.social_mobility + '/100')}
${statRow('Avg commute', l.commute_min, ' min', l.commute_min < 35 ? '#16a34a' : l.commute_min < 55 ? '#ca8a04' : '#dc2626')}
${statRow('Housing/income ratio', l.housing_income_ratio + 'x', '', l.housing_income_ratio < 10 ? '#16a34a' : l.housing_income_ratio < 15 ? '#ca8a04' : '#dc2626')}
</div>
${l.contradiction_note ? `<div style="background:rgba(245,158,11,0.07);border-left:3px solid #f59e0b;border-radius: 0 7px 7px 0;padding:8px 10px;margin-bottom:6px;font-size:10px;color:rgba(28,28,30,0.65);line-height:1.5"><span style="font-weight:800;color:#92400e">Landvex Contradiction: </span>${l.contradiction_note}</div>` : ''}
<div style="font-size:9px;color:rgba(28,28,30,0.3)">Source: ${l.source||'WHO / OECD / World Bank 2024'}</div>
</div>`;
}
function frictionPopup(p) {
const f = p.friction;
if (!f) return '';
const score = f.lfs_score || 0;
const col = score <= 20 ? '#16a34a' : score <= 40 ? '#4ade80' :
score <= 55 ? '#ca8a04' : score <= 70 ? '#f97316' : '#dc2626';
const grade_col = score <= 20 ? '#166534' : score <= 40 ? '#15803d' :
score <= 55 ? '#92400e' : score <= 70 ? '#c2410c' : '#991b1b';
const pct = Math.round((100-score));
const co = f.components || {};
const row = (label, val, unit='') => val !== undefined ?
`<div style="display:flex;justify-content:space-between;font-size:10px;padding:3px 0;border-bottom:1px solid rgba(0,0,0,0.04)">
<span style="color:rgba(28,28,30,0.55)">${label}</span>
<span style="font-weight:700;color:#0A0A0A">${val}${unit}</span>
</div>` : '';
const tagList = (items, col) => items && items.length ?
items.map(t=>`<span style="background:${col}15;color:${col};border-radius: var(--radius-sm);padding:2px 6px;font-size:9px;font-weight:600">${t}</span>`).join(' ') : '';
return `<div style="border-top:1px solid rgba(0,0,0,0.08);margin-top:10px;padding-top:10px">
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.4);margin-bottom:8px">LANDVEX FRICTION SCORE</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:12px;margin-bottom:8px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
<div>
<div style="font-size:28px;font-weight:900;color:${col};line-height:1;font-family:monospace">${score}</div>
<div style="font-size:10px;color:rgba(28,28,30,0.45);margin-top:2px">out of 100</div>
</div>
<div style="text-align:right">
<div style="font-size:13px;font-weight:800;color:${grade_col}">${f.lfs_grade}</div>
<div style="font-size:9px;color:rgba(28,28,30,0.4);margin-top:2px">${pct}% efficiency vs max friction</div>
</div>
</div>
<div style="height:8px;background:rgba(0,0,0,0.08);border-radius: var(--radius-sm);overflow:hidden">
<div style="width:${score}%;height:100%;background:${col};border-radius: 4px"></div>
</div>
<div style="display:flex;justify-content:space-between;font-size:8px;color:rgba(28,28,30,0.3);margin-top:3px">
<span>0 — No friction</span><span>100 — Extreme</span>
</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:8px 10px;margin-bottom:6px">
<div style="font-size:9px;font-weight:700;color:rgba(28,28,30,0.4);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px">Key metrics</div>
${row('Company start', co.company_start_days, ' days')}
${row('Tax hours/year', co.tax_hours_per_year, ' hrs')}
${row('Construction permit', co.construction_permit_days, ' days')}
${row('Digital govt score', co.digital_govt_score, '/100')}
${row('Court resolution', co.legal_resolution_days, ' days')}
</div>
${f.strengths ? `<div style="margin-bottom:5px"><div style="font-size:9px;color:rgba(28,28,30,0.4);margin-bottom:4px;font-weight:600">STRENGTHS</div><div style="display:flex;flex-wrap:wrap;gap:4px">${tagList(f.strengths,'#16a34a')}</div></div>` : ''}
${f.weaknesses ? `<div style="margin-bottom:8px"><div style="font-size:9px;color:rgba(28,28,30,0.4);margin-bottom:4px;font-weight:600">FRICTION POINTS</div><div style="display:flex;flex-wrap:wrap;gap:4px">${tagList(f.weaknesses,'#dc2626')}</div></div>` : ''}
${f.entry_friction !== undefined ? `
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;margin-top:8px;margin-bottom:6px">
<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:6px 8px;text-align:center">
<div style="font-size:8px;color:rgba(28,28,30,0.45);margin-bottom:2px">Entry</div>
<div style="font-size:14px;font-weight:800;color:${f.entry_friction<=20?'#16a34a':f.entry_friction<=40?'#ca8a04':'#dc2626'}">${f.entry_friction}</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:6px 8px;text-align:center">
<div style="font-size:8px;color:rgba(28,28,30,0.45);margin-bottom:2px">Operating</div>
<div style="font-size:14px;font-weight:800;color:${f.operating_friction<=25?'#16a34a':f.operating_friction<=50?'#ca8a04':'#dc2626'}">${f.operating_friction}</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:6px 8px;text-align:center">
<div style="font-size:8px;color:rgba(28,28,30,0.45);margin-bottom:2px">Scale adv.</div>
<div style="font-size:14px;font-weight:800;color:${f.scale_advantage<=30?'#16a34a':f.scale_advantage<=55?'#ca8a04':'#dc2626'}">${f.scale_advantage}</div>
</div>
</div>` : ''}
${f.sme_burden_pct !== undefined ? `
<div style="background:rgba(220,38,38,0.06);border-radius: var(--radius-sm);padding:8px 10px;margin-bottom:6px;font-size:10px">
<div style="font-weight:700;color:#991b1b;margin-bottom:4px">Compliance cost as % of turnover</div>
<div style="display:flex;justify-content:space-between">
<div><span style="color:rgba(28,28,30,0.5)">Micro-business</span><br><span style="font-size:16px;font-weight:900;color:#dc2626">${f.sme_burden_pct}%</span></div>
<div style="text-align:center;padding:0 8px;color:rgba(28,28,30,0.2);font-size:18px">vs</div>
<div style="text-align:right"><span style="color:rgba(28,28,30,0.5)">Large corp</span><br><span style="font-size:16px;font-weight:900;color:#16a34a">${f.large_co_burden_pct}%</span></div>
</div>
<div style="height:4px;background:rgba(0,0,0,0.06);border-radius: var(--radius-sm);margin-top:6px;overflow:hidden">
<div style="width:${Math.min(f.sme_burden_pct/30*100,100)}%;height:100%;background:#dc2626;border-radius: 2px"></div>
</div>
</div>` : ''}
${f.glasskiosk_note ? `<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.6);line-height:1.5;margin-bottom:6px"><span style="font-weight:700">Example: </span>${f.glasskiosk_note}</div>` : ''}
${f.elasticity ? `<div style="font-size:10px;color:rgba(28,28,30,0.55);margin-bottom:6px"><span style="font-weight:700">Regressivity: </span>${f.elasticity}</div>` : ''}
<div style="font-size:9px;color:rgba(28,28,30,0.3)">Source: ${f.source||'World Bank / OECD 2023-2024'}</div>
</div>`;
}
function mentalHealthPopup(p) {
const m = p.mental_health;
if (!m) return '';
const suicide_col = m.suicide_per100k > 15 ? '#dc2626' :
m.suicide_per100k > 10 ? '#f97316' :
m.suicide_per100k > 6 ? '#ca8a04' : '#16a34a';
const stigma_col = m.stigma_index > 65 ? '#dc2626' :
m.stigma_index > 40 ? '#f97316' :
m.stigma_index > 20 ? '#ca8a04' : '#16a34a';
const system_col = m.mental_health_system >= 8 ? '#16a34a' :
m.mental_health_system >= 6 ? '#ca8a04' : '#dc2626';
const bar = (v, max) => {
const pct = Math.round((v/max)*100);
const col = v/max >= 0.8 ? '#16a34a' : v/max >= 0.5 ? '#ca8a04' : '#dc2626';
return `<div style="flex:1;height:4px;border-radius: var(--radius-sm);background:rgba(0,0,0,0.08)"><div style="width:${pct}%;height:100%;background:${col};border-radius: 2px"></div></div>`;
};
return `<div style="border-top:1px solid rgba(0,0,0,0.08);margin-top:10px;padding-top:10px">
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.4);margin-bottom:8px">MENTAL HEALTH INTELLIGENCE</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:8px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Depression rate</div>
<div style="font-size:13px;font-weight:800;color:#0A0A0A">${m.depression_pct}%</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Burnout rate</div>
<div style="font-size:13px;font-weight:800;color:#0A0A0A">${m.burnout_pct}%</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Suicide /100k</div>
<div style="font-size:13px;font-weight:800;color:${suicide_col}">${m.suicide_per100k}</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Stigma index</div>
<div style="font-size:13px;font-weight:800;color:${stigma_col}">${m.stigma_index}/100</div>
<div style="font-size:9px;color:rgba(28,28,30,0.35)">Low = accepting</div>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:11px;margin-bottom:8px">
<div style="display:flex;gap:8px;align-items:center">
<span style="width:72px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px">System</span>
${bar(m.mental_health_system, 10)}
<span style="color:${system_col};font-weight:700;font-size:11px;width:28px">${m.mental_health_system}/10</span>
</div>
<div style="display:flex;gap:8px;align-items:center">
<span style="width:72px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px">Work stress</span>
${bar(m.work_stress_index, 10)}
<span style="color:rgba(28,28,30,0.7);font-weight:700;font-size:11px;width:28px">${m.work_stress_index}/10</span>
</div>
<div style="display:flex;gap:8px;align-items:center">
<span style="width:72px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px">Antidepress.</span>
${bar(m.antidepressant_use_pct, 25)}
<span style="color:rgba(28,28,30,0.7);font-weight:700;font-size:11px;width:36px">${m.antidepressant_use_pct}%</span>
</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.65);line-height:1.5;margin-bottom:5px">${m.therapy_access}</div>
<div style="display:flex;align-items:center;gap:6px;background:rgba(220,38,38,0.06);border-radius: var(--radius-sm);padding:6px 9px">
<span style="font-size:9px;font-weight:700;color:#dc2626;flex-shrink:0">Crisis line</span>
<span style="font-size:11px;font-weight:700;color:#0A0A0A">${m.crisis_line}</span>
</div>
<div style="font-size:9px;color:rgba(28,28,30,0.3);margin-top:5px">Source: ${m.source} · Not medical advice</div>
</div>`;
}
function socialPopup(p) {
const s = p.social;
if (!s) return '';
const lgbtq_col = s.lgbtq_index >= 80 ? '#16a34a' :
s.lgbtq_index >= 50 ? '#ca8a04' :
s.lgbtq_index >= 25 ? '#f97316' : '#dc2626';
const marriage_col = s.gay_marriage && s.gay_marriage.toLowerCase().startsWith('legal') ? '#16a34a' :
s.gay_marriage && s.gay_marriage.toLowerCase().includes('partner') ? '#ca8a04' : '#dc2626';
const lgbtq_bar_pct = s.lgbtq_index || 0;
let religion_html = '';
if (s.religion_pct) {
const entries = Object.entries(s.religion_pct).sort((a,b)=>b[1]-a[1]).slice(0,4);
const colors = ['#60a5fa','#34d399','#f59e0b','#a78bfa'];
religion_html = `<div style="margin-top:8px">
<div style="font-size:9px;color:rgba(28,28,30,0.45);margin-bottom:5px">Religion (population %)</div>
<div style="display:flex;height:8px;border-radius: var(--radius-sm);overflow:hidden;margin-bottom:4px">
${entries.map((e,i)=>`<div style="width:${e[1]}%;background:${colors[i]};"></div>`).join('')}
</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;font-size:9px">
${entries.map((e,i)=>`<span style="color:${colors[i]};font-weight:600">${e[0]} ${e[1]}%</span>`).join('')}
</div>
</div>`;
}
return `<div style="border-top:1px solid rgba(0,0,0,0.08);margin-top:10px;padding-top:10px">
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.4);margin-bottom:8px">SOCIAL INTELLIGENCE</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:8px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">LGBTQ+ Index</div>
<div style="height:4px;background:rgba(0,0,0,0.08);border-radius: var(--radius-sm);margin-bottom:3px">
<div style="width:${lgbtq_bar_pct}%;height:100%;background:${lgbtq_col};border-radius: 2px"></div>
</div>
<div style="font-size:13px;font-weight:800;color:${lgbtq_col}">${s.lgbtq_index}/100</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Married %</div>
<div style="font-size:13px;font-weight:800;color:#0A0A0A">${s.married_pct}%</div>
<div style="font-size:9px;color:rgba(28,28,30,0.4)">Divorce ${s.divorce_rate}/1000</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Same-sex marriage</div>
<div style="font-size:10px;font-weight:700;color:${marriage_col};line-height:1.3">${s.gay_marriage}</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Foreign-born</div>
<div style="font-size:13px;font-weight:800;color:#0A0A0A">${s.foreign_born_pct}%</div>
</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.65);line-height:1.5;margin-bottom:6px">${s.lgbtq_note}</div>
${religion_html}
<div style="font-size:9px;color:rgba(28,28,30,0.3);margin-top:5px">Sources: ${s.social_source||'ILGA World / Eurostat 2025'}</div>
</div>`;
}
function legalPopup(p) {
const l = p.legal;
if (!l) return '';
const cannabis_col = l.cannabis && l.cannabis.toLowerCase().startsWith('legal') ? '#16a34a' :
l.cannabis && (l.cannabis.toLowerCase().includes('decrim') || l.cannabis.toLowerCase().includes('tolerat')) ? '#ca8a04' : '#dc2626';
const vaping_col = l.vaping && l.vaping.toLowerCase().includes('illegal') || l.vaping && l.vaping.toLowerCase().includes('ban') ? '#dc2626' :
l.vaping && l.vaping.toLowerCase().startsWith('legal') ? '#16a34a' : '#ca8a04';
const alcohol_col = l.alcohol_public && l.alcohol_public.toLowerCase().includes('ban') ? '#ca8a04' : '#16a34a';
return `<div style="border-top:1px solid rgba(0,0,0,0.08);margin-top:10px;padding-top:10px">
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.4);margin-bottom:8px">LEGAL INTELLIGENCE</div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:11px;margin-bottom:8px">
<div style="display:flex;gap:8px;align-items:baseline">
<span style="width:72px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px">Alcohol</span>
<span style="color:${alcohol_col};font-weight:600">${l.alcohol_public} (age ${l.alcohol_age})</span>
</div>
<div style="display:flex;gap:8px;align-items:baseline">
<span style="width:72px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px">Cannabis</span>
<span style="color:${cannabis_col};font-weight:600">${l.cannabis}</span>
</div>
<div style="display:flex;gap:8px;align-items:baseline">
<span style="width:72px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px">Vaping</span>
<span style="color:${vaping_col};font-weight:600">${l.vaping}</span>
</div>
<div style="display:flex;gap:8px;align-items:baseline">
<span style="width:72px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px">Gambling</span>
<span style="color:#0A0A0A;font-weight:600">${l.gambling}</span>
</div>
${l.snus ? `<div style="display:flex;gap:8px;align-items:flex-start">
<span style="width:72px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px;margin-top:1px">Snus</span>
<div style="flex:1"><span style="color:${l.snus.startsWith('Legal')||l.snus.startsWith('Grey')?'#1c1c1e':l.snus.startsWith('Banned')?'#dc2626':'#ca8a04'};font-weight:600;font-size:11px">${l.snus.split(' — ')[0]}</span>${l.snus.includes(' — ')?`<div style="font-size:9px;color:rgba(28,28,30,0.45);margin-top:1px">${l.snus.split(' — ').slice(1).join(' — ')}</div>`:''}</div>
</div>` : ''}
${l.prostitution ? `<div style="display:flex;gap:8px;align-items:flex-start">
<span style="width:72px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px;margin-top:1px">Prostitution</span>
<span style="color:#0A0A0A;font-weight:600;font-size:11px;line-height:1.3">${l.prostitution}</span>
</div>` : ''}
${l.prostitution_detail ? `<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:6px 8px;font-size:9px;color:rgba(28,28,30,0.6);line-height:1.4;margin-top:2px">${l.prostitution_detail}</div>` : ''}
</div>
${l.notes ? `<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.65);line-height:1.5">${l.notes}</div>` : ''}
<div style="font-size:9px;color:rgba(28,28,30,0.3);margin-top:5px">Source: ${p.legal_source||'EMCDDA/UNODC 2025'} · Not legal advice</div>
</div>`;
}
function practicalPopup(z) {
const p = getPractical(z.name);
if (!p) return '';
const bar = (v, max=10) => {
const pct = Math.round((v/max)*100);
const col = v>=8?'#16a34a':v>=6?'#ca8a04':'#dc2626';
return `<div style="flex:1;height:4px;border-radius: var(--radius-sm);background:rgba(0,0,0,0.08)"><div style="width:${pct}%;height:100%;background:${col};border-radius: 2px"></div></div>`;
};
const cashCol = p.cashless>=90?'#16a34a':p.cashless>=70?'#ca8a04':'#dc2626';
const speedCol = p.speed_mbps>=100?'#16a34a':p.speed_mbps>=40?'#ca8a04':'#dc2626';
const g5Col = p.g5_coverage>=80?'#16a34a':p.g5_coverage>=50?'#ca8a04':'#dc2626';
const restrictHtml = p.vpn_needed
? `<div style="background:rgba(220,38,38,0.08);border:1px solid rgba(220,38,38,0.2);border-radius: var(--radius-sm);padding:6px 9px;margin-top:6px;font-size:10px;color:#991b1b;font-weight:600">VPN REQUIRED — Internet restricted</div>`
: (p.restrictions.length ? `<div style="background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.2);border-radius: var(--radius-sm);padding:6px 9px;margin-top:6px;font-size:10px;color:#92400e">${p.restrictions.join(' · ')}</div>` : '');
const blockedHtml = p.social_blocked.length
? `<div style="font-size:10px;color:#dc2626;margin-top:4px">Blocked: ${p.social_blocked.join(', ')}</div>` : '';
return `<div style="border-top:1px solid rgba(0,0,0,0.08);margin-top:10px;padding-top:10px;font-family:-apple-system,sans-serif">
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.4);margin-bottom:8px">PRACTICAL INTELLIGENCE</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:8px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Public transport</div>
${bar(p.transport)}<div style="font-size:13px;font-weight:800;color:#0A0A0A">${p.transport}/10</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Cashless %</div>
${bar(p.cashless,100)}<div style="font-size:13px;font-weight:800;color:${cashCol}">${p.cashless}%</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Internet speed</div>
${bar(Math.min(p.speed_mbps/3,10))}<div style="font-size:13px;font-weight:800;color:${speedCol}">${p.speed_mbps} Mbps</div>
</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">5G coverage</div>
${bar(p.g5_coverage,100)}<div style="font-size:13px;font-weight:800;color:${g5Col}">${p.g5_coverage}%</div>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:11px;margin-bottom:6px">
<div style="display:flex;gap:8px;align-items:baseline">
<span style="width:68px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px">Taxi</span>
<span style="color:#0A0A0A;font-weight:600">${p.taxi_app}</span>
</div>
<div style="display:flex;gap:8px;align-items:baseline">
<span style="width:68px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px">Payments</span>
<span style="color:#0A0A0A;font-weight:600">${p.pay_apps}</span>
</div>
<div style="display:flex;gap:8px;align-items:baseline">
<span style="width:68px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px">SIM/eSIM</span>
<span style="color:#0A0A0A;font-weight:600">${p.sim}</span>
</div>
<div style="display:flex;gap:8px;align-items:baseline">
<span style="width:68px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px">English</span>
<span style="color:#0A0A0A;font-weight:600">${p.english}/10</span>
</div>
<div style="display:flex;gap:8px;align-items:baseline">
<span style="width:68px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px">Tipping</span>
<span style="color:#0A0A0A;font-weight:600">${p.tip}</span>
</div>
</div>
${restrictHtml}${blockedHtml}
${(()=>{
const sl={'Stockholm':'Available','G\u00f6teborg':'Available','Malm\u00f6':'Available','Uppsala':'Available','Lule\u00e5':'Available','Kiruna':'Available','London':'Available','Bangkok':'Available','Dubai':'Limited \u2014 regulatory','Singapore':'Unavailable \u2014 banned','New York':'Available','Houston':'Available','Los Angeles':'Available','Tokyo':'Available','Beijing':'Unavailable \u2014 banned','Shanghai':'Unavailable \u2014 banned','Mumbai':'Pending approval','Seoul':'Available','Amsterdam':'Available','Berlin':'Available','Paris':'Available','Madrid':'Available','Istanbul':'Available','Riyadh':'Limited \u2014 restricted','Cairo':'Unavailable \u2014 banned','Casablanca':'Available','Mexico City':'Available','S\u00e3o Paulo':'Available','Buenos Aires':'Available','Lagos':'Available','Nairobi':'Available','Jakarta':'Pending approval','Kuala Lumpur':'Available','Ho Chi Minh City':'Pending approval','Milan':'Available','Warsaw':'Available','Lisbon':'Available','Moscow':'Unavailable \u2014 banned'};
const status=sl[z.name]||null;
if(!status)return '';
const col=status.startsWith('Available')?'#16a34a':status.startsWith('Unavailable')?'#dc2626':'#ca8a04';
return `<div style="display:flex;align-items:center;justify-content:space-between;background:#f2f2f7;border-radius: var(--radius-md);padding:7px 10px;margin-top:6px"><div style="font-size:10px;color:rgba(28,28,30,0.5);font-weight:600">Starlink</div><div style="font-size:11px;font-weight:800;color:${col}">${status}</div></div>` + lcsPopup(p) + frictionPopup(p) + legalPopup(p) + socialPopup(p) + mentalHealthPopup(p) + foodPopup(p) + (p.speed_source?`<div style="font-size:9px;color:rgba(28,28,30,0.3);margin-top:5px">Speed: ${p.speed_source}</div>`:'');
})()}
</div>`;
}
const IN={infra:'Infrastructure Risk',commercial:'Commercial Activity',safety:'Safety Index',climate:'Climate Risk'};
function bar(v){const p=Math.round(v*10),c=v>=7.5?'#22c55e':v>=5?'#eab308':'#ef4444';return `<div class="zbar"><div class="zfill" style="width:${p}%;background:${c}"></div></div>`;}
function zpop(z){
let freeHtml='';
Object.entries(z.idx).forEach(([k,v])=>{
freeHtml+=`<div class="zidx"><div class="zil">${IN[k]||k}</div>${bar(v)}<div class="zval">${v}/10</div></div>`;
});
const cityCoord=z.lat?`${z.lat.toFixed(3)},${z.lng.toFixed(3)}`:`${((z.coords[0][0]+z.coords[2][0])/2).toFixed(3)},${((z.coords[0][1]+z.coords[1][1])/2).toFixed(3)}`;
const livePreview=`<div class="zidx"><div class="zil">Urban Growth</div>${bar(7.1)}<div class="zval">7.1/10</div></div><div class="zidx"><div class="zil">Crowd Density</div>${bar(6.4)}<div class="zval">6.4/10</div></div><div class="zidx"><div class="zil">Decay Index</div>${bar(4.8)}<div class="zval">4.8/10</div></div><div class="zidx"><div class="zil">Property Dev.</div>${bar(7.7)}<div class="zval">7.7/10</div></div>`;
return `<div class="zpop">
<div class="zcity">${z.name} <span class="zcountry">· ${z.country}</span></div>
<div class="zconf" style="display:flex;align-items:center;gap:6px">
<span>Confidence ${z.conf}%</span>
${['Stockholm','Bangkok','London','Dubai'].includes(z.name)?'<span style="background:rgba(0,122,255,0.12);color:#007aff;font-size:9px;font-weight:700;padding:1px 7px;border-radius: var(--radius-sm);border:1px solid rgba(0,122,255,0.2)">Deep Dive</span>':''}
</div>
<div class="zsect">Free indices — historical data</div>
<div class="zindices">${freeHtml}</div>
<div class="zcontra ${z.c}"><span class="zcl">${CL[z.c]}</span>${z.ct}</div>
<div class="zage"><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> Data period: ${z.age}</div>
<div class="zsect">Live intelligence — on demand</div>
<div class="lock-wrap">
<div class="zindices lock-blur">${livePreview}</div>
<div class="lock-over"><span style="font-size:14px">&#128274;</span><span>Available with live subscription</span></div>
</div>
${(()=>{
const deepLinks={'Stockholm':'/stockholm-intelligence','Bangkok':'/stockholm-intelligence?city=bangkok','London':'/stockholm-intelligence?city=london','Dubai':'/stockholm-intelligence?city=dubai'};
const dl=deepLinks[z.name];
if(dl){
return `<a href="${dl}" class="zcta" style="background:#007aff">Explore ${z.name} Intelligence </a>`;
}
return `<a href="https://www.landvex.com/enterprise?city=${encodeURIComponent(z.name)}" class="zcta">Request live intelligence →</a>`;
})()}
${practicalPopup(z)}
<div class="zdisc">Landvex Assessment. Not a factual statement. <a href="https://www.landvex.com/methodology">Methodology </a></div>
</div>`;
}
let zg=L.layerGroup().addTo(map),cg=L.layerGroup().addTo(map);
let ls={zones:true,contra:true};
function renderZ(){zg.clearLayers();if(!ls.zones)return;CITIES.forEach(z=>{const st=AS[z.a]||AS.yellow;const pop=zpop(z);const hasDeep=['Stockholm','Bangkok','London','Dubai'].includes(z.name);L.circle([z.lat,z.lng],{radius:z.r||15000,color:hasDeep?'#007aff':st.color,fillColor:st.color,fillOpacity:st.fo,weight:hasDeep?3:st.w,dashArray:st.d}).on("click",function(){if(isMob()){setTimeout(function(){map.closePopup();},10);openBsheet(zpop(z));}}).bindPopup(pop,{maxWidth:300,minWidth:260,autoPan:!isMob()}).addTo(zg);const lbl=L.marker([z.lat,z.lng],{icon:L.divIcon({className:"",html:`<div style="font-size:10px;font-weight:700;color:#0A0A0A;background:rgba(255,255,255,0.95);padding:2px 6px;border-radius: var(--radius-sm);white-space:nowrap;pointer-events:none;box-shadow:0 1px 3px rgba(0,0,0,0.15);border:1px solid rgba(0,0,0,0.1)">${z.name}</div>`,iconAnchor:[0,0]}),interactive:false});lbl.addTo(zg);});}
function renderC(){cg.clearLayers();if(!ls.contra)return;CITIES.filter(z=>z.c==='high').forEach(z=>{L.circle([z.lat,z.lng],{radius:(z.r||15000)*1.4,color:'#f59e0b',fillColor:'#f59e0b',fillOpacity:0.04,weight:1.5,dashArray:'3 6'}).addTo(cg);});}
loadPracticalData().then(() => { renderZ(); renderC(); });
// Also render immediately with empty practical (fills in async)
renderZ(); renderC();
function toggleLayer(id){ls[id]=!ls[id];const el=document.getElementById('tog-'+id);el.classList.toggle('on',ls[id]);el.classList.toggle('off',!ls[id]);if(id==='zones')renderZ();if(id==='contra')renderC();}
function selIdx(el){document.querySelectorAll('.ir').forEach(r=>r.classList.remove('active'));el.classList.add('active');}
function openBsheet(html){
var bs=document.getElementById('bsheet');
var bd=document.getElementById('bsheet-backdrop');
document.getElementById('bsheet-content').innerHTML=html;
bs.classList.add('open');
bd.classList.add('open');
document.body.style.overflow='hidden';
map.closePopup();
}
function closeBsheet(){
document.getElementById('bsheet').classList.remove('open');
document.getElementById('bsheet-backdrop').classList.remove('open');
document.body.style.overflow='';
}
// Swipe down to close
(function(){
var sy=0;
var el=document.getElementById('bsheet');
if(!el)return;
el.addEventListener('touchstart',function(e){sy=e.touches[0].clientY;},{passive:true});
el.addEventListener('touchend',function(e){if(e.changedTouches[0].clientY-sy>60)closeBsheet();},{passive:true});
})();
function isMob(){return window.innerWidth<640;}
</script>
</body>
</html>