2026-07-05 06:41:32 +00:00
<!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 , 0 px ) ; --safe-bottom : env ( safe - area - inset - bottom , 0 px ) }
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 ) + 72 px ) !important ; margin-left : 16 px !important }
2026-07-07 07:11:50 +00:00
. leaflet-control-zoom a { background : var ( - - surface ) !important ; color : var ( - - text ) !important ; border-color : var ( - - border2 ) !important ; width : 34 px !important ; height : 34 px !important ; line-height : 34 px !important ; border-radius : var ( - - radius - md ) !important ; font-size : 18 px !important }
2026-07-05 06:41:32 +00:00
# hdr { position : fixed ; top : 0 ; left : 0 ; right : 0 ; z-index : 1000 ; background : rgba ( 7 , 12 , 20 , 0.96 ) ; backdrop-filter : blur ( 24 px ) saturate ( 200 % ) ; -webkit- backdrop-filter : blur ( 24 px ) saturate ( 200 % ) ; border-bottom : 1 px solid var ( - - border ) ; padding : calc ( var ( - - safe - top ) + 8 px ) 20 px 8 px }
# hdr-i { max-width : 1400 px ; margin : 0 auto ; display : flex ; align-items : center ; gap : 12 px ; flex-wrap : nowrap ; overflow : hidden }
# logo { font-size : 16 px ; font-weight : 800 ; letter-spacing : -0.5 px ; color : var ( - - text ) ; text-decoration : none ; white-space : nowrap }
# logo em { color : var ( - - accent ) ; font-style : normal }
. hdiv { width : 1 px ; height : 20 px ; background : var ( - - border2 ) }
# hdr-title { font-size : 13 px ; font-weight : 600 ; color : var ( - - text2 ) ; flex : 1 }
2026-07-07 07:11:50 +00:00
. hpill { font-size : 10 px ; font-weight : 700 ; letter-spacing : 0.6 px ; text-transform : uppercase ; border-radius : var ( - - radius - xl ) ; padding : 3 px 10 px ; white-space : nowrap }
2026-07-05 06:41:32 +00:00
. pill-free { background : rgba ( 34 , 197 , 94 , 0.12 ) ; color : #4ade80 ; border : 1 px solid rgba ( 34 , 197 , 94 , 0.25 ) }
. pill-live { background : rgba ( 14 , 165 , 233 , 0.15 ) ; color : var ( - - accent2 ) ; border : 1 px solid rgba ( 14 , 165 , 233 , 0.3 ) }
2026-07-07 07:11:50 +00:00
# hdr-cta { background : var ( - - accent ) ; color : white ; border : none ; border-radius : var ( - - radius - md ) ; padding : 7 px 16 px ; font-size : 12 px ; font-weight : 700 ; cursor : pointer ; white-space : nowrap ; text-decoration : none ; display : inline-block ; transition : opacity 0.2 s }
2026-07-05 06:41:32 +00:00
# hdr-cta : hover { opacity : 0.85 }
# sidebar { position : fixed ; top : calc ( var ( - - safe - top ) + 54 px ) ; left : 16 px ; z-index : 900 ; width : 220 px ; display : flex ; flex-direction : column ; gap : 6 px ; pointer-events : none }
2026-07-07 07:11:50 +00:00
. sbc { background : rgba ( 13 , 21 , 32 , 0.93 ) ; backdrop-filter : blur ( 16 px ) ; -webkit- backdrop-filter : blur ( 16 px ) ; border : 1 px solid var ( - - border ) ; border-radius : var ( - - radius - md ) ; padding : 12 px 14 px ; pointer-events : all }
2026-07-05 06:41:32 +00:00
. sbl { font-size : 10 px ; font-weight : 700 ; letter-spacing : 0.8 px ; text-transform : uppercase ; color : var ( - - text2 ) ; margin-bottom : 8 px }
. lr { display : flex ; align-items : center ; justify-content : space-between ; padding : 5 px 0 ; border-bottom : 1 px solid var ( - - border ) ; cursor : pointer }
. lr : last-child { border-bottom : none }
. ll { display : flex ; align-items : center ; gap : 8 px ; font-size : 12 px ; font-weight : 500 ; color : var ( - - text ) }
2026-07-07 07:11:50 +00:00
. ld2 { width : 9 px ; height : 9 px ; border-radius : var ( - - radius - full ) ; flex-shrink : 0 }
. tog { width : 32 px ; height : 18 px ; border-radius : var ( - - radius - md ) ; position : relative ; transition : background 0.2 s ; flex-shrink : 0 }
2026-07-05 06:41:32 +00:00
. tog . on { background : var ( - - accent ) }
. tog . off { background : var ( - - surface3 ) }
2026-07-07 07:11:50 +00:00
. tog :: after { content : '' ; position : absolute ; top : 3 px ; left : 3 px ; width : 12 px ; height : 12 px ; border-radius : var ( - - radius - full ) ; background : white ; transition : transform 0.2 s }
2026-07-05 06:41:32 +00:00
. tog . on :: after { transform : translateX ( 14 px ) }
. ig { display : flex ; flex-direction : column ; gap : 3 px }
2026-07-07 07:11:50 +00:00
. ir { display : flex ; align-items : center ; justify-content : space-between ; padding : 5 px 6 px ; border-radius : var ( - - radius - sm ) ; cursor : pointer ; transition : background 0.15 s }
2026-07-05 06:41:32 +00:00
. ir : hover { background : var ( - - surface3 ) }
. ir . active { background : rgba ( 14 , 165 , 233 , 0.12 ) ; border : 1 px solid rgba ( 14 , 165 , 233 , 0.2 ) }
. in { font-size : 11 px ; color : var ( - - text ) ; font-weight : 500 }
2026-07-07 07:11:50 +00:00
. ib { font-size : 10 px ; font-weight : 700 ; border-radius : var ( - - radius - sm ) ; padding : 2 px 6 px }
2026-07-05 06:41:32 +00:00
. 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 ( 20 px ) ; -webkit- backdrop-filter : blur ( 20 px ) ; border-top : 1 px solid var ( - - border ) ; padding : 8 px 20 px calc ( var ( - - safe - bottom ) + 8 px ) }
# bot-i { max-width : 1400 px ; margin : 0 auto ; display : flex ; align-items : center ; gap : 12 px ; flex-wrap : nowrap ; overflow : hidden }
# leg { display : flex ; gap : 10 px ; flex-wrap : wrap ; flex : 1 }
. li { display : flex ; align-items : center ; gap : 5 px ; font-size : 11 px ; color : var ( - - text2 ) }
2026-07-07 07:11:50 +00:00
. ld { width : 10 px ; height : 10 px ; border-radius : var ( - - radius - sm ) ; flex-shrink : 0 }
2026-07-05 06:41:32 +00:00
# disc { font-size : 10 px ; color : var ( - - text3 ) ; line-height : 1.4 ; max-width : 380 px }
. leaflet-popup-pane { z-index : 2000 !important }
2026-07-07 07:11:50 +00:00
. leaflet-popup-content-wrapper { background : #ffffff !important ; border : none !important ; border-radius : var ( - - radius - lg ) !important ; box-shadow : 0 8 px 40 px rgba ( 0 , 0 , 0 , 0.18 ) !important ; padding : 0 !important ; max-height : 65 vh !important ; overflow-y : auto !important }
2026-07-05 06:41:32 +00:00
. leaflet-popup-content { margin : 0 !important ; width : 280 px !important ; color : #0A0A0A !important ; max-height : 60 vh !important ; overflow-y : auto !important }
. leaflet-popup-tip { background : #ffffff !important }
. leaflet-popup-close-button { color : rgba ( 28 , 28 , 30 , 0.45 ) !important ; top : 10 px !important ; right : 12 px !important ; font-size : 18 px !important }
. zpop { padding : 16 px 18 px ; font-family : - apple-system , BlinkMacSystemFont , 'Segoe UI' , sans-serif }
. zcity { font-size : 15 px ; font-weight : 800 ; color : #0A0A0A ; letter-spacing : -0.3 px }
. zcountry { font-size : 12 px ; color : rgba ( 28 , 28 , 30 , 0.55 ) ; font-weight : 400 }
2026-07-07 07:11:50 +00:00
. zconf { font-size : 10 px ; font-weight : 600 ; background : #f2f2f7 ; border-radius : var ( - - radius - sm ) ; padding : 2 px 7 px ; color : rgba ( 28 , 28 , 30 , 0.6 ) ; display : inline-block ; margin : 4 px 0 10 px }
2026-07-05 06:41:32 +00:00
. zsect { font-size : 10 px ; font-weight : 700 ; letter-spacing : 0.7 px ; text-transform : uppercase ; color : var ( - - text2 ) ; margin : 8 px 0 5 px }
. zindices { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 5 px ; margin-bottom : 8 px }
2026-07-07 07:11:50 +00:00
. zidx { background : #f2f2f7 ; border-radius : var ( - - radius - md ) ; padding : 7 px 9 px }
2026-07-05 06:41:32 +00:00
. zil { font-size : 10 px ; color : rgba ( 28 , 28 , 30 , 0.55 ) ; font-weight : 500 ; margin-bottom : 3 px }
2026-07-07 07:11:50 +00:00
. zbar { height : 3 px ; border-radius : var ( - - radius - sm ) ; background : rgba ( 0 , 0 , 0 , 0.08 ) ; margin-bottom : 3 px }
. zfill { height : 100 % ; border-radius : var ( - - radius - sm ) }
2026-07-05 06:41:32 +00:00
. zval { font-size : 13 px ; font-weight : 800 ; color : #0A0A0A }
2026-07-07 07:11:50 +00:00
. zcontra { border-radius : var ( - - radius - md ) ; padding : 8 px 11 px ; font-size : 11 px ; line-height : 1.5 ; margin-bottom : 8 px }
2026-07-05 06:41:32 +00:00
. zcontra . low { background : rgba ( 34 , 197 , 94 , 0.1 ) ; color : #86efac ; border : 1 px solid rgba ( 34 , 197 , 94 , 0.18 ) }
. zcontra . medium { background : rgba ( 249 , 115 , 22 , 0.08 ) ; color : #92400e ; border : 1 px solid rgba ( 249 , 115 , 22 , 0.2 ) }
. zcontra . high { background : rgba ( 239 , 68 , 68 , 0.08 ) ; color : #991b1b ; border : 1 px solid rgba ( 239 , 68 , 68 , 0.2 ) }
. zcl { font-weight : 800 ; display : block ; margin-bottom : 2 px ; font-size : 11 px ; color : inherit }
. zage { font-size : 11 px ; color : #16a34a ; font-weight : 600 ; margin-bottom : 10 px }
. lock-wrap { position : relative ; margin-bottom : 10 px }
. lock-blur { filter : blur ( 3 px ) ; pointer-events : none ; opacity : 0.5 }
2026-07-07 07:11:50 +00:00
. lock-over { position : absolute ; inset : 0 ; display : flex ; flex-direction : column ; align-items : center ; justify-content : center ; gap : 3 px ; background : rgba ( 7 , 12 , 20 , 0.75 ) ; border-radius : var ( - - radius - md ) ; border : 1 px solid var ( - - border2 ) }
2026-07-05 06:41:32 +00:00
. lock-over span : first-child { font-size : 16 px }
. lock-over span : last-child { font-size : 11 px ; color : var ( - - text2 ) ; font-weight : 600 }
2026-07-07 07:11:50 +00:00
. zcta { display : block ; width : 100 % ; background : #007aff ; color : white ; border : none ; border-radius : var ( - - radius - md ) ; padding : 10 px ; font-size : 12 px ; font-weight : 700 ; cursor : pointer ; text-align : center ; text-decoration : none ; transition : opacity 0.2 s ; margin-top : 2 px }
2026-07-05 06:41:32 +00:00
. zcta : hover { opacity : 0.85 }
. zdisc { font-size : 10 px ; color : rgba ( 28 , 28 , 30 , 0.35 ) ; margin-top : 8 px ; 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 : 6 px ; flex-wrap : nowrap ; overflow : hidden } # hdr-cta { padding : 6 px 12 px ; font-size : 11 px ; flex-shrink : 0 } . hpill { font-size : 9 px ; padding : 2 px 7 px } . 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 : 1 px solid rgba ( 0 , 0 , 0 , 0.1 ) ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - lg ) 16 px 0 0 ;
2026-07-05 06:41:32 +00:00
box-shadow : 0 -4 px 24 px rgba ( 0 , 0 , 0 , 0.15 ) ;
max-height : 72 vh ; overflow-y : auto ;
-webkit- overflow-scrolling : touch ;
transform : translateY ( 100 % ) ;
transition : transform 0.28 s cubic-bezier ( 0.32 , 0.72 , 0 , 1 ) ;
}
# bsheet . open { display : block ; transform : translateY ( 0 ) }
2026-07-07 07:11:50 +00:00
# bsheet-handle { width : 36 px ; height : 4 px ; border-radius : var ( - - radius - sm ) ; background : rgba ( 0 , 0 , 0 , 0.15 ) ; margin : 10 px auto 0 }
# bsheet-close { position : absolute ; top : 10 px ; right : 12 px ; background : rgba ( 0 , 0 , 0 , 0.06 ) ; border : none ; color : #0A0A0A ; font-size : 16 px ; width : 28 px ; height : 28 px ; border-radius : var ( - - radius - full ) ; cursor : pointer ; display : flex ; align-items : center ; justify-content : center }
2026-07-05 06:41:32 +00:00
# bsheet-content { padding : 8 px 0 env ( safe - area - inset - bottom , 16 px ) }
# 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 : 14 px 16 px }
# 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 : #1d1d1f fff !important }
< / style >
< style >
@ media ( max-width : 640px ) {
. hero h1 { font-size : 2 rem !important ; }
. hero p { font-size : 1 rem !important ; }
. card { padding : 1.5 rem !important ; }
. grid { grid-template-columns : 1 fr !important ; }
. btn { padding : 0.75 rem 1.5 rem !important ; font-size : 1 rem !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()" > × < / 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>
2026-07-07 07:11:50 +00:00
${ 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">
2026-07-05 06:41:32 +00:00
<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">
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
${ 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>
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<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>
2026-07-05 06:41:32 +00:00
</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 -->
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:12px;margin-bottom:8px">
2026-07-05 06:41:32 +00:00
<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 -->
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:8px 10px;margin-bottom:6px">
2026-07-05 06:41:32 +00:00
<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 -->
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:8px 10px;margin-bottom:6px">
2026-07-05 06:41:32 +00:00
<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' ) }
2026-07-07 07:11:50 +00:00
${ 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> ` : '' }
2026-07-05 06:41:32 +00:00
</div>
<!-- Society -->
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:8px 10px;margin-bottom:6px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
${ 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> ` : '' }
2026-07-05 06:41:32 +00:00
<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 ?
2026-07-07 07:11:50 +00:00
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 ( ' ' ) : '' ;
2026-07-05 06:41:32 +00:00
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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:12px;margin-bottom:8px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<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>
2026-07-05 06:41:32 +00:00
</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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:8px 10px;margin-bottom:6px">
2026-07-05 06:41:32 +00:00
<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">
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:6px 8px;text-align:center">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:6px 8px;text-align:center">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:6px 8px;text-align:center">
2026-07-05 06:41:32 +00:00
<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 ? `
2026-07-07 07:11:50 +00:00
<div style="background:rgba(220,38,38,0.06);border-radius: var(--radius-sm);padding:8px 10px;margin-bottom:6px;font-size:10px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<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>
2026-07-05 06:41:32 +00:00
</div>
</div> ` : '' }
2026-07-07 07:11:50 +00:00
${ 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> ` : '' }
2026-07-05 06:41:32 +00:00
${ 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' ;
2026-07-07 07:11:50 +00:00
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> ` ;
2026-07-05 06:41:32 +00:00
} ;
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">
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<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">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="display:flex;height:8px;border-radius: var(--radius-sm);overflow:hidden;margin-bottom:4px">
2026-07-05 06:41:32 +00:00
${ 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">
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">LGBTQ+ Index</div>
2026-07-07 07:11:50 +00:00
<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>
2026-07-05 06:41:32 +00:00
</div>
<div style="font-size:13px;font-weight:800;color: ${ lgbtq _col } "> ${ s . lgbtq _index } /100</div>
</div>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<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>
2026-07-05 06:41:32 +00:00
${ 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> ` : '' }
2026-07-07 07:11:50 +00:00
${ 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> ` : '' }
2026-07-05 06:41:32 +00:00
</div>
2026-07-07 07:11:50 +00:00
${ 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> ` : '' }
2026-07-05 06:41:32 +00:00
<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' ;
2026-07-07 07:11:50 +00:00
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> ` ;
2026-07-05 06:41:32 +00:00
} ;
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
2026-07-07 07:11:50 +00:00
? ` <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> ` : '' ) ;
2026-07-05 06:41:32 +00:00
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">
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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>
2026-07-07 07:11:50 +00:00
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
2026-07-05 06:41:32 +00:00
<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';
2026-07-07 07:11:50 +00:00
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 < / d i v > < d i v s t y l e = " f o n t - s i z e : 1 1 p x ; f o n t - w e i g h t : 8 0 0 ; c o l o r : $ { c o l } " > $ { s t a t u s } < / d i v > < / d i v > ` + l c s P o p u p ( p ) + f r i c t i o n P o p u p ( p ) + l e g a l P o p u p ( p ) + s o c i a l P o p u p ( p ) + m e n t a l H e a l t h P o p u p ( p ) + f o o d P o p u p ( p ) + ( p . s p e e d _ s o u r c e ? ` < d i v s t y l e = " f o n t - s i z e : 9 p x ; c o l o r : r g b a ( 2 8 , 2 8 , 3 0 , 0 . 3 ) ; m a r g i n - t o p : 5 p x " > S p e e d : $ { p . s p e e d _ s o u r c e } < / d i v > ` : ' ' ) ;
2026-07-05 06:41:32 +00:00
} ) ( ) }
< / d i v > ` ;
}
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>
2026-07-07 07:11:50 +00:00
${ [ '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>' : '' }
2026-07-05 06:41:32 +00:00
</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>
2026-07-07 07:11:50 +00:00
<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>
2026-07-05 06:41:32 +00:00
<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">🔒</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 > < / d i v >
< / d i v > ` ;
}
let zg = L . layerGroup ( ) . addTo ( map ) , cg = L . layerGroup ( ) . addTo ( map ) ;
let ls = { zones : true , contra : true } ;
2026-07-07 07:11:50 +00:00
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 ) ; } ) ; }
2026-07-05 06:41:32 +00:00
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 >