feat: Passwordless cross-device authentication

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

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+76 -76
View File
@@ -17,61 +17,61 @@
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,sans-serif;background:var(--bg);color:var(--text);overflow:hidden}
#map{position:fixed;inset:0;z-index:1}
.leaflet-control-zoom{margin-top:calc(var(--safe-top) + 72px) !important;margin-left:16px !important}
.leaflet-control-zoom a{background:var(--surface) !important;color:var(--text) !important;border-color:var(--border2) !important;width:34px !important;height:34px !important;line-height:34px !important;border-radius:8px !important;font-size:18px !important}
.leaflet-control-zoom a{background:var(--surface) !important;color:var(--text) !important;border-color:var(--border2) !important;width:34px !important;height:34px !important;line-height:34px !important;border-radius: var(--radius-md) !important;font-size:18px !important}
#hdr{position:fixed;top:0;left:0;right:0;z-index:1000;background:rgba(7,12,20,0.96);backdrop-filter:blur(24px) saturate(200%);-webkit-backdrop-filter:blur(24px) saturate(200%);border-bottom:1px solid var(--border);padding:calc(var(--safe-top) + 8px) 20px 8px}
#hdr-i{max-width:1400px;margin:0 auto;display:flex;align-items:center;gap:12px;flex-wrap:nowrap;overflow:hidden}
#logo{font-size:16px;font-weight:800;letter-spacing:-0.5px;color:var(--text);text-decoration:none;white-space:nowrap}
#logo em{color:var(--accent);font-style:normal}
.hdiv{width:1px;height:20px;background:var(--border2)}
#hdr-title{font-size:13px;font-weight:600;color:var(--text2);flex:1}
.hpill{font-size:10px;font-weight:700;letter-spacing:0.6px;text-transform:uppercase;border-radius:20px;padding:3px 10px;white-space:nowrap}
.hpill{font-size:10px;font-weight:700;letter-spacing:0.6px;text-transform:uppercase;border-radius: var(--radius-xl);padding:3px 10px;white-space:nowrap}
.pill-free{background:rgba(34,197,94,0.12);color:#4ade80;border:1px solid rgba(34,197,94,0.25)}
.pill-live{background:rgba(14,165,233,0.15);color:var(--accent2);border:1px solid rgba(14,165,233,0.3)}
#hdr-cta{background:var(--accent);color:white;border:none;border-radius:8px;padding:7px 16px;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap;text-decoration:none;display:inline-block;transition:opacity 0.2s}
#hdr-cta{background:var(--accent);color:white;border:none;border-radius: var(--radius-md);padding:7px 16px;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap;text-decoration:none;display:inline-block;transition:opacity 0.2s}
#hdr-cta:hover{opacity:0.85}
#sidebar{position:fixed;top:calc(var(--safe-top) + 54px);left:16px;z-index:900;width:220px;display:flex;flex-direction:column;gap:6px;pointer-events:none}
.sbc{background:rgba(13,21,32,0.93);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:12px;padding:12px 14px;pointer-events:all}
.sbc{background:rgba(13,21,32,0.93);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid var(--border);border-radius: var(--radius-md);padding:12px 14px;pointer-events:all}
.sbl{font-size:10px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:var(--text2);margin-bottom:8px}
.lr{display:flex;align-items:center;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border);cursor:pointer}
.lr:last-child{border-bottom:none}
.ll{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:500;color:var(--text)}
.ld2{width:9px;height:9px;border-radius:50%;flex-shrink:0}
.tog{width:32px;height:18px;border-radius:9px;position:relative;transition:background 0.2s;flex-shrink:0}
.ld2{width:9px;height:9px;border-radius: var(--radius-full);flex-shrink:0}
.tog{width:32px;height:18px;border-radius: var(--radius-md);position:relative;transition:background 0.2s;flex-shrink:0}
.tog.on{background:var(--accent)}
.tog.off{background:var(--surface3)}
.tog::after{content:'';position:absolute;top:3px;left:3px;width:12px;height:12px;border-radius:50%;background:white;transition:transform 0.2s}
.tog::after{content:'';position:absolute;top:3px;left:3px;width:12px;height:12px;border-radius: var(--radius-full);background:white;transition:transform 0.2s}
.tog.on::after{transform:translateX(14px)}
.ig{display:flex;flex-direction:column;gap:3px}
.ir{display:flex;align-items:center;justify-content:space-between;padding:5px 6px;border-radius:7px;cursor:pointer;transition:background 0.15s}
.ir{display:flex;align-items:center;justify-content:space-between;padding:5px 6px;border-radius: var(--radius-sm);cursor:pointer;transition:background 0.15s}
.ir:hover{background:var(--surface3)}
.ir.active{background:rgba(14,165,233,0.12);border:1px solid rgba(14,165,233,0.2)}
.in{font-size:11px;color:var(--text);font-weight:500}
.ib{font-size:10px;font-weight:700;border-radius:5px;padding:2px 6px}
.ib{font-size:10px;font-weight:700;border-radius: var(--radius-sm);padding:2px 6px}
.ib-free{background:rgba(34,197,94,0.12);color:#4ade80}
.ib-paid{background:rgba(245,158,11,0.12);color:#fbbf24}
#bot{position:fixed;bottom:0;left:0;right:0;z-index:900;background:rgba(7,12,20,0.96);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-top:1px solid var(--border);padding:8px 20px calc(var(--safe-bottom) + 8px)}
#bot-i{max-width:1400px;margin:0 auto;display:flex;align-items:center;gap:12px;flex-wrap:nowrap;overflow:hidden}
#leg{display:flex;gap:10px;flex-wrap:wrap;flex:1}
.li{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--text2)}
.ld{width:10px;height:10px;border-radius:2px;flex-shrink:0}
.ld{width:10px;height:10px;border-radius: var(--radius-sm);flex-shrink:0}
#disc{font-size:10px;color:var(--text3);line-height:1.4;max-width:380px}
.leaflet-popup-pane{z-index:2000 !important}
.leaflet-popup-content-wrapper{background:#ffffff !important;border:none !important;border-radius:16px !important;box-shadow:0 8px 40px rgba(0,0,0,0.18) !important;padding:0 !important;max-height:65vh !important;overflow-y:auto !important}
.leaflet-popup-content-wrapper{background:#ffffff !important;border:none !important;border-radius: var(--radius-lg) !important;box-shadow:0 8px 40px rgba(0,0,0,0.18) !important;padding:0 !important;max-height:65vh !important;overflow-y:auto !important}
.leaflet-popup-content{margin:0 !important;width:280px !important;color:#0A0A0A !important;max-height:60vh !important;overflow-y:auto !important}
.leaflet-popup-tip{background:#ffffff !important}
.leaflet-popup-close-button{color:rgba(28,28,30,0.45) !important;top:10px !important;right:12px !important;font-size:18px !important}
.zpop{padding:16px 18px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
.zcity{font-size:15px;font-weight:800;color:#0A0A0A;letter-spacing:-0.3px}
.zcountry{font-size:12px;color:rgba(28,28,30,0.55);font-weight:400}
.zconf{font-size:10px;font-weight:600;background:#f2f2f7;border-radius:5px;padding:2px 7px;color:rgba(28,28,30,0.6);display:inline-block;margin:4px 0 10px}
.zconf{font-size:10px;font-weight:600;background:#f2f2f7;border-radius: var(--radius-sm);padding:2px 7px;color:rgba(28,28,30,0.6);display:inline-block;margin:4px 0 10px}
.zsect{font-size:10px;font-weight:700;letter-spacing:0.7px;text-transform:uppercase;color:var(--text2);margin:8px 0 5px}
.zindices{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:8px}
.zidx{background:#f2f2f7;border-radius:8px;padding:7px 9px}
.zidx{background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px}
.zil{font-size:10px;color:rgba(28,28,30,0.55);font-weight:500;margin-bottom:3px}
.zbar{height:3px;border-radius:2px;background:rgba(0,0,0,0.08);margin-bottom:3px}
.zfill{height:100%;border-radius:2px}
.zbar{height:3px;border-radius: var(--radius-sm);background:rgba(0,0,0,0.08);margin-bottom:3px}
.zfill{height:100%;border-radius: var(--radius-sm)}
.zval{font-size:13px;font-weight:800;color:#0A0A0A}
.zcontra{border-radius:8px;padding:8px 11px;font-size:11px;line-height:1.5;margin-bottom:8px}
.zcontra{border-radius: var(--radius-md);padding:8px 11px;font-size:11px;line-height:1.5;margin-bottom:8px}
.zcontra.low{background:rgba(34,197,94,0.1);color:#86efac;border:1px solid rgba(34,197,94,0.18)}
.zcontra.medium{background:rgba(249,115,22,0.08);color:#92400e;border:1px solid rgba(249,115,22,0.2)}
.zcontra.high{background:rgba(239,68,68,0.08);color:#991b1b;border:1px solid rgba(239,68,68,0.2)}
@@ -79,10 +79,10 @@
.zage{font-size:11px;color:#16a34a;font-weight:600;margin-bottom:10px}
.lock-wrap{position:relative;margin-bottom:10px}
.lock-blur{filter:blur(3px);pointer-events:none;opacity:0.5}
.lock-over{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;background:rgba(7,12,20,0.75);border-radius:8px;border:1px solid var(--border2)}
.lock-over{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;background:rgba(7,12,20,0.75);border-radius: var(--radius-md);border:1px solid var(--border2)}
.lock-over span:first-child{font-size:16px}
.lock-over span:last-child{font-size:11px;color:var(--text2);font-weight:600}
.zcta{display:block;width:100%;background:#007aff;color:white;border:none;border-radius:9px;padding:10px;font-size:12px;font-weight:700;cursor:pointer;text-align:center;text-decoration:none;transition:opacity 0.2s;margin-top:2px}
.zcta{display:block;width:100%;background:#007aff;color:white;border:none;border-radius: var(--radius-md);padding:10px;font-size:12px;font-weight:700;cursor:pointer;text-align:center;text-decoration:none;transition:opacity 0.2s;margin-top:2px}
.zcta:hover{opacity:0.85}
.zdisc{font-size:10px;color:rgba(28,28,30,0.35);margin-top:8px;text-align:center;line-height:1.4}
.zdisc a{color:#007aff;text-decoration:none}
@@ -93,7 +93,7 @@
#bsheet{
display:none;position:fixed;left:0;right:0;bottom:0;z-index:9999;
background:#ffffff;border-top:1px solid rgba(0,0,0,0.1);
border-radius:16px 16px 0 0;
border-radius: var(--radius-lg) 16px 0 0;
box-shadow:0 -4px 24px rgba(0,0,0,0.15);
max-height:72vh;overflow-y:auto;
-webkit-overflow-scrolling:touch;
@@ -101,8 +101,8 @@
transition:transform 0.28s cubic-bezier(0.32,0.72,0,1);
}
#bsheet.open{display:block;transform:translateY(0)}
#bsheet-handle{width:36px;height:4px;border-radius:2px;background:rgba(0,0,0,0.15);margin:10px auto 0}
#bsheet-close{position:absolute;top:10px;right:12px;background:rgba(0,0,0,0.06);border:none;color:#0A0A0A;font-size:16px;width:28px;height:28px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center}
#bsheet-handle{width:36px;height:4px;border-radius: var(--radius-sm);background:rgba(0,0,0,0.15);margin:10px auto 0}
#bsheet-close{position:absolute;top:10px;right:12px;background:rgba(0,0,0,0.06);border:none;color:#0A0A0A;font-size:16px;width:28px;height:28px;border-radius: var(--radius-full);cursor:pointer;display:flex;align-items:center;justify-content:center}
#bsheet-content{padding:8px 0 env(safe-area-inset-bottom,16px)}
#bsheet-backdrop{display:none;position:fixed;inset:0;z-index:9998;background:rgba(0,0,0,0.3)}
#bsheet-backdrop.open{display:block}
@@ -385,7 +385,7 @@
const heeScore = l && l.hee_score ? l.hee_score : null;
return `<div style="border-top:1px solid rgba(0,0,0,0.08);margin-top:10px;padding-top:10px">
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.4);margin-bottom:8px">FOOD ENVIRONMENT & HUMAN ENERGY</div>
${heeScore ? `<div style="background:#f2f2f7;border-radius:10px;padding:10px 12px;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between">
${heeScore ? `<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:10px 12px;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between">
<div>
<div style="font-size:9px;color:rgba(28,28,30,0.45);margin-bottom:2px">Human Energy Efficiency</div>
<div style="font-size:9px;color:rgba(28,28,30,0.35)">LCS adjusted for food environment</div>
@@ -393,26 +393,26 @@
<div style="font-size:32px;font-weight:900;color:${heeScore>=78?'#16a34a':heeScore>=62?'#ca8a04':'#dc2626'};font-family:monospace">${heeScore}</div>
</div>` : ''}
<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:8px">
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:2px">Food env. score</div>
<div style="font-size:15px;font-weight:800;color:${scoreCol}">${f.food_environment_score}/100</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:2px">Ultra-processed</div>
<div style="font-size:15px;font-weight:800;color:${upfCol}">${f.ultra_processed_pct}%</div>
<div style="font-size:8px;color:rgba(28,28,30,0.35)">of calories</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:2px">Obesity rate</div>
<div style="font-size:15px;font-weight:800;color:${obeseCol}">${f.obesity_pct}%</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:2px">Food/income</div>
<div style="font-size:15px;font-weight:800;color:${foodIncCol}">${f.food_income_pct}%</div>
</div>
</div>
${f.contradiction ? `<div style="background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.2);border-radius:7px;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:7px;padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.6);line-height:1.5;margin-bottom:5px">${f.note}</div>
${f.contradiction ? `<div style="background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.2);border-radius: var(--radius-sm);padding:7px 9px;font-size:10px;color:#92400e;line-height:1.4;margin-bottom:6px"><span style="font-weight:800">Food contradiction: </span>${f.contradiction}</div>` : ''}
<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.6);line-height:1.5;margin-bottom:5px">${f.note}</div>
<div style="font-size:9px;color:rgba(28,28,30,0.3)">Source: ${f.source||'WHO / NOVA 2024'}</div>
</div>`;
}
@@ -425,8 +425,8 @@
const dimBar = (val, label, col) => `
<div style="display:flex;align-items:center;gap:6px;margin-bottom:5px">
<span style="width:72px;font-size:10px;color:rgba(28,28,30,0.55);flex-shrink:0">${label}</span>
<div style="flex:1;height:6px;background:rgba(0,0,0,0.08);border-radius:3px">
<div style="width:${val}%;height:100%;background:${col};border-radius:3px"></div>
<div style="flex:1;height:6px;background:rgba(0,0,0,0.08);border-radius: 3px">
<div style="width:${val}%;height:100%;background:${col};border-radius: 3px"></div>
</div>
<span style="font-size:11px;font-weight:800;color:${col};width:28px;text-align:right">${val}</span>
</div>`;
@@ -443,7 +443,7 @@
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.4);margin-bottom:8px">LANDVEX CIVILIZATIONAL SCORE</div>
<!-- Main score -->
<div style="background:#f2f2f7;border-radius:12px;padding:12px;margin-bottom:8px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:12px;margin-bottom:8px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<div>
<div style="font-size:36px;font-weight:900;color:${scoreCol};line-height:1;font-family:monospace">${l.lcs_score}</div>
@@ -459,7 +459,7 @@
</div>
<!-- Vitality stats -->
<div style="background:#f2f2f7;border-radius:8px;padding:8px 10px;margin-bottom:6px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:8px 10px;margin-bottom:6px">
<div style="font-size:9px;font-weight:700;color:rgba(28,28,30,0.4);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:5px">Vitality</div>
${statRow('Life expectancy', l.life_expectancy, ' yrs')}
${statRow('Healthy years', l.healthy_years, ' yrs')}
@@ -470,15 +470,15 @@
</div>
<!-- Demography -->
<div style="background:#f2f2f7;border-radius:8px;padding:8px 10px;margin-bottom:6px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:8px 10px;margin-bottom:6px">
<div style="font-size:9px;font-weight:700;color:rgba(28,28,30,0.4);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:5px">Demographics</div>
${statRow('Fertility rate', l.fertility_rate, ' children/woman', fertCol)}
${statRow('Median age', l.median_age, ' yrs')}
${l.fertility_rate < 1.4 ? `<div style="background:rgba(220,38,38,0.08);border-radius:5px;padding:4px 7px;font-size:9px;color:#991b1b;font-weight:600;margin-top:4px">Demographic warning: below replacement level</div>` : ''}
${l.fertility_rate < 1.4 ? `<div style="background:rgba(220,38,38,0.08);border-radius: var(--radius-sm);padding:4px 7px;font-size:9px;color:#991b1b;font-weight:600;margin-top:4px">Demographic warning: below replacement level</div>` : ''}
</div>
<!-- Society -->
<div style="background:#f2f2f7;border-radius:8px;padding:8px 10px;margin-bottom:6px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:8px 10px;margin-bottom:6px">
<div style="font-size:9px;font-weight:700;color:rgba(28,28,30,0.4);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:5px">Society</div>
${statRow('Homicide rate', l.homicide_per100k, '/100k', l.homicide_per100k < 1 ? '#16a34a' : l.homicide_per100k < 3 ? '#ca8a04' : '#dc2626')}
${statRow('Prison rate', l.prison_per100k, '/100k')}
@@ -488,7 +488,7 @@
${statRow('Housing/income ratio', l.housing_income_ratio + 'x', '', l.housing_income_ratio < 10 ? '#16a34a' : l.housing_income_ratio < 15 ? '#ca8a04' : '#dc2626')}
</div>
${l.contradiction_note ? `<div style="background:rgba(245,158,11,0.07);border-left:3px solid #f59e0b;border-radius:0 7px 7px 0;padding:8px 10px;margin-bottom:6px;font-size:10px;color:rgba(28,28,30,0.65);line-height:1.5"><span style="font-weight:800;color:#92400e">Landvex Contradiction: </span>${l.contradiction_note}</div>` : ''}
${l.contradiction_note ? `<div style="background:rgba(245,158,11,0.07);border-left:3px solid #f59e0b;border-radius: 0 7px 7px 0;padding:8px 10px;margin-bottom:6px;font-size:10px;color:rgba(28,28,30,0.65);line-height:1.5"><span style="font-weight:800;color:#92400e">Landvex Contradiction: </span>${l.contradiction_note}</div>` : ''}
<div style="font-size:9px;color:rgba(28,28,30,0.3)">Source: ${l.source||'WHO / OECD / World Bank 2024'}</div>
</div>`;
}
@@ -511,11 +511,11 @@
</div>` : '';
const tagList = (items, col) => items && items.length ?
items.map(t=>`<span style="background:${col}15;color:${col};border-radius:4px;padding:2px 6px;font-size:9px;font-weight:600">${t}</span>`).join(' ') : '';
items.map(t=>`<span style="background:${col}15;color:${col};border-radius: var(--radius-sm);padding:2px 6px;font-size:9px;font-weight:600">${t}</span>`).join(' ') : '';
return `<div style="border-top:1px solid rgba(0,0,0,0.08);margin-top:10px;padding-top:10px">
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.4);margin-bottom:8px">LANDVEX FRICTION SCORE</div>
<div style="background:#f2f2f7;border-radius:10px;padding:12px;margin-bottom:8px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:12px;margin-bottom:8px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
<div>
<div style="font-size:28px;font-weight:900;color:${col};line-height:1;font-family:monospace">${score}</div>
@@ -526,14 +526,14 @@
<div style="font-size:9px;color:rgba(28,28,30,0.4);margin-top:2px">${pct}% efficiency vs max friction</div>
</div>
</div>
<div style="height:8px;background:rgba(0,0,0,0.08);border-radius:4px;overflow:hidden">
<div style="width:${score}%;height:100%;background:${col};border-radius:4px"></div>
<div style="height:8px;background:rgba(0,0,0,0.08);border-radius: var(--radius-sm);overflow:hidden">
<div style="width:${score}%;height:100%;background:${col};border-radius: 4px"></div>
</div>
<div style="display:flex;justify-content:space-between;font-size:8px;color:rgba(28,28,30,0.3);margin-top:3px">
<span>0 — No friction</span><span>100 — Extreme</span>
</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:8px 10px;margin-bottom:6px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:8px 10px;margin-bottom:6px">
<div style="font-size:9px;font-weight:700;color:rgba(28,28,30,0.4);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px">Key metrics</div>
${row('Company start', co.company_start_days, ' days')}
${row('Tax hours/year', co.tax_hours_per_year, ' hrs')}
@@ -545,32 +545,32 @@
${f.weaknesses ? `<div style="margin-bottom:8px"><div style="font-size:9px;color:rgba(28,28,30,0.4);margin-bottom:4px;font-weight:600">FRICTION POINTS</div><div style="display:flex;flex-wrap:wrap;gap:4px">${tagList(f.weaknesses,'#dc2626')}</div></div>` : ''}
${f.entry_friction !== undefined ? `
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;margin-top:8px;margin-bottom:6px">
<div style="background:#f2f2f7;border-radius:7px;padding:6px 8px;text-align:center">
<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:6px 8px;text-align:center">
<div style="font-size:8px;color:rgba(28,28,30,0.45);margin-bottom:2px">Entry</div>
<div style="font-size:14px;font-weight:800;color:${f.entry_friction<=20?'#16a34a':f.entry_friction<=40?'#ca8a04':'#dc2626'}">${f.entry_friction}</div>
</div>
<div style="background:#f2f2f7;border-radius:7px;padding:6px 8px;text-align:center">
<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:6px 8px;text-align:center">
<div style="font-size:8px;color:rgba(28,28,30,0.45);margin-bottom:2px">Operating</div>
<div style="font-size:14px;font-weight:800;color:${f.operating_friction<=25?'#16a34a':f.operating_friction<=50?'#ca8a04':'#dc2626'}">${f.operating_friction}</div>
</div>
<div style="background:#f2f2f7;border-radius:7px;padding:6px 8px;text-align:center">
<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:6px 8px;text-align:center">
<div style="font-size:8px;color:rgba(28,28,30,0.45);margin-bottom:2px">Scale adv.</div>
<div style="font-size:14px;font-weight:800;color:${f.scale_advantage<=30?'#16a34a':f.scale_advantage<=55?'#ca8a04':'#dc2626'}">${f.scale_advantage}</div>
</div>
</div>` : ''}
${f.sme_burden_pct !== undefined ? `
<div style="background:rgba(220,38,38,0.06);border-radius:7px;padding:8px 10px;margin-bottom:6px;font-size:10px">
<div style="background:rgba(220,38,38,0.06);border-radius: var(--radius-sm);padding:8px 10px;margin-bottom:6px;font-size:10px">
<div style="font-weight:700;color:#991b1b;margin-bottom:4px">Compliance cost as % of turnover</div>
<div style="display:flex;justify-content:space-between">
<div><span style="color:rgba(28,28,30,0.5)">Micro-business</span><br><span style="font-size:16px;font-weight:900;color:#dc2626">${f.sme_burden_pct}%</span></div>
<div style="text-align:center;padding:0 8px;color:rgba(28,28,30,0.2);font-size:18px">vs</div>
<div style="text-align:right"><span style="color:rgba(28,28,30,0.5)">Large corp</span><br><span style="font-size:16px;font-weight:900;color:#16a34a">${f.large_co_burden_pct}%</span></div>
</div>
<div style="height:4px;background:rgba(0,0,0,0.06);border-radius:2px;margin-top:6px;overflow:hidden">
<div style="width:${Math.min(f.sme_burden_pct/30*100,100)}%;height:100%;background:#dc2626;border-radius:2px"></div>
<div style="height:4px;background:rgba(0,0,0,0.06);border-radius: var(--radius-sm);margin-top:6px;overflow:hidden">
<div style="width:${Math.min(f.sme_burden_pct/30*100,100)}%;height:100%;background:#dc2626;border-radius: 2px"></div>
</div>
</div>` : ''}
${f.glasskiosk_note ? `<div style="background:#f2f2f7;border-radius:7px;padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.6);line-height:1.5;margin-bottom:6px"><span style="font-weight:700">Example: </span>${f.glasskiosk_note}</div>` : ''}
${f.glasskiosk_note ? `<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.6);line-height:1.5;margin-bottom:6px"><span style="font-weight:700">Example: </span>${f.glasskiosk_note}</div>` : ''}
${f.elasticity ? `<div style="font-size:10px;color:rgba(28,28,30,0.55);margin-bottom:6px"><span style="font-weight:700">Regressivity: </span>${f.elasticity}</div>` : ''}
<div style="font-size:9px;color:rgba(28,28,30,0.3)">Source: ${f.source||'World Bank / OECD 2023-2024'}</div>
</div>`;
@@ -590,24 +590,24 @@
const bar = (v, max) => {
const pct = Math.round((v/max)*100);
const col = v/max >= 0.8 ? '#16a34a' : v/max >= 0.5 ? '#ca8a04' : '#dc2626';
return `<div style="flex:1;height:4px;border-radius:2px;background:rgba(0,0,0,0.08)"><div style="width:${pct}%;height:100%;background:${col};border-radius:2px"></div></div>`;
return `<div style="flex:1;height:4px;border-radius: var(--radius-sm);background:rgba(0,0,0,0.08)"><div style="width:${pct}%;height:100%;background:${col};border-radius: 2px"></div></div>`;
};
return `<div style="border-top:1px solid rgba(0,0,0,0.08);margin-top:10px;padding-top:10px">
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.4);margin-bottom:8px">MENTAL HEALTH INTELLIGENCE</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:8px">
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Depression rate</div>
<div style="font-size:13px;font-weight:800;color:#0A0A0A">${m.depression_pct}%</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Burnout rate</div>
<div style="font-size:13px;font-weight:800;color:#0A0A0A">${m.burnout_pct}%</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Suicide /100k</div>
<div style="font-size:13px;font-weight:800;color:${suicide_col}">${m.suicide_per100k}</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Stigma index</div>
<div style="font-size:13px;font-weight:800;color:${stigma_col}">${m.stigma_index}/100</div>
<div style="font-size:9px;color:rgba(28,28,30,0.35)">Low = accepting</div>
@@ -630,8 +630,8 @@
<span style="color:rgba(28,28,30,0.7);font-weight:700;font-size:11px;width:36px">${m.antidepressant_use_pct}%</span>
</div>
</div>
<div style="background:#f2f2f7;border-radius:7px;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:6px;padding:6px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.65);line-height:1.5;margin-bottom:5px">${m.therapy_access}</div>
<div style="display:flex;align-items:center;gap:6px;background:rgba(220,38,38,0.06);border-radius: var(--radius-sm);padding:6px 9px">
<span style="font-size:9px;font-weight:700;color:#dc2626;flex-shrink:0">Crisis line</span>
<span style="font-size:11px;font-weight:700;color:#0A0A0A">${m.crisis_line}</span>
</div>
@@ -655,7 +655,7 @@
const colors = ['#60a5fa','#34d399','#f59e0b','#a78bfa'];
religion_html = `<div style="margin-top:8px">
<div style="font-size:9px;color:rgba(28,28,30,0.45);margin-bottom:5px">Religion (population %)</div>
<div style="display:flex;height:8px;border-radius:4px;overflow:hidden;margin-bottom:4px">
<div style="display:flex;height:8px;border-radius: var(--radius-sm);overflow:hidden;margin-bottom:4px">
${entries.map((e,i)=>`<div style="width:${e[1]}%;background:${colors[i]};"></div>`).join('')}
</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;font-size:9px">
@@ -667,28 +667,28 @@
return `<div style="border-top:1px solid rgba(0,0,0,0.08);margin-top:10px;padding-top:10px">
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.4);margin-bottom:8px">SOCIAL INTELLIGENCE</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:8px">
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">LGBTQ+ Index</div>
<div style="height:4px;background:rgba(0,0,0,0.08);border-radius:2px;margin-bottom:3px">
<div style="width:${lgbtq_bar_pct}%;height:100%;background:${lgbtq_col};border-radius:2px"></div>
<div style="height:4px;background:rgba(0,0,0,0.08);border-radius: var(--radius-sm);margin-bottom:3px">
<div style="width:${lgbtq_bar_pct}%;height:100%;background:${lgbtq_col};border-radius: 2px"></div>
</div>
<div style="font-size:13px;font-weight:800;color:${lgbtq_col}">${s.lgbtq_index}/100</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Married %</div>
<div style="font-size:13px;font-weight:800;color:#0A0A0A">${s.married_pct}%</div>
<div style="font-size:9px;color:rgba(28,28,30,0.4)">Divorce ${s.divorce_rate}/1000</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Same-sex marriage</div>
<div style="font-size:10px;font-weight:700;color:${marriage_col};line-height:1.3">${s.gay_marriage}</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Foreign-born</div>
<div style="font-size:13px;font-weight:800;color:#0A0A0A">${s.foreign_born_pct}%</div>
</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.65);line-height:1.5;margin-bottom:6px">${s.lgbtq_note}</div>
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.65);line-height:1.5;margin-bottom:6px">${s.lgbtq_note}</div>
${religion_html}
<div style="font-size:9px;color:rgba(28,28,30,0.3);margin-top:5px">Sources: ${s.social_source||'ILGA World / Eurostat 2025'}</div>
</div>`;
@@ -729,9 +729,9 @@
<span style="width:72px;color:rgba(28,28,30,0.5);flex-shrink:0;font-size:10px;margin-top:1px">Prostitution</span>
<span style="color:#0A0A0A;font-weight:600;font-size:11px;line-height:1.3">${l.prostitution}</span>
</div>` : ''}
${l.prostitution_detail ? `<div style="background:#f2f2f7;border-radius:6px;padding:6px 8px;font-size:9px;color:rgba(28,28,30,0.6);line-height:1.4;margin-top:2px">${l.prostitution_detail}</div>` : ''}
${l.prostitution_detail ? `<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:6px 8px;font-size:9px;color:rgba(28,28,30,0.6);line-height:1.4;margin-top:2px">${l.prostitution_detail}</div>` : ''}
</div>
${l.notes ? `<div style="background:#f2f2f7;border-radius:7px;padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.65);line-height:1.5">${l.notes}</div>` : ''}
${l.notes ? `<div style="background:#f2f2f7;border-radius: var(--radius-sm);padding:7px 9px;font-size:10px;color:rgba(28,28,30,0.65);line-height:1.5">${l.notes}</div>` : ''}
<div style="font-size:9px;color:rgba(28,28,30,0.3);margin-top:5px">Source: ${p.legal_source||'EMCDDA/UNODC 2025'} · Not legal advice</div>
</div>`;
}
@@ -742,33 +742,33 @@
const bar = (v, max=10) => {
const pct = Math.round((v/max)*100);
const col = v>=8?'#16a34a':v>=6?'#ca8a04':'#dc2626';
return `<div style="flex:1;height:4px;border-radius:2px;background:rgba(0,0,0,0.08)"><div style="width:${pct}%;height:100%;background:${col};border-radius:2px"></div></div>`;
return `<div style="flex:1;height:4px;border-radius: var(--radius-sm);background:rgba(0,0,0,0.08)"><div style="width:${pct}%;height:100%;background:${col};border-radius: 2px"></div></div>`;
};
const cashCol = p.cashless>=90?'#16a34a':p.cashless>=70?'#ca8a04':'#dc2626';
const speedCol = p.speed_mbps>=100?'#16a34a':p.speed_mbps>=40?'#ca8a04':'#dc2626';
const g5Col = p.g5_coverage>=80?'#16a34a':p.g5_coverage>=50?'#ca8a04':'#dc2626';
const restrictHtml = p.vpn_needed
? `<div style="background:rgba(220,38,38,0.08);border:1px solid rgba(220,38,38,0.2);border-radius:6px;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:6px;padding:6px 9px;margin-top:6px;font-size:10px;color:#92400e">${p.restrictions.join(' · ')}</div>` : '');
? `<div style="background:rgba(220,38,38,0.08);border:1px solid rgba(220,38,38,0.2);border-radius: var(--radius-sm);padding:6px 9px;margin-top:6px;font-size:10px;color:#991b1b;font-weight:600">VPN REQUIRED — Internet restricted</div>`
: (p.restrictions.length ? `<div style="background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.2);border-radius: var(--radius-sm);padding:6px 9px;margin-top:6px;font-size:10px;color:#92400e">${p.restrictions.join(' · ')}</div>` : '');
const blockedHtml = p.social_blocked.length
? `<div style="font-size:10px;color:#dc2626;margin-top:4px">Blocked: ${p.social_blocked.join(', ')}</div>` : '';
return `<div style="border-top:1px solid rgba(0,0,0,0.08);margin-top:10px;padding-top:10px;font-family:-apple-system,sans-serif">
<div style="font-size:9px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:rgba(28,28,30,0.4);margin-bottom:8px">PRACTICAL INTELLIGENCE</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:8px">
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Public transport</div>
${bar(p.transport)}<div style="font-size:13px;font-weight:800;color:#0A0A0A">${p.transport}/10</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Cashless %</div>
${bar(p.cashless,100)}<div style="font-size:13px;font-weight:800;color:${cashCol}">${p.cashless}%</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">Internet speed</div>
${bar(Math.min(p.speed_mbps/3,10))}<div style="font-size:13px;font-weight:800;color:${speedCol}">${p.speed_mbps} Mbps</div>
</div>
<div style="background:#f2f2f7;border-radius:8px;padding:7px 9px">
<div style="background:#f2f2f7;border-radius: var(--radius-md);padding:7px 9px">
<div style="font-size:9px;color:rgba(28,28,30,0.5);margin-bottom:3px">5G coverage</div>
${bar(p.g5_coverage,100)}<div style="font-size:13px;font-weight:800;color:${g5Col}">${p.g5_coverage}%</div>
</div>
@@ -801,7 +801,7 @@
const status=sl[z.name]||null;
if(!status)return '';
const col=status.startsWith('Available')?'#16a34a':status.startsWith('Unavailable')?'#dc2626':'#ca8a04';
return `<div style="display:flex;align-items:center;justify-content:space-between;background:#f2f2f7;border-radius:8px;padding:7px 10px;margin-top:6px"><div style="font-size:10px;color:rgba(28,28,30,0.5);font-weight:600">Starlink</div><div style="font-size:11px;font-weight:800;color:${col}">${status}</div></div>` + lcsPopup(p) + frictionPopup(p) + legalPopup(p) + socialPopup(p) + mentalHealthPopup(p) + foodPopup(p) + (p.speed_source?`<div style="font-size:9px;color:rgba(28,28,30,0.3);margin-top:5px">Speed: ${p.speed_source}</div>`:'');
return `<div style="display:flex;align-items:center;justify-content:space-between;background:#f2f2f7;border-radius: var(--radius-md);padding:7px 10px;margin-top:6px"><div style="font-size:10px;color:rgba(28,28,30,0.5);font-weight:600">Starlink</div><div style="font-size:11px;font-weight:800;color:${col}">${status}</div></div>` + lcsPopup(p) + frictionPopup(p) + legalPopup(p) + socialPopup(p) + mentalHealthPopup(p) + foodPopup(p) + (p.speed_source?`<div style="font-size:9px;color:rgba(28,28,30,0.3);margin-top:5px">Speed: ${p.speed_source}</div>`:'');
})()}
</div>`;
}
@@ -822,12 +822,12 @@
<div class="zcity">${z.name} <span class="zcountry">· ${z.country}</span></div>
<div class="zconf" style="display:flex;align-items:center;gap:6px">
<span>Confidence ${z.conf}%</span>
${['Stockholm','Bangkok','London','Dubai'].includes(z.name)?'<span style="background:rgba(0,122,255,0.12);color:#007aff;font-size:9px;font-weight:700;padding:1px 7px;border-radius:4px;border:1px solid rgba(0,122,255,0.2)">Deep Dive</span>':''}
${['Stockholm','Bangkok','London','Dubai'].includes(z.name)?'<span style="background:rgba(0,122,255,0.12);color:#007aff;font-size:9px;font-weight:700;padding:1px 7px;border-radius: var(--radius-sm);border:1px solid rgba(0,122,255,0.2)">Deep Dive</span>':''}
</div>
<div class="zsect">Free indices — historical data</div>
<div class="zindices">${freeHtml}</div>
<div class="zcontra ${z.c}"><span class="zcl">${CL[z.c]}</span>${z.ct}</div>
<div class="zage"> Data period: ${z.age}</div>
<div class="zage"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Data period: ${z.age}</div>
<div class="zsect">Live intelligence — on demand</div>
<div class="lock-wrap">
<div class="zindices lock-blur">${livePreview}</div>
@@ -849,7 +849,7 @@
let zg=L.layerGroup().addTo(map),cg=L.layerGroup().addTo(map);
let ls={zones:true,contra:true};
function renderZ(){zg.clearLayers();if(!ls.zones)return;CITIES.forEach(z=>{const st=AS[z.a]||AS.yellow;const pop=zpop(z);const hasDeep=['Stockholm','Bangkok','London','Dubai'].includes(z.name);L.circle([z.lat,z.lng],{radius:z.r||15000,color:hasDeep?'#007aff':st.color,fillColor:st.color,fillOpacity:st.fo,weight:hasDeep?3:st.w,dashArray:st.d}).on("click",function(){if(isMob()){setTimeout(function(){map.closePopup();},10);openBsheet(zpop(z));}}).bindPopup(pop,{maxWidth:300,minWidth:260,autoPan:!isMob()}).addTo(zg);const lbl=L.marker([z.lat,z.lng],{icon:L.divIcon({className:"",html:`<div style="font-size:10px;font-weight:700;color:#0A0A0A;background:rgba(255,255,255,0.95);padding:2px 6px;border-radius:4px;white-space:nowrap;pointer-events:none;box-shadow:0 1px 3px rgba(0,0,0,0.15);border:1px solid rgba(0,0,0,0.1)">${z.name}</div>`,iconAnchor:[0,0]}),interactive:false});lbl.addTo(zg);});}
function renderZ(){zg.clearLayers();if(!ls.zones)return;CITIES.forEach(z=>{const st=AS[z.a]||AS.yellow;const pop=zpop(z);const hasDeep=['Stockholm','Bangkok','London','Dubai'].includes(z.name);L.circle([z.lat,z.lng],{radius:z.r||15000,color:hasDeep?'#007aff':st.color,fillColor:st.color,fillOpacity:st.fo,weight:hasDeep?3:st.w,dashArray:st.d}).on("click",function(){if(isMob()){setTimeout(function(){map.closePopup();},10);openBsheet(zpop(z));}}).bindPopup(pop,{maxWidth:300,minWidth:260,autoPan:!isMob()}).addTo(zg);const lbl=L.marker([z.lat,z.lng],{icon:L.divIcon({className:"",html:`<div style="font-size:10px;font-weight:700;color:#0A0A0A;background:rgba(255,255,255,0.95);padding:2px 6px;border-radius: var(--radius-sm);white-space:nowrap;pointer-events:none;box-shadow:0 1px 3px rgba(0,0,0,0.15);border:1px solid rgba(0,0,0,0.1)">${z.name}</div>`,iconAnchor:[0,0]}),interactive:false});lbl.addTo(zg);});}
function renderC(){cg.clearLayers();if(!ls.contra)return;CITIES.filter(z=>z.c==='high').forEach(z=>{L.circle([z.lat,z.lng],{radius:(z.r||15000)*1.4,color:'#f59e0b',fillColor:'#f59e0b',fillOpacity:0.04,weight:1.5,dashArray:'3 6'}).addTo(cg);});}
loadPracticalData().then(() => { renderZ(); renderC(); });