Files
boc/landvex-all/malmo-intelligence.html
T

646 lines
40 KiB
HTML
Raw Normal View History

<!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"/>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#f2f2f7">
<link rel="apple-touch-icon" href="https://landvex.com/apple-touch-icon.png">
<meta name="apple-mobile-web-app-title" content="Landvex">
<link rel="manifest" href="/intelligence-manifest.json">
<title>Malmö — Landvex Intelligence Brief</title>
<link rel="stylesheet" href="/vendor/leaflet.css"/>
<script src="/vendor/leaflet.js"></script>
<link rel="stylesheet" href="/assets/css/landvex-complete.css">
<style>
*{box-sizing:border-box;margin:0;padding:0;-webkit-font-smoothing:antialiased}
:root{
--bg:#f2f2f7;--s1:#ffffff;--s2:#f2f2f7;--s3:#e5e5ea;
--border:rgba(0,0,0,0.08);--b2:rgba(0,0,0,0.12);
--text:#1c1c1e;--t2:rgba(28,28,30,0.72);--t3:rgba(28,28,30,0.50);
--accent:#007aff;--accent2:#0071e3;--accent3:#0066cc;
--mono:'SF Mono',ui-monospace,monospace;
}
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Helvetica Neue',sans-serif;background:var(--bg);color:var(--text);overflow:hidden}
.leaflet-control-attribution{display:none!important}
.leaflet-bottom.leaflet-right{display:none!important}
#app{display:grid;grid-template-columns:260px 1fr 320px;height:100vh;overflow:hidden}
#left{background:var(--s1);border-right:1px solid var(--border);overflow-y:auto;display:flex;flex-direction:column}
#city-block{padding: 24px 14px;border-bottom:1px solid var(--border)}
#city-eyebrow{font-size:9px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent3);margin-bottom:6px}
#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: 16px;margin-top:12px}
.cstat{background:var(--s2);border:1px solid var(--border);border-radius: var(--radius-sm);padding: 24px}
.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: 24px;border-bottom:1px solid var(--border)}
#profile-tabs{display:grid;grid-template-columns:1fr 1fr 1fr;gap: 16px;background:rgba(0,0,0,0.06);border-radius: var(--radius-md);padding:2px}
.ptab{padding: 24px;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: 24px}
#district-lbl{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);margin-bottom:8px}
.dcard{border-radius: var(--radius-md);background:var(--s2);padding: 24px;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: 24px;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: 16px;font-size:9px;font-weight:700;border-radius: var(--radius-sm);padding: 24px}
.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}
#freshness-block{padding: 24px;border-top:1px solid var(--border);margin-top:auto}
#freshness-title{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);margin-bottom:8px}
.fresh-row{display:flex;align-items:center;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border)}
.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: 16px}
.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)}
#map{width:100%;height:100%}
#map-topbar{position:absolute;top:0;left:0;right:0;z-index:1000;height:44px;display:flex;align-items:center;gap: 16px;padding:0 14px;background:rgba(0,0,0,0.92);backdrop-filter:blur(20px) saturate(180%);border-bottom:1px solid rgba(0,0,0,0.1)}
#map-logo{font-size:11px;font-weight:800;color:var(--text);text-decoration:none}
#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: var(--radius-sm);padding: 24px;border:1px solid}
#layer-bar{position:absolute;top:44px;left:0;right:0;z-index:999;display:flex;align-items:center;gap: 16px;padding: 24px;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: 16px;font-size:11px;font-weight:600;color:var(--t2);background:rgba(0,0,0,0.05);border:none;border-radius: var(--radius-xl);padding: 24px;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: 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: var(--radius-lg)!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: 24px}
.mpop-eyebrow{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--accent3);margin-bottom:3px}
.mpop-name{font-size:14px;font-weight:800;color:var(--text);margin-bottom:8px}
.mpop-score-row{display:flex;align-items:baseline;gap: 16px;margin-bottom:10px}
.mpop-score{font-size:26px;font-weight:900;font-family:var(--mono)}
.mpop-bars{display:flex;flex-direction:column;gap: 16px;margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid var(--border)}
.mpop-bar-row{display:flex;align-items:center;gap: 16px}
.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: 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: var(--radius-sm);padding: 24px;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)}
.mpop-contra-lbl{font-weight:800;font-size:9px;text-transform:uppercase;display:block;margin-bottom:2px}
.mpop-insight{font-size:11px;color:var(--t2);line-height:1.5}
#right{background:var(--s1);border-left:1px solid var(--border);overflow-y:auto;display:flex;flex-direction:column}
#right-header{padding: 24px 10px;border-bottom:1px solid var(--border)}
#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: 24px 8px;display:grid;grid-template-columns:1fr 1fr;gap: 16px;border-bottom:1px solid var(--border)}
.kpi{background:var(--s2);border-radius: var(--radius-md);padding: 24px}
.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}
.kpi.span2{grid-column:span 2}
#insights-block{padding: 24px;border-bottom:1px solid var(--border)}
#insights-title{font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);margin-bottom:10px}
.insight-row{display:flex;gap: 16px;align-items:flex-start;padding:7px 0;border-bottom:1px solid var(--border)}
.insight-row:last-child{border-bottom:none}
.insight-icon{font-size:11px;font-weight:800;color:var(--accent3);flex-shrink:0;margin-top:3px;width:12px}
.insight-text{font-size:13px;color:rgba(28,28,30,0.8);line-height:1.6}
.insight-text strong{color:var(--text);font-weight:700}
#contra-block{padding: 24px;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: 16px}
.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: 24px;border-radius: var(--radius-md)}
.contra-card{border-radius: var(--radius-sm);border:1px solid var(--border);background:var(--s2);padding: 24px;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: 24px;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: 24px;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: 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: 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: var(--radius-md);padding: 24px}
@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: 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}
#left{order:1;border-right:none}
#city-stats{display:flex;gap: 16px;flex-wrap:nowrap}
.cstat{background:transparent;border:none;padding:0;flex-shrink:0}
#district-block{display:none}
#freshness-block{display:none}
#map-wrap{order:2;height:60vh;min-height:340px;max-height:520px}
#right{order:3;border-left:none;border-top:1px solid var(--border)}
.leaflet-popup{display:none!important}
}
@media(max-width:480px){#map-wrap{height:55vh;min-height:300px}}
@media(max-width:900px){.label-marker{display:none!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>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": [
"Organization",
"Corporation"
],
"@id": "https://landvex.com/#organization",
"name": "Landvex",
"alternateName": [
"LandveX",
"Landvex AB",
"LandveX AB"
],
"url": "https://landvex.com",
"logo": {
"@type": "ImageObject",
"url": "https://landvex.com/apple-touch-icon.png",
"width": 180,
"height": 180
},
"foundingDate": "2024",
"foundingLocation": {
"@type": "Place",
"addressLocality": "Gothenburg",
"addressRegion": "Gothenburg",
"addressCountry": "SE"
},
"legalName": "Landvex Inc",
"address": {
"@type": "PostalAddress",
"addressLocality": "Houston",
"addressRegion": "TX",
"addressCountry": "US"
},
"location": [
{
"@type": "Place",
"name": "Houston, Texas, USA (US HQ)",
"address": {
"@type": "PostalAddress",
"addressLocality": "Houston",
"addressRegion": "TX",
"addressCountry": "US"
}
},
{
"@type": "Place",
"name": "Stockholm, Sweden (EU HQ)",
"address": {
"@type": "PostalAddress",
"addressLocality": "Gothenburg",
"addressRegion": "Gothenburg",
"addressCountry": "SE"
}
}
],
"taxID": "pending-EIN",
"description": "Landvex is a decision intelligence company that identifies contradictions between official narratives and observed physical reality. Using the quiXzoom field observation network and the AMOS AI analysis engine, Landvex delivers infrastructure risk indexes, urban intelligence scores, and contradiction reports to infrastructure owners, municipalities, enterprises, and investors globally.",
"disambiguatingDescription": "Landvex (LandveX) is a decision intelligence company with US headquarters in Houston, Texas (Landvex Inc.) and European headquarters in Stockholm, Sweden (Landvex AB, org.nr 559141-7042). Landvex delivers control intelligence — infrastructure risk scores, urban intelligence, and contradiction analysis — to municipalities, enterprises, infrastructure operators and investors. Landvex is not a UK meat wholesaler, food distributor, logistics company, package tracker, or land investment platform.",
"knowsAbout": [
"control intelligence",
"decision intelligence",
"infrastructure risk assessment",
"urban intelligence",
"field data collection",
"contradiction detection",
"physical world analytics",
"geospatial analytics"
],
"brand": {
"@type": "Brand",
"name": "Landvex",
"slogan": "Control intelligence for the physical world."
},
"sameAs": [
"https://landvex.com",
"https://www.linkedin.com/company/landvex",
"https://x.com/landvex",
"https://twitter.com/landvex",
"https://www.crunchbase.com/organization/landvex",
"https://github.com/landvex",
"https://www.instagram.com/landvex"
],
"contactPoint": {
"@type": "ContactPoint",
"email": "contact@landvex.com",
"contactType": "sales"
},
"slogan": "Where reported reality conflicts with observed reality."
}
</script>
</head>
<body>
<div id="app">
<div id="left">
<div id="city-block">
<div id="city-eyebrow">Landvex Intelligence Brief</div>
<div id="city-name">MALMÖ</div>
<div id="city-region">Skåne County · Kingdom of Sweden</div>
<div id="city-stats">
<div class="cstat"><div class="cstat-val">86%</div><div class="cstat-lbl">Confidence</div></div>
<div class="cstat"><div class="cstat-val" id="district-count">7</div><div class="cstat-lbl">Districts</div></div>
<div class="cstat"><div class="cstat-val">High</div><div class="cstat-lbl">Contradiction</div></div>
<div class="cstat"><div class="cstat-val">Jun 2026</div><div class="cstat-lbl">Updated</div></div>
</div>
</div>
<div id="profile-block">
<div id="profile-tabs">
<button class="ptab active" data-p="business" onclick="setProfile('business',this)">Business</button>
<button class="ptab" data-p="tourist" onclick="setProfile('tourist',this)">Tourist</button>
<button class="ptab" data-p="safety" onclick="setProfile('safety',this)">Safety</button>
<button class="ptab" data-p="investment" onclick="setProfile('investment',this)">Investment</button>
<button class="ptab" data-p="nightlife" onclick="setProfile('nightlife',this)">Nightlife</button>
<button class="ptab" data-p="infrastructure" onclick="setProfile('infrastructure',this)">Infra</button>
</div>
</div>
<div id="district-block">
<div id="district-lbl">Districts · ranked by profile</div>
<div id="district-list"></div>
</div>
<div id="freshness-block">
<div id="freshness-title">Data Freshness</div>
<div class="fresh-row">
<div class="fresh-source">Police events</div>
<div class="fresh-val"><div class="fresh-dot live"></div><span id="police-fresh">Loading…</span></div>
</div>
<div class="fresh-row">
<div class="fresh-source">SVT Skåne</div>
<div class="fresh-val"><div class="fresh-dot live"></div><span>Live</span></div>
</div>
<div class="fresh-row">
<div class="fresh-source">SCB demographics</div>
<div class="fresh-val"><div class="fresh-dot recent"></div><span>2024 annual</span></div>
</div>
</div>
</div>
<div id="bottom-sheet-backdrop" onclick="closeBottomSheet()"></div>
<div id="bottom-sheet">
<div id="bottom-sheet-handle"></div>
<button id="bottom-sheet-close" onclick="closeBottomSheet()">&#215;</button>
<div id="bottom-sheet-content"></div>
</div>
<div id="map-wrap">
<div id="map-topbar">
<a href="https://landvex.com" id="map-logo">Land<em>vex</em></a>
<div class="map-sep"></div>
<div id="map-city-label">Malmö — <span id="map-profile-label">Business</span></div>
<div id="map-profile-chip">Business</div>
<div>
<button onclick="map.setView([55.605,13.004],12)">MMA</button>
<button onclick="map.setView([55.7,13.2],9)">Skåne</button>
<button onclick="window.location='/intelligence'">All cities</button>
</div>
</div>
<div id="layer-bar">
<div class="ltoggle on" id="ltog-districts" onclick="toggleLayer('districts',this)">
<div class="ltoggle-dot"></div>Areas
</div>
<div class="ltoggle" id="ltog-pulse" onclick="toggleLayer('pulse',this)">
<div class="ltoggle-dot"></div>Activity
</div>
<div class="ltoggle" id="ltog-alerts" onclick="toggleLayer('alerts',this)">
<div class="ltoggle-dot"></div>Contradictions
</div>
<div class="ltoggle on" id="ltog-police" onclick="togglePolice(this)">
<div class="ltoggle-dot"></div>Police
</div>
</div>
<div id="map"></div>
<div id="map-legend">
<div>Intelligence Score</div>
<div>
<div><div></div>Strong (810)</div>
<div><div></div>Moderate (68)</div>
<div><div></div>Weak (46)</div>
<div><div></div>Poor (&lt;4)</div>
</div>
<div>Tap any area for details</div>
</div>
</div>
<div id="right">
<div id="right-header">
<div id="right-profile-label">Profile</div>
<div id="right-title">Business</div>
</div>
<div id="kpi-grid"></div>
<div id="insights-block">
<div id="insights-title">Key Insights</div>
<div id="insights-list"></div>
</div>
<div id="contra-block">
<div id="contra-header">
<div id="contra-title"><div class="contra-live-dot"></div>Contradiction Alerts</div>
<div class="contra-count" id="contra-count">2 active</div>
</div>
<div id="contra-list"></div>
</div>
<div id="news-block">
<div>
SVT Skåne — Live News
<span id="news-time">Loading…</span>
</div>
<div id="news-list">Loading…</div>
</div>
<div id="police-block">
<div>
Police Events · Live
<span id="police-count">Loading…</span>
</div>
<div id="police-list">Loading…</div>
</div>
<div id="right-footer">
<a href="/intelligence" >← All Cities</a> ·
<a href="/stockholm-intelligence" >Stockholm</a> ·
<a href="/goteborg-intelligence" >Göteborg</a> ·
<a href="/uppsala-intelligence" >Uppsala</a><br><br>
Data: Polisen.se · SVT Skåne · SCB · Landvex Intelligence Engine<br>
© 2026 Landvex. Source First.
</div>
</div>
</div>
<script>
const D=[
{id:'centrum',name:'Centrum',lat:55.605,lng:13.003,r:1200,
confidence:{score:88,observation_count:441},
s:{business:7.8,tourist:8.2,safety:7.1,investment:7.8,nightlife:7.9,infrastructure:8.4},
c:'medium',ct:'Night safety slightly below official. Commercial activity aligns well.',
col:'#16a34a',pulse:{level:0.61,label:'High Activity'},umi:+34,
ins:{business:'Strong retail, growing tech scene. Central station hub.',tourist:'Stortorget, Lilla Torg. Vibrant city centre.',safety:'Good by day. Some night risk areas.',investment:'Rising market. Copenhagen proximity premium.',nightlife:'Best in Malmö. Lilla Torg cluster.',infrastructure:'Excellent. Pågatåg, Öresund direct trains.'}},
{id:'hyllie',name:'Hyllie',lat:55.560,lng:13.020,r:1500,
confidence:{score:90,observation_count:247},
s:{business:8.6,tourist:4.2,safety:8.4,investment:9.3,nightlife:3.2,infrastructure:9.4},
c:'low',ct:'Major development zone. Malmö Arena. Strong infrastructure investment.',
col:'#16a34a',pulse:{level:0.39,label:'LowModerate'},umi:+78,
ins:{business:'Emporia mall, Malmö Arena. Large event economy.',tourist:'Not a tourist area.',safety:'Safe. Well-managed commercial zone.',investment:'Highest development activity in Malmö.',nightlife:'Event-driven only.',infrastructure:'Hyllie station — direct to Copenhagen 20 min.'}},
{id:'husie',name:'Husie',lat:55.580,lng:13.080,r:1600,
confidence:{score:82,observation_count:198},
s:{business:5.4,tourist:3.8,safety:8.6,investment:7.1,nightlife:3.4,infrastructure:7.2},
c:'low',ct:'Stable residential suburb. Alignment strong.',
col:'#16a34a',pulse:{level:0.28,label:'Low Activity'},umi:+12,
ins:{business:'Local services. Limited commercial.',tourist:'No tourist appeal.',safety:'Very safe. Residential character.',investment:'Affordable family market.',nightlife:'Very limited.',infrastructure:'Bus connections. Car-dependent.'}},
{id:'rosengard',name:'Rosengård',lat:55.592,lng:13.043,r:1800,
confidence:{score:73,observation_count:194},
s:{business:3.4,tourist:2.1,safety:3.9,investment:6.1,nightlife:3.2,infrastructure:5.6},
c:'high',ct:'CONTRADICTION HIGH: Official data significantly understates observed conditions. Infrastructure decay, elevated incidents.',
col:'#dc2626',pulse:{level:0.19,label:'Sparse Activity'},umi:-14,
ins:{business:'Very limited. Local services only.',tourist:'Not suitable for tourism.',safety:'Elevated risk. Night incidents regular.',investment:'Redevelopment potential. High risk horizon required.',nightlife:'Not recommended.',infrastructure:'Metro accessible. Poor local street conditions.'}},
{id:'limhamn',name:'Limhamn',lat:55.581,lng:12.937,r:1400,
confidence:{score:91,observation_count:208},
s:{business:5.4,tourist:6.2,safety:9.1,investment:8.4,nightlife:4.8,infrastructure:7.4},
c:'low',ct:'Premium coastal family area. Strong alignment.',
col:'#16a34a',pulse:{level:0.34,label:'Low Activity'},umi:+28,
ins:{business:'Local commercial. Limestone heritage.',tourist:'Beaches, ferry to Denmark.',safety:'Among safest in Malmö.',investment:'Strong family demand. Coastal premium.',nightlife:'Limited.',infrastructure:'Tram. Öresund bridge access.'}},
{id:'vastra-hamnen',name:'Västra Hamnen',lat:55.614,lng:12.993,r:1000,
confidence:{score:93,observation_count:318},
s:{business:8.1,tourist:8.8,safety:9.2,investment:9.1,nightlife:6.4,infrastructure:8.9},
c:'low',ct:'Showcase sustainable district. Strong alignment official/observed.',
col:'#16a34a',pulse:{level:0.48,label:'Moderate Activity'},umi:+56,
ins:{business:'Premium offices, WeWork, startups.',tourist:'Turning Torso, waterfront architecture.',safety:'Extremely safe. Premium residential.',investment:'Highest Malmö property values. Waterfront premium.',nightlife:'Growing scene.',infrastructure:'Excellent waterfront connectivity.'}},
{id:'oxie',name:'Oxie',lat:55.548,lng:13.060,r:1800,
confidence:{score:79,observation_count:163},
s:{business:4.8,tourist:3.2,safety:8.8,investment:7.2,nightlife:2.8,infrastructure:6.8},
c:'low',ct:'Outer suburb. Growing commuter area. Alignment strong.',
col:'#16a34a',pulse:{level:0.22,label:'Low Activity'},umi:+18,
ins:{business:'Local services. Growing commuter hub.',tourist:'No tourist appeal.',safety:'Very safe. Suburban residential.',investment:'Affordable entry. Growing demand.',nightlife:'Very limited.',infrastructure:'Bus connections. E65 access.'}}
];
const CONTRA_ALERTS=[
{distId:'rosengard',name:'Rosengård',level:'high',desc:'Safety classification significantly understates observed risk. Infrastructure maintenance gaps confirmed. Incident density elevated vs official data.',date:'2026-06-18'},
{distId:'centrum',name:'Centrum',level:'medium',desc:'Night incidents in Lilla Torg area underreported. Avenyn-equivalent risk on weekends.',date:'2026-06-15'}
];
const PROF={
business:{label:'Business',col:'#2d8fcf',chipBg:'rgba(45,143,207,0.15)',chipBorder:'rgba(45,143,207,0.35)',chipCol:'#0066cc'},
tourist:{label:'Tourist',col:'#22c55e',chipBg:'rgba(34,197,94,0.15)',chipBorder:'rgba(34,197,94,0.3)',chipCol:'#16a34a'},
safety:{label:'Safety',col:'#dc2626',chipBg:'rgba(220,38,38,0.15)',chipBorder:'rgba(220,38,38,0.3)',chipCol:'#991b1b'},
investment:{label:'Investment',col:'#d97706',chipBg:'rgba(217,119,6,0.15)',chipBorder:'rgba(217,119,6,0.3)',chipCol:'#92400e'},
nightlife:{label:'Nightlife',col:'#9333ea',chipBg:'rgba(147,51,234,0.15)',chipBorder:'rgba(147,51,234,0.3)',chipCol:'#6b21a8'},
infrastructure:{label:'Infrastructure',col:'#b45309',chipBg:'rgba(180,83,9,0.15)',chipBorder:'rgba(180,83,9,0.3)',chipCol:'#78350f'}
};
const INSIGHTS={
business:['<strong>Hyllie</strong> — highest business score. Emporia mall + Malmö Arena. Major event economy.','<strong>Västra Hamnen</strong> — flagship premium offices. WeWork, international startups.','<strong>Centrum</strong> — retail core. Tech scene growing. Copenhagen spillover.','<strong>Rosengård contradiction</strong> — official commercial data severely lags reality.'],
tourist:['<strong>Västra Hamnen</strong> — Turning Torso, waterfront. Architecture tourism peak.','<strong>Centrum Lilla Torg</strong> — best restaurant & café cluster. Instagram-friendly.','<strong>Limhamn</strong> — coastal beaches. Ferry to Denmark. Day trip destination.','<strong>Hyllie</strong> — event tourism. Malmö Arena international acts.'],
safety:['<strong>Västra Hamnen</strong> — safest district. Premium residential, well-lit.','<strong>Limhamn & Husie</strong> — very safe. Family residential character.','<strong>Rosengård CONTRADICTION</strong> — significant official vs observed safety gap.','<strong>Centrum</strong> — generally safe. Night awareness on weekends.'],
investment:['<strong>Hyllie</strong> — highest investment score. Station + arena driving growth.','<strong>Västra Hamnen</strong> — premium waterfront. Highest property values in Malmö.','<strong>Limhamn</strong> — coastal family demand. Öresund bridge premium.','<strong>Rosengård</strong> — high risk, high potential. Government priority area.'],
nightlife:['<strong>Centrum Lilla Torg</strong> — best nightlife cluster in Malmö.','<strong>Västra Hamnen</strong> — growing premium bar scene. Waterfront venue.', '<strong>Hyllie</strong> — event-driven. Malmö Arena nights.','<strong>Limhamn / Oxie</strong> — very limited.'],
infrastructure:['<strong>Hyllie station</strong> — direct Copenhagen 20 min. Regional hub.','<strong>Västra Hamnen</strong> — excellent. Ferry potential under discussion.','<strong>Centrum</strong> — Pågatåg + Öresund. Malmö Central hub.','<strong>Rosengård</strong> — metro accessible. Poor local streets — contradiction.']
};
const KPI={
business:{main:'9.3',mainLabel:'Peak Score — Hyllie',sub:'Emporia + Malmö Arena',extra:'8.1',extraLabel:'Västra Hamnen'},
tourist:{main:'8.8',mainLabel:'Tourist — Västra Hamnen',sub:'Turning Torso · waterfront',extra:'8.2',extraLabel:'Centrum — Lilla Torg'},
safety:{main:'3.9',mainLabel:'Lowest — Rosengård',sub:'CONTRADICTION: data lag',extra:'9.2',extraLabel:'Highest — Västra Hamnen'},
investment:{main:'9.3',mainLabel:'Peak Score — Hyllie',sub:'Station + development zone',extra:'+78',extraLabel:'Top UMI — Hyllie'},
nightlife:{main:'7.9',mainLabel:'Nightlife — Centrum',sub:'Lilla Torg cluster',extra:'6.4',extraLabel:'Västra Hamnen growing'},
infrastructure:{main:'9.4',mainLabel:'Infra — Hyllie',sub:'Direct CPH 20 min',extra:'8.9',extraLabel:'Västra Hamnen'}
};
let map,profile='business',polys={},pulseMarkers=[],alertMarkers=[];
let layerState={districts:true,pulse:false,alerts:false};
let policeGroup=L.layerGroup(),policeVisible=true;
function sc(v){return v>=8?'#22c55e':v>=6?'#eab308':v>=4?'#f97316':'#ef4444'}
function isMobile(){return window.innerWidth<=900}
function openBottomSheet(html){document.getElementById('bottom-sheet-content').innerHTML=html;document.getElementById('bottom-sheet').classList.add('open');document.getElementById('bottom-sheet-backdrop').classList.add('open')}
function closeBottomSheet(){document.getElementById('bottom-sheet').classList.remove('open');document.getElementById('bottom-sheet-backdrop').classList.remove('open')}
function buildPopup(d,p){
const sv=d.s[p],col=sc(sv);
const cLabel=d.c==='high'?'HIGH CONTRADICTION':d.c==='medium'?'MEDIUM CONTRADICTION':'LOW CONTRADICTION';
const barsHtml=Object.entries(d.s).map(([k,v])=>`<div class="mpop-bar-row"><div class="mpop-bar-lbl">${k}</div><div class="mpop-bar-track"><div class="mpop-bar-fill"></div></div><div class="mpop-bar-val">${v}</div></div>`).join('');
const confBadge=d.confidence?`<div>Confidence: <strong>${d.confidence.score}%</strong> · ${d.confidence.observation_count} observations</div>`:'';
return `<div class="mpop">
<div class="mpop-eyebrow">Malmö · ${PROF[p].label}</div>
<div class="mpop-name">${d.name}</div>
${confBadge}
<div class="mpop-score-row"><div class="mpop-score">${sv.toFixed(1)}</div><div>/10</div><div>${PROF[p].label}<br>score</div></div>
<div class="mpop-bars">${barsHtml}</div>
<div class="mpop-contra ${d.c}"><span class="mpop-contra-lbl">${cLabel}</span>${d.ct}</div>
${d.ins&&d.ins[p]?`<div class="mpop-insight">${d.ins[p]}</div>`:''}
${d.umi!==undefined?`<div>Urban Momentum: <strong>${d.umi>0?'+':''}${d.umi}</strong></div>`:''}
</div>`;
}
function buildList(p){
const sorted=[...D].sort((a,b)=>b.s[p]-a.s[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':'<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">${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('');
}
function buildRight(p){
const kpi=KPI[p],pr=PROF[p];
document.getElementById('right-title').textContent=pr.label;
const chip=document.getElementById('map-profile-chip');
chip.textContent=pr.label;chip.style.background=pr.chipBg;chip.style.color=pr.chipCol;chip.style.borderColor=pr.chipBorder;
document.getElementById('map-profile-label').textContent=pr.label;
document.getElementById('kpi-grid').innerHTML=`
<div class="kpi span2"><div class="kpi-label">${kpi.mainLabel}</div><div class="kpi-value">${kpi.main}</div><div class="kpi-sub">${kpi.sub}</div></div>
<div class="kpi"><div class="kpi-label">Alt Score</div><div class="kpi-value">${kpi.extra}</div><div class="kpi-sub">${kpi.extraLabel}</div></div>
<div class="kpi"><div class="kpi-label">Districts</div><div class="kpi-value">${D.length}</div><div class="kpi-sub">monitored</div></div>`;
const ins=INSIGHTS[p]||[];
document.getElementById('insights-list').innerHTML=ins.map(t=>`<div class="insight-row"><div class="insight-icon"></div><div class="insight-text">${t}</div></div>`).join('');
buildContraAlerts();
}
function buildContraAlerts(){
document.getElementById('contra-count').textContent=CONTRA_ALERTS.length+' active';
document.getElementById('contra-list').innerHTML=CONTRA_ALERTS.map(c=>`<div class="contra-card c-${c.level}"><div class="contra-card-head"><div class="contra-card-name">${c.name}</div><div class="contra-card-badge ${c.level}">${c.level.toUpperCase()}</div></div><div class="contra-card-desc">${c.desc}</div><div>${c.date}</div></div>`).join('');
}
function setProfile(p,btn){
profile=p;
document.querySelectorAll('.ptab').forEach(t=>t.classList.remove('active'));
btn.classList.add('active');
buildList(p);buildRight(p);updatePolyColors(p);
}
function focusDist(id){
const d=D.find(x=>x.id===id);if(!d)return;
map.setView([d.lat,d.lng],13);
if(polys[id]){polys[id].openPopup();if(isMobile())openBottomSheet(buildPopup(d,profile));}
document.querySelectorAll('.dcard').forEach(c=>c.classList.remove('active'));
const el=document.getElementById('dc-'+id);if(el)el.classList.add('active');
}
function updatePolyColors(p){
D.forEach(d=>{const c=sc(d.s[p]);if(polys[d.id]){polys[d.id].setStyle({color:c,fillColor:c});polys[d.id].setPopupContent(buildPopup(d,p));}});
}
function toggleLayer(name,btn){
layerState[name]=!layerState[name];btn.classList.toggle('on',layerState[name]);
if(name==='districts')D.forEach(d=>{if(polys[d.id]){layerState.districts?polys[d.id].addTo(map):polys[d.id].remove();}});
if(name==='pulse')pulseMarkers.forEach(m=>layerState.pulse?m.addTo(map):m.remove());
if(name==='alerts')alertMarkers.forEach(m=>layerState.alerts?m.addTo(map):m.remove());
}
function togglePolice(btn){policeVisible=!policeVisible;btn.classList.toggle('on',policeVisible);policeVisible?policeGroup.addTo(map):map.removeLayer(policeGroup);}
function buildPulseMarkers(){
pulseMarkers.forEach(m=>m.remove());pulseMarkers=[];
D.forEach(d=>{if(!d.pulse)return;const lvl=d.pulse.level,sz=Math.round(8+lvl*10),col=lvl>0.7?'#c084fc':lvl>0.4?'#60a5fa':'#34d399';
const icon=L.divIcon({className:'pulse-marker',html:`<div><div class="pulse-outer"></div><div class="pulse-inner"></div></div>`,iconSize:[1,1],iconAnchor:[0,0]});
const m=L.marker([d.lat,d.lng],{icon,interactive:false});if(layerState.pulse)m.addTo(map);pulseMarkers.push(m);
});
}
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>${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><div>CONTRADICTION ${d.c.toUpperCase()}</div><div>${d.name}</div><div>${ca.desc}</div></div>`,{maxWidth:240});
if(layerState.alerts)marker.addTo(map);alertMarkers.push(marker);
});
}
const INCIDENT_COLORS={'Misshandel':'#ef4444','Rån':'#dc2626','Skottlossning':'#7f1d1d','Narkotikabrott':'#eab308','Stöld':'#f59e0b','Trafikolycka':'#3b82f6','default':'#6b7280'};
async function loadPoliceEvents(){
try{
const now=new Date(),ym=now.getFullYear()+'-'+String(now.getMonth()+1).padStart(2,'0');
const r=await fetch('https://polisen.se/api/events?locationname=Malmö&DateTime='+ym);
const data=await r.json();
const events=data.filter(e=>e.location&&e.location.gps&&e.location.gps!=='0,0');
policeGroup.clearLayers();
events.forEach(e=>{
const [lat,lng]=e.location.gps.split(',').map(Number);if(isNaN(lat)||isNaN(lng))return;
const c=INCIDENT_COLORS[e.type]||INCIDENT_COLORS.default;
L.circleMarker([lat,lng],{radius:5,color:c,fillColor:c,fillOpacity:0.75,weight:1})
.bindPopup(`<div><div>${e.type}</div><div>${e.summary}</div><div>Polisen.se · ${new Date(e.datetime).toLocaleString('sv-SE',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}</div></div>`,{maxWidth:240})
.addTo(policeGroup);
});
if(policeVisible)policeGroup.addTo(map);
document.getElementById('police-count').textContent=events.length+' events';
document.getElementById('police-fresh').textContent='Live';
document.getElementById('police-list').innerHTML=events.slice(0,5).map(e=>`<div><span>${e.type}</span> · ${e.summary.substring(0,60)}${e.summary.length>60?'…':''}<div>${new Date(e.datetime).toLocaleString('sv-SE',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}</div></div>`).join('');
}catch(e){document.getElementById('police-count').textContent='unavailable';document.getElementById('police-fresh').textContent='Unavailable';}
}
function timeSince(date){const s=Math.floor((new Date()-date)/1000);if(s<3600)return Math.floor(s/60)+'m ago';if(s<86400)return Math.floor(s/3600)+'h ago';return Math.floor(s/86400)+'d ago';}
async function loadNews(){
try{
const r=await fetch('/api/svt-malmo'),d=await r.json();
const parser=new DOMParser(),xml=parser.parseFromString(d.contents,'text/xml'),items=xml.querySelectorAll('item');
let html='',count=0;
items.forEach(item=>{
if(count>=6)return;
const title=item.querySelector('title'),link=item.querySelector('link'),pubDate=item.querySelector('pubDate');
if(!title)return;
const t=title.textContent.replace(/<!\[CDATA\[|\]\]>/g,'').trim(),l=link?link.textContent.trim():'',dt=pubDate?new Date(pubDate.textContent):null,age=dt?timeSince(dt):'';
html+=`<div onclick="window.open('${l}','_blank')"><div>${t}</div><div>${age} · SVT Skåne</div></div>`;
count++;
});
document.getElementById('news-list').innerHTML=html||'<div>No news available</div>';
document.getElementById('news-time').textContent='Updated '+new Date().toLocaleTimeString('sv-SE',{hour:'2-digit',minute:'2-digit'});
}catch(e){document.getElementById('news-list').innerHTML='<a href="https://svt.se/nyheter/lokalt/skane" target="_blank" >SVT Skåne →</a>';}
}
window.addEventListener('DOMContentLoaded',()=>{
map=L.map('map',{center:[55.6050,13.0038],zoom:12,zoomControl:true,attributionControl:false});
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',{maxZoom:19,subdomains:'abcd'}).addTo(map);
D.forEach(d=>{
const c=sc(d.s.business);
const poly=L.circle([d.lat,d.lng],{radius:d.r*0.65,color:c,fillColor:c,fillOpacity:0.14,weight:2,dashArray:d.c==='high'?'4 3':d.c==='medium'?'6 4':null});
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>${d.name}</div>`,iconAnchor:[0,0]}),interactive:false}).addTo(map);}
});
document.getElementById('district-count').textContent=D.length;
buildPulseMarkers();buildAlertMarkers();buildList('business');buildRight('business');
loadPoliceEvents();loadNews();
});
</script>
</body>
</html>