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
+13 -13
View File
@@ -57,7 +57,7 @@
.stat-label { font-size: 14px; color: var(--text-dim); line-height: 1.5; }
.districts-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 40px; }
.district-chip { background: var(--bg-dark); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 16px 20px; display: flex; align-items: center; gap: 10px; }
.district-chip .chip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--blue); flex-shrink: 0; }
.district-chip .chip-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--blue); flex-shrink: 0; }
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
.intel-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 40px; }
.intel-card { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 32px 28px; transition: border-color 0.25s, transform 0.2s; }
@@ -79,13 +79,13 @@
.district-name { font-weight: 600; color: #1d1d1f; }
.table-note { margin-top: 12px; font-size: 12px; color: var(--text-muted); }
.contradiction-bg { background: #e8f0fe; }
.contradiction-box { background: rgba(0,0,0,.05); border: 1px solid rgba(0,0,0,.1); border-radius: 16px; padding: 36px 40px; margin-bottom: 32px; }
.contradiction-box { background: rgba(0,0,0,.05); border: 1px solid rgba(0,0,0,.1); border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px; }
.conflict-header { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 24px; }
.conflict-flag-svg { flex-shrink: 0; margin-top: 2px; }
.conflict-title { color: #f87171; font-weight: 800; font-size: .9375rem; margin-bottom: 4px; }
.conflict-desc { color: rgba(0,0,0,.6); font-size: .875rem; line-height: 1.6; }
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
.conflict-side-label { font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; margin-bottom: 10px; }
@@ -101,7 +101,7 @@
.ce-feature-icon { width: 28px; height: 28px; margin: 0 auto 10px; color: rgba(0,0,0,0.75); }
.ce-feature h4 { color: #1d1d1f; font-weight: 700; margin-bottom: 6px; }
.ce-feature p { color: rgba(0,0,0,.75); font-size: .8125rem; }
.cta-banner { background: #e8f0fe; border-radius: 16px; padding: 40px 48px; display: flex; align-items: center; gap: 48px; flex-wrap: wrap; margin-top: 48px; }
.cta-banner { background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px; display: flex; align-items: center; gap: 48px; flex-wrap: wrap; margin-top: 48px; }
.cta-banner h3 { color: #1d1d1f; font-size: 1.25rem; font-weight: 800; margin-bottom: 8px; }
.cta-banner p { color: rgba(0,0,0,.7); line-height: 1.65; font-size: .9375rem; }
.cta-banner-text { flex: 1; min-width: 260px; }
@@ -127,7 +127,7 @@
.footer-links { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; }
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social { width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px; display: flex; align-items: center; justify-content: center; transition: border-color 0.2s, background 0.2s; }
.footer-social { width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; transition: border-color 0.2s, background 0.2s; }
.footer-social:hover { border-color: rgba(0,0,0,0.25); background: rgba(0,0,0,0.04); }
@media (max-width: 900px) { nav { padding: 0 20px; } .stats-row { grid-template-columns: 1fr 1fr; } .districts-grid { grid-template-columns: repeat(2, 1fr); } .intel-grid { grid-template-columns: 1fr; } .conflict-grid { grid-template-columns: 1fr; } .ce-features { grid-template-columns: 1fr; } }
@media (max-width: 600px) {
@@ -151,7 +151,7 @@
.scores-grid { grid-template-columns: 1fr !important; }
}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius:8px;line-height:1.6;max-width:800px}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius: var(--radius-md);line-height:1.6;max-width:800px}
.lv-disclaimer a{color:rgba(0,0,0,.65);text-decoration:underline}
.lv-disclaimer--light{color:#888;background:#f5f5f5}
.lv-disclaimer--light a{color:#555}
@@ -361,18 +361,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; City prioritises residential over commercial development</li>
<li>&#10003; Zuidas: controlled mixed-use densification 2024&ndash;2026</li>
<li>&#10003; Noord: transformation to residential and creative hub</li>
<li>&#100003 City prioritises residential over commercial development</li>
<li>&#100003 Zuidas: controlled mixed-use densification 2024&ndash;2026</li>
<li>&#100003 Noord: transformation to residential and creative hub</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; Office-to-residential conversions behind schedule across Centrum</li>
<li>&#10007; Nieuw-West: commercial vacancy rising despite residential demand signals</li>
<li>&#10007; Noord: commercial development outpacing residential completions</li>
<li>&#10007; Zuidas: institutional demand exceeding permitted supply</li>
<li>&#100007 Office-to-residential conversions behind schedule across Centrum</li>
<li>&#100007 Nieuw-West: commercial vacancy rising despite residential demand signals</li>
<li>&#100007 Noord: commercial development outpacing residential completions</li>
<li>&#100007 Zuidas: institutional demand exceeding permitted supply</li>
</ul>
</div>
</div>
+6 -6
View File
@@ -57,7 +57,7 @@
.stat-label { font-size: 14px; color: var(--text-dim); line-height: 1.5; }
.districts-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 40px; }
.district-chip { background: var(--bg-dark); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 16px 20px; display: flex; align-items: center; gap: 10px; }
.district-chip .chip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--blue); flex-shrink: 0; }
.district-chip .chip-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--blue); flex-shrink: 0; }
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
.intel-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 40px; }
.intel-card { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 32px 28px; transition: border-color 0.25s, transform 0.2s; }
@@ -79,13 +79,13 @@
.district-name { font-weight: 600; color: #1d1d1f; }
.table-note { margin-top: 12px; font-size: 12px; color: var(--text-muted); }
.contradiction-bg { background: #e8f0fe; }
.contradiction-box { background: rgba(0,0,0,.05); border: 1px solid rgba(0,0,0,.1); border-radius: 16px; padding: 36px 40px; margin-bottom: 32px; }
.contradiction-box { background: rgba(0,0,0,.05); border: 1px solid rgba(0,0,0,.1); border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px; }
.conflict-header { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 24px; }
.conflict-flag-svg { flex-shrink: 0; margin-top: 2px; }
.conflict-title { color: #f87171; font-weight: 800; font-size: .9375rem; margin-bottom: 4px; }
.conflict-desc { color: rgba(0,0,0,.6); font-size: .875rem; line-height: 1.6; }
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
.conflict-side-label { font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; margin-bottom: 10px; }
@@ -101,7 +101,7 @@
.ce-feature-icon { width: 28px; height: 28px; margin: 0 auto 10px; color: rgba(0,0,0,0.75); }
.ce-feature h4 { color: #1d1d1f; font-weight: 700; margin-bottom: 6px; }
.ce-feature p { color: rgba(0,0,0,.75); font-size: .8125rem; }
.cta-banner { background: #e8f0fe; border-radius: 16px; padding: 40px 48px; display: flex; align-items: center; gap: 48px; flex-wrap: wrap; margin-top: 48px; }
.cta-banner { background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px; display: flex; align-items: center; gap: 48px; flex-wrap: wrap; margin-top: 48px; }
.cta-banner h3 { color: #1d1d1f; font-size: 1.25rem; font-weight: 800; margin-bottom: 8px; }
.cta-banner p { color: rgba(0,0,0,.7); line-height: 1.65; font-size: .9375rem; }
.cta-banner-text { flex: 1; min-width: 260px; }
@@ -127,7 +127,7 @@
.footer-links { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; }
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social { width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px; display: flex; align-items: center; justify-content: center; transition: border-color 0.2s, background 0.2s; }
.footer-social { width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; transition: border-color 0.2s, background 0.2s; }
.footer-social:hover { border-color: rgba(0,0,0,0.25); background: rgba(0,0,0,0.04); }
@media (max-width: 900px) { nav { padding: 0 20px; } .stats-row { grid-template-columns: 1fr 1fr; } .districts-grid { grid-template-columns: repeat(2, 1fr); } .intel-grid { grid-template-columns: 1fr; } .conflict-grid { grid-template-columns: 1fr; } .ce-features { grid-template-columns: 1fr; } }
@media (max-width: 600px) {
@@ -150,7 +150,7 @@
.districts-grid { grid-template-columns: 1fr !important; }
.scores-grid { grid-template-columns: 1fr !important; }
}
.lv-disclaimer { font-size: .75rem; color: rgba(0,0,0,.75); margin-top: 24px; padding: 12px 16px; background: rgba(0,0,0,.04); border-radius: 8px; line-height: 1.6; max-width: 800px; }
.lv-disclaimer { font-size: .75rem; color: rgba(0,0,0,.75); margin-top: 24px; padding: 12px 16px; background: rgba(0,0,0,.04); border-radius: var(--radius-md); line-height: 1.6; max-width: 800px; }
.lv-disclaimer a { color: rgba(0,0,0,.75); text-decoration: underline; }
</style>
+13 -13
View File
@@ -180,7 +180,7 @@
display: flex; align-items: center; gap: 10px;
}
.district-chip .chip-dot {
width: 8px; height: 8px; border-radius: 50%;
width: 8px; height: 8px; border-radius: var(--radius-full);
background: var(--blue); flex-shrink: 0;
}
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
@@ -251,7 +251,7 @@
.contradiction-box {
background: rgba(0,0,0,.05);
border: 1px solid rgba(0,0,0,.1);
border-radius: 16px; padding: 36px 40px; margin-bottom: 32px;
border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px;
}
.conflict-header {
@@ -265,7 +265,7 @@
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
@@ -300,7 +300,7 @@
/* CTA BANNER */
.cta-banner {
background: #e8f0fe; border-radius: 16px; padding: 40px 48px;
background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px;
display: flex; align-items: center; gap: 48px; flex-wrap: wrap;
margin-top: 48px;
}
@@ -352,7 +352,7 @@
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social {
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px;
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
@@ -387,7 +387,7 @@
.scores-grid { grid-template-columns: 1fr !important; }
}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius:8px;line-height:1.6;max-width:800px}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius: var(--radius-md);line-height:1.6;max-width:800px}
.lv-disclaimer a{color:rgba(0,0,0,.65);text-decoration:underline}
.lv-disclaimer--light{color:#888;background:#f5f5f5}
.lv-disclaimer--light a{color:#555}
@@ -632,18 +632,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; &euro;2.4bn development investment committed 2024&ndash;2025</li>
<li>&#10003; Tempelhof: major mixed-use regeneration zone</li>
<li>&#10003; Construction permits up 18% year-on-year</li>
<li>&#100003 &euro;2.4bn development investment committed 2024&ndash;2025</li>
<li>&#100003 Tempelhof: major mixed-use regeneration zone</li>
<li>&#100003 Construction permits up 18% year-on-year</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; Active construction sites below permit volume</li>
<li>&#10007; Tempelhof: planning delays visible on ground</li>
<li>&#10007; Neuk&ouml;lln commercial vacancy increasing</li>
<li>&#10007; Prenzlauer Berg outperforming official projections</li>
<li>&#100007 Active construction sites below permit volume</li>
<li>&#100007 Tempelhof: planning delays visible on ground</li>
<li>&#100007 Neuk&ouml;lln commercial vacancy increasing</li>
<li>&#100007 Prenzlauer Berg outperforming official projections</li>
</ul>
</div>
</div>
+13 -13
View File
@@ -181,7 +181,7 @@
display: flex; align-items: center; gap: 10px;
}
.district-chip .chip-dot {
width: 8px; height: 8px; border-radius: 50%;
width: 8px; height: 8px; border-radius: var(--radius-full);
background: var(--blue); flex-shrink: 0;
}
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
@@ -252,7 +252,7 @@
.contradiction-box {
background: rgba(0,0,0,.05);
border: 1px solid rgba(0,0,0,.1);
border-radius: 16px; padding: 36px 40px; margin-bottom: 32px;
border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px;
}
.conflict-header {
@@ -266,7 +266,7 @@
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
@@ -300,7 +300,7 @@
.ce-feature p { color: rgba(0,0,0,.75); font-size: .8125rem; }
.cta-banner {
background: #e8f0fe; border-radius: 16px; padding: 40px 48px;
background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px;
display: flex; align-items: center; gap: 48px; flex-wrap: wrap;
margin-top: 48px;
}
@@ -350,7 +350,7 @@
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social {
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px;
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
@@ -385,7 +385,7 @@
.scores-grid { grid-template-columns: 1fr !important; }
}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius:8px;line-height:1.6;max-width:800px}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius: var(--radius-md);line-height:1.6;max-width:800px}
.lv-disclaimer a{color:rgba(0,0,0,.65);text-decoration:underline}
</style>
@@ -613,18 +613,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; Ixelles: EU quarter spillover driving sustained commercial demand</li>
<li>&#10003; Molenbeek: urban revitalisation programme approved 2024</li>
<li>&#10003; Regional plan targets 15% vacancy reduction by 2026</li>
<li>&#100003 Ixelles: EU quarter spillover driving sustained commercial demand</li>
<li>&#100003 Molenbeek: urban revitalisation programme approved 2024</li>
<li>&#100003 Regional plan targets 15% vacancy reduction by 2026</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; Ixelles: institutional demand not translating to street-level retail vitality</li>
<li>&#10007; Molenbeek: revitalisation slower than approved programme timeline</li>
<li>&#10007; Schaerbeek commercial strips showing early distress signals</li>
<li>&#10007; Etterbeek holding stronger than official projections suggest</li>
<li>&#100007 Ixelles: institutional demand not translating to street-level retail vitality</li>
<li>&#100007 Molenbeek: revitalisation slower than approved programme timeline</li>
<li>&#100007 Schaerbeek commercial strips showing early distress signals</li>
<li>&#100007 Etterbeek holding stronger than official projections suggest</li>
</ul>
</div>
</div>
+13 -13
View File
@@ -181,7 +181,7 @@
display: flex; align-items: center; gap: 10px;
}
.district-chip .chip-dot {
width: 8px; height: 8px; border-radius: 50%;
width: 8px; height: 8px; border-radius: var(--radius-full);
background: var(--blue); flex-shrink: 0;
}
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
@@ -252,7 +252,7 @@
.contradiction-box {
background: rgba(0,0,0,.05);
border: 1px solid rgba(0,0,0,.1);
border-radius: 16px; padding: 36px 40px; margin-bottom: 32px;
border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px;
}
.conflict-header {
@@ -266,7 +266,7 @@
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
@@ -300,7 +300,7 @@
.ce-feature p { color: rgba(0,0,0,.75); font-size: .8125rem; }
.cta-banner {
background: #e8f0fe; border-radius: 16px; padding: 40px 48px;
background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px;
display: flex; align-items: center; gap: 48px; flex-wrap: wrap;
margin-top: 48px;
}
@@ -350,7 +350,7 @@
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social {
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px;
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
@@ -385,7 +385,7 @@
.scores-grid { grid-template-columns: 1fr !important; }
}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius:8px;line-height:1.6;max-width:800px}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius: var(--radius-md);line-height:1.6;max-width:800px}
.lv-disclaimer a{color:rgba(0,0,0,.65);text-decoration:underline}
</style>
@@ -613,18 +613,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; Indre By: stable retail core with sustained footfall</li>
<li>&#10003; N&oslash;rrebro: municipal regeneration programme on schedule</li>
<li>&#10003; Development permits: 11 approved 2024&ndash;2025</li>
<li>&#100003 Indre By: stable retail core with sustained footfall</li>
<li>&#100003 N&oslash;rrebro: municipal regeneration programme on schedule</li>
<li>&#100003 Development permits: 11 approved 2024&ndash;2025</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; Indre By: mid-market retail closures accelerating</li>
<li>&#10007; N&oslash;rrebro: regeneration delayed; vacancy elevated</li>
<li>&#10007; Permit-to-start conversion below forecast</li>
<li>&#10007; Amager outperforming planned growth projections</li>
<li>&#100007 Indre By: mid-market retail closures accelerating</li>
<li>&#100007 N&oslash;rrebro: regeneration delayed; vacancy elevated</li>
<li>&#100007 Permit-to-start conversion below forecast</li>
<li>&#100007 Amager outperforming planned growth projections</li>
</ul>
</div>
</div>
+13 -13
View File
@@ -181,7 +181,7 @@
display: flex; align-items: center; gap: 10px;
}
.district-chip .chip-dot {
width: 8px; height: 8px; border-radius: 50%;
width: 8px; height: 8px; border-radius: var(--radius-full);
background: var(--blue); flex-shrink: 0;
}
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
@@ -252,7 +252,7 @@
.contradiction-box {
background: rgba(0,0,0,.05);
border: 1px solid rgba(0,0,0,.1);
border-radius: 16px; padding: 36px 40px; margin-bottom: 32px;
border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px;
}
.conflict-header {
@@ -266,7 +266,7 @@
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
@@ -300,7 +300,7 @@
.ce-feature p { color: rgba(0,0,0,.75); font-size: .8125rem; }
.cta-banner {
background: #e8f0fe; border-radius: 16px; padding: 40px 48px;
background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px;
display: flex; align-items: center; gap: 48px; flex-wrap: wrap;
margin-top: 48px;
}
@@ -350,7 +350,7 @@
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social {
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px;
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
@@ -385,7 +385,7 @@
.scores-grid { grid-template-columns: 1fr !important; }
}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius:8px;line-height:1.6;max-width:800px}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius: var(--radius-md);line-height:1.6;max-width:800px}
.lv-disclaimer a{color:rgba(0,0,0,.65);text-decoration:underline}
</style>
@@ -613,18 +613,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; Eteläinen: historic core with stable premium footfall</li>
<li>&#10003; Vuosaari: port logistics zone designated for expansion</li>
<li>&#10003; Helsinki city strategy targets carbon-neutral growth by 2030</li>
<li>&#100003 Eteläinen: historic core with stable premium footfall</li>
<li>&#100003 Vuosaari: port logistics zone designated for expansion</li>
<li>&#100003 Helsinki city strategy targets carbon-neutral growth by 2030</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; Eteläinen: weekend tourist footfall masking weekday commercial softness</li>
<li>&#10007; Vuosaari expansion pace behind logistics demand signals</li>
<li>&#10007; Kallio creative district showing rent-driven displacement pressures</li>
<li>&#10007; Pitäjänmäki tech campus vacancy above city average</li>
<li>&#100007 Eteläinen: weekend tourist footfall masking weekday commercial softness</li>
<li>&#100007 Vuosaari expansion pace behind logistics demand signals</li>
<li>&#100007 Kallio creative district showing rent-driven displacement pressures</li>
<li>&#100007 Pitäjänmäki tech campus vacancy above city average</li>
</ul>
</div>
</div>
+13 -13
View File
@@ -120,7 +120,7 @@
.districts-bg { background: var(--bg-dark); }
.districts-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 40px; }
.district-chip { background: var(--bg-dark); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 16px 20px; display: flex; align-items: center; gap: 10px; }
.district-chip .chip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--blue); flex-shrink: 0; }
.district-chip .chip-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--blue); flex-shrink: 0; }
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
.intel-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 40px; }
@@ -145,13 +145,13 @@
.table-note { margin-top: 12px; font-size: 12px; color: var(--text-muted); }
.contradiction-bg { background: #e8f0fe; }
.contradiction-box { background: rgba(0,0,0,.05); border: 1px solid rgba(0,0,0,.1); border-radius: 16px; padding: 36px 40px; margin-bottom: 32px; }
.contradiction-box { background: rgba(0,0,0,.05); border: 1px solid rgba(0,0,0,.1); border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px; }
.conflict-header { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 24px; }
.conflict-flag-svg { flex-shrink: 0; margin-top: 2px; }
.conflict-title { color: #f87171; font-weight: 800; font-size: .9375rem; margin-bottom: 4px; }
.conflict-desc { color: rgba(0,0,0,.6); font-size: .875rem; line-height: 1.6; }
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
.conflict-side-label { font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; margin-bottom: 10px; }
@@ -169,7 +169,7 @@
.ce-feature h4 { color: #1d1d1f; font-weight: 700; margin-bottom: 6px; }
.ce-feature p { color: rgba(0,0,0,.75); font-size: .8125rem; }
.cta-banner { background: #e8f0fe; border-radius: 16px; padding: 40px 48px; display: flex; align-items: center; gap: 48px; flex-wrap: wrap; margin-top: 48px; }
.cta-banner { background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px; display: flex; align-items: center; gap: 48px; flex-wrap: wrap; margin-top: 48px; }
.cta-banner h3 { color: #1d1d1f; font-size: 1.25rem; font-weight: 800; margin-bottom: 8px; }
.cta-banner p { color: rgba(0,0,0,.7); line-height: 1.65; font-size: .9375rem; }
.cta-banner-text { flex: 1; min-width: 260px; }
@@ -195,10 +195,10 @@
.footer-links { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; }
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social { width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px; display: flex; align-items: center; justify-content: center; transition: border-color 0.2s, background 0.2s; }
.footer-social { width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; transition: border-color 0.2s, background 0.2s; }
.footer-social:hover { border-color: rgba(0,0,0,0.25); background: rgba(0,0,0,0.04); }
.lv-disclaimer { font-size:.75rem; color:rgba(0,0,0,.75); margin-top:24px; padding:12px 16px; background:rgba(0,0,0,.04); border-radius:8px; line-height:1.6; max-width:800px; }
.lv-disclaimer { font-size:.75rem; color:rgba(0,0,0,.75); margin-top:24px; padding:12px 16px; background:rgba(0,0,0,.04); border-radius: var(--radius-md); line-height:1.6; max-width:800px; }
.lv-disclaimer a { color:rgba(0,0,0,.65); text-decoration:underline; }
@media (max-width: 900px) {
@@ -470,18 +470,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; City of London vacancy rate: within 10-year norm</li>
<li>&#10003; Canary Wharf: anchor tenant commitments renewed</li>
<li>&#10003; Office-to-residential conversions: reducing surplus</li>
<li>&#100003 City of London vacancy rate: within 10-year norm</li>
<li>&#100003 Canary Wharf: anchor tenant commitments renewed</li>
<li>&#100003 Office-to-residential conversions: reducing surplus</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; Sub-let market expanding in City: direct vacancy understated</li>
<li>&#10007; Canary Wharf ground floor retail vacancy materially elevated</li>
<li>&#10007; Conversion pipeline slower than reported in planning data</li>
<li>&#10007; Shoreditch commercial rents compressing despite headline growth</li>
<li>&#100007 Sub-let market expanding in City: direct vacancy understated</li>
<li>&#100007 Canary Wharf ground floor retail vacancy materially elevated</li>
<li>&#100007 Conversion pipeline slower than reported in planning data</li>
<li>&#100007 Shoreditch commercial rents compressing despite headline growth</li>
</ul>
</div>
</div>
+13 -13
View File
@@ -181,7 +181,7 @@
display: flex; align-items: center; gap: 10px;
}
.district-chip .chip-dot {
width: 8px; height: 8px; border-radius: 50%;
width: 8px; height: 8px; border-radius: var(--radius-full);
background: var(--blue); flex-shrink: 0;
}
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
@@ -252,7 +252,7 @@
.contradiction-box {
background: rgba(0,0,0,.05);
border: 1px solid rgba(0,0,0,.1);
border-radius: 16px; padding: 36px 40px; margin-bottom: 32px;
border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px;
}
.conflict-header {
@@ -266,7 +266,7 @@
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
@@ -300,7 +300,7 @@
.ce-feature p { color: rgba(0,0,0,.75); font-size: .8125rem; }
.cta-banner {
background: #e8f0fe; border-radius: 16px; padding: 40px 48px;
background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px;
display: flex; align-items: center; gap: 48px; flex-wrap: wrap;
margin-top: 48px;
}
@@ -350,7 +350,7 @@
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social {
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px;
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
@@ -385,7 +385,7 @@
.scores-grid { grid-template-columns: 1fr !important; }
}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius:8px;line-height:1.6;max-width:800px}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius: var(--radius-md);line-height:1.6;max-width:800px}
.lv-disclaimer a{color:rgba(0,0,0,.65);text-decoration:underline}
</style>
@@ -613,18 +613,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; Centro: high-footfall retail corridor rated stable</li>
<li>&#10003; Vallecas: urban renewal investment zone designated</li>
<li>&#10003; Tourism-led recovery projected to sustain 2024&ndash;2026</li>
<li>&#100003 Centro: high-footfall retail corridor rated stable</li>
<li>&#100003 Vallecas: urban renewal investment zone designated</li>
<li>&#100003 Tourism-led recovery projected to sustain 2024&ndash;2026</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; Centro: luxury displacement eroding mid-market operators</li>
<li>&#10007; Vallecas: renewal activity slower than designated timeline</li>
<li>&#10007; Short-term rental saturation suppressing residential vitality</li>
<li>&#10007; Getafe industrial cluster outperforming city core projections</li>
<li>&#100007 Centro: luxury displacement eroding mid-market operators</li>
<li>&#100007 Vallecas: renewal activity slower than designated timeline</li>
<li>&#100007 Short-term rental saturation suppressing residential vitality</li>
<li>&#100007 Getafe industrial cluster outperforming city core projections</li>
</ul>
</div>
</div>
+13 -13
View File
@@ -171,7 +171,7 @@
border-radius: var(--radius-md); padding: 16px 20px;
display: flex; align-items: center; gap: 10px;
}
.district-chip .chip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--blue); flex-shrink: 0; }
.district-chip .chip-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--blue); flex-shrink: 0; }
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
.intel-grid {
@@ -229,7 +229,7 @@
.contradiction-box {
background: rgba(0,0,0,.05);
border: 1px solid rgba(0,0,0,.1);
border-radius: 16px; padding: 36px 40px; margin-bottom: 32px;
border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px;
}
.conflict-header { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 24px; }
@@ -239,7 +239,7 @@
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
@@ -268,7 +268,7 @@
.ce-feature p { color: rgba(0,0,0,.75); font-size: .8125rem; }
.cta-banner {
background: #e8f0fe; border-radius: 16px; padding: 40px 48px;
background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px;
display: flex; align-items: center; gap: 48px; flex-wrap: wrap;
margin-top: 48px;
}
@@ -318,7 +318,7 @@
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social {
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px;
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
@@ -353,7 +353,7 @@
.scores-grid { grid-template-columns: 1fr !important; }
}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius:8px;line-height:1.6;max-width:800px}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius: var(--radius-md);line-height:1.6;max-width:800px}
.lv-disclaimer a{color:rgba(0,0,0,.65);text-decoration:underline}
.lv-disclaimer--light{color:#888;background:#f5f5f5}
.lv-disclaimer--light a{color:#555}
@@ -598,18 +598,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; Milan ranks among top 5 European investment destinations</li>
<li>&#10003; Porta Nuova: sustained institutional investment 2024&ndash;2025</li>
<li>&#10003; City commercial vacancy rate cited at 4.2% (JLL 2025)</li>
<li>&#100003 Milan ranks among top 5 European investment destinations</li>
<li>&#100003 Porta Nuova: sustained institutional investment 2024&ndash;2025</li>
<li>&#100003 City commercial vacancy rate cited at 4.2% (JLL 2025)</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; Centro / Duomo: elevated vacancy on secondary retail streets</li>
<li>&#10007; Sesto San Giovanni: industrial-to-mixed conversion stalling</li>
<li>&#10007; Navigli: hospitality occupancy below pre-2023 levels</li>
<li>&#10007; Isola outperforming official projections for residential demand</li>
<li>&#100007 Centro / Duomo: elevated vacancy on secondary retail streets</li>
<li>&#100007 Sesto San Giovanni: industrial-to-mixed conversion stalling</li>
<li>&#100007 Navigli: hospitality occupancy below pre-2023 levels</li>
<li>&#100007 Isola outperforming official projections for residential demand</li>
</ul>
</div>
</div>
+13 -13
View File
@@ -181,7 +181,7 @@
display: flex; align-items: center; gap: 10px;
}
.district-chip .chip-dot {
width: 8px; height: 8px; border-radius: 50%;
width: 8px; height: 8px; border-radius: var(--radius-full);
background: var(--blue); flex-shrink: 0;
}
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
@@ -252,7 +252,7 @@
.contradiction-box {
background: rgba(0,0,0,.05);
border: 1px solid rgba(0,0,0,.1);
border-radius: 16px; padding: 36px 40px; margin-bottom: 32px;
border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px;
}
.conflict-header {
@@ -266,7 +266,7 @@
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
@@ -300,7 +300,7 @@
.ce-feature p { color: rgba(0,0,0,.75); font-size: .8125rem; }
.cta-banner {
background: #e8f0fe; border-radius: 16px; padding: 40px 48px;
background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px;
display: flex; align-items: center; gap: 48px; flex-wrap: wrap;
margin-top: 48px;
}
@@ -350,7 +350,7 @@
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social {
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px;
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
@@ -385,7 +385,7 @@
.scores-grid { grid-template-columns: 1fr !important; }
}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius:8px;line-height:1.6;max-width:800px}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius: var(--radius-md);line-height:1.6;max-width:800px}
.lv-disclaimer a{color:rgba(0,0,0,.65);text-decoration:underline}
</style>
@@ -613,18 +613,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; Sentrum: waterfront regeneration driving sustained commercial growth</li>
<li>&#10003; Groruddalen: major infrastructure investment corridor active 2023&ndash;2026</li>
<li>&#10003; Oslo master plan targets eastward commercial rebalancing</li>
<li>&#100003 Sentrum: waterfront regeneration driving sustained commercial growth</li>
<li>&#100003 Groruddalen: major infrastructure investment corridor active 2023&ndash;2026</li>
<li>&#100003 Oslo master plan targets eastward commercial rebalancing</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; Sentrum waterfront: luxury-led development suppressing mixed-use vitality</li>
<li>&#10007; Groruddalen investment slower to materialise than corridor plan implies</li>
<li>&#10007; Frogner holding premium position despite broader market softness</li>
<li>&#10007; Grünerløkka commercial density nearing saturation point</li>
<li>&#100007 Sentrum waterfront: luxury-led development suppressing mixed-use vitality</li>
<li>&#100007 Groruddalen investment slower to materialise than corridor plan implies</li>
<li>&#100007 Frogner holding premium position despite broader market softness</li>
<li>&#100007 Grünerløkka commercial density nearing saturation point</li>
</ul>
</div>
</div>
+13 -13
View File
@@ -135,7 +135,7 @@
border-radius: var(--radius-md); padding: 16px 20px;
display: flex; align-items: center; gap: 10px;
}
.district-chip .chip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--blue); flex-shrink: 0; }
.district-chip .chip-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--blue); flex-shrink: 0; }
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
.intel-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 40px; }
@@ -169,13 +169,13 @@
.table-note { margin-top: 12px; font-size: 12px; color: var(--text-muted); }
.contradiction-bg { background: #e8f0fe; }
.contradiction-box { background: rgba(0,0,0,.05); border: 1px solid rgba(0,0,0,.1); border-radius: 16px; padding: 36px 40px; margin-bottom: 32px; }
.contradiction-box { background: rgba(0,0,0,.05); border: 1px solid rgba(0,0,0,.1); border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px; }
.conflict-header { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 24px; }
.conflict-flag-svg { flex-shrink: 0; margin-top: 2px; }
.conflict-title { color: #f87171; font-weight: 800; font-size: .9375rem; margin-bottom: 4px; }
.conflict-desc { color: rgba(0,0,0,.6); font-size: .875rem; line-height: 1.6; }
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
.conflict-side-label { font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; margin-bottom: 10px; }
@@ -193,7 +193,7 @@
.ce-feature h4 { color: #1d1d1f; font-weight: 700; margin-bottom: 6px; }
.ce-feature p { color: rgba(0,0,0,.75); font-size: .8125rem; }
.cta-banner { background: #e8f0fe; border-radius: 16px; padding: 40px 48px; display: flex; align-items: center; gap: 48px; flex-wrap: wrap; margin-top: 48px; }
.cta-banner { background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px; display: flex; align-items: center; gap: 48px; flex-wrap: wrap; margin-top: 48px; }
.cta-banner h3 { color: #1d1d1f; font-size: 1.25rem; font-weight: 800; margin-bottom: 8px; }
.cta-banner p { color: rgba(0,0,0,.7); line-height: 1.65; font-size: .9375rem; }
.cta-banner-text { flex: 1; min-width: 260px; }
@@ -224,10 +224,10 @@
.footer-links { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; }
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social { width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px; display: flex; align-items: center; justify-content: center; transition: border-color 0.2s, background 0.2s; }
.footer-social { width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; transition: border-color 0.2s, background 0.2s; }
.footer-social:hover { border-color: rgba(0,0,0,0.25); background: rgba(0,0,0,0.04); }
.lv-disclaimer { font-size:.75rem; color:rgba(0,0,0,.75); margin-top:24px; padding:12px 16px; background:rgba(0,0,0,.04); border-radius:8px; line-height:1.6; max-width:800px; }
.lv-disclaimer { font-size:.75rem; color:rgba(0,0,0,.75); margin-top:24px; padding:12px 16px; background:rgba(0,0,0,.04); border-radius: var(--radius-md); line-height:1.6; max-width:800px; }
.lv-disclaimer a { color:rgba(0,0,0,.65); text-decoration:underline; }
@media (max-width: 900px) {
@@ -499,18 +499,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; Paris Plan Local d&rsquo;Urbanisme: commercial priority zones active</li>
<li>&#10003; Invest in Paris: &euro;2.3bn commercial investment committed 2024</li>
<li>&#10003; Arrondissement vacancy rate: below EU major city average</li>
<li>&#100003 Paris Plan Local d&rsquo;Urbanisme: commercial priority zones active</li>
<li>&#100003 Invest in Paris: &euro;2.3bn commercial investment committed 2024</li>
<li>&#100003 Arrondissement vacancy rate: below EU major city average</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; 8e arrondissement: luxury retail vacancy elevated on secondary streets</li>
<li>&#10007; 10e/11e: independent retail closures accelerating post-pandemic</li>
<li>&#10007; La D&eacute;fense: sub-let market expanding, direct vacancy understated</li>
<li>&#10007; Investment permits not translating to observed ground-floor activation</li>
<li>&#100007 8e arrondissement: luxury retail vacancy elevated on secondary streets</li>
<li>&#100007 10e/11e: independent retail closures accelerating post-pandemic</li>
<li>&#100007 La D&eacute;fense: sub-let market expanding, direct vacancy understated</li>
<li>&#100007 Investment permits not translating to observed ground-floor activation</li>
</ul>
</div>
</div>
+13 -13
View File
@@ -181,7 +181,7 @@
display: flex; align-items: center; gap: 10px;
}
.district-chip .chip-dot {
width: 8px; height: 8px; border-radius: 50%;
width: 8px; height: 8px; border-radius: var(--radius-full);
background: var(--blue); flex-shrink: 0;
}
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
@@ -252,7 +252,7 @@
.contradiction-box {
background: rgba(0,0,0,.05);
border: 1px solid rgba(0,0,0,.1);
border-radius: 16px; padding: 36px 40px; margin-bottom: 32px;
border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px;
}
.conflict-header {
@@ -266,7 +266,7 @@
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
@@ -300,7 +300,7 @@
.ce-feature p { color: rgba(0,0,0,.75); font-size: .8125rem; }
.cta-banner {
background: #e8f0fe; border-radius: 16px; padding: 40px 48px;
background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px;
display: flex; align-items: center; gap: 48px; flex-wrap: wrap;
margin-top: 48px;
}
@@ -350,7 +350,7 @@
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social {
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px;
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
@@ -385,7 +385,7 @@
.scores-grid { grid-template-columns: 1fr !important; }
}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius:8px;line-height:1.6;max-width:800px}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius: var(--radius-md);line-height:1.6;max-width:800px}
.lv-disclaimer a{color:rgba(0,0,0,.65);text-decoration:underline}
</style>
@@ -613,18 +613,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; Centrum: architecture-led regeneration attracting sustained investment</li>
<li>&#10003; Feijenoord: social infrastructure investment programme active</li>
<li>&#10003; Port-adjacent development rated strategically sound by municipality</li>
<li>&#100003 Centrum: architecture-led regeneration attracting sustained investment</li>
<li>&#100003 Feijenoord: social infrastructure investment programme active</li>
<li>&#100003 Port-adjacent development rated strategically sound by municipality</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; Centrum regeneration: design-led investment not penetrating secondary streets</li>
<li>&#10007; Feijenoord social investment slower than programme commitment</li>
<li>&#10007; Logistics demand in Prins Alexander exceeding residential plan assumptions</li>
<li>&#10007; Delfshaven creative cluster emerging ahead of formal recognition</li>
<li>&#100007 Centrum regeneration: design-led investment not penetrating secondary streets</li>
<li>&#100007 Feijenoord social investment slower than programme commitment</li>
<li>&#100007 Logistics demand in Prins Alexander exceeding residential plan assumptions</li>
<li>&#100007 Delfshaven creative cluster emerging ahead of formal recognition</li>
</ul>
</div>
</div>
+16 -16
View File
@@ -182,7 +182,7 @@
display: flex; align-items: center; gap: 10px;
}
.district-chip .chip-dot {
width: 8px; height: 8px; border-radius: 50%;
width: 8px; height: 8px; border-radius: var(--radius-full);
background: var(--blue); flex-shrink: 0;
}
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
@@ -256,7 +256,7 @@
.contradiction-box {
background: rgba(0,0,0,.05);
border: 1px solid rgba(0,0,0,.1);
border-radius: 16px; padding: 36px 40px; margin-bottom: 32px;
border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px;
}
.conflict-header {
@@ -270,7 +270,7 @@
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
@@ -305,7 +305,7 @@
/* CTA BANNER */
.cta-banner {
background: #e8f0fe; border-radius: 16px; padding: 40px 48px;
background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px;
display: flex; align-items: center; gap: 48px; flex-wrap: wrap;
margin-top: 48px;
}
@@ -357,7 +357,7 @@
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social {
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px;
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
@@ -392,7 +392,7 @@
.scores-grid { grid-template-columns: 1fr !important; }
}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius:8px;line-height:1.6;max-width:800px}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius: var(--radius-md);line-height:1.6;max-width:800px}
.lv-disclaimer a{color:rgba(0,0,0,.65);text-decoration:underline}
.lv-disclaimer--light{color:#888;background:#f5f5f5}
.lv-disclaimer--light a{color:#555}
@@ -565,7 +565,7 @@
<div class="container">
<div class="section-label">District scores</div>
<h2 class="section-title">Sample intelligence scores</h2>
<p class="section-sub" style="background:#fff3cd;border:2px solid #ffc107;border-radius:8px;padding:12px 16px;color:#856404;font-weight:600;">⚠️ ILLUSTRATIVE SAMPLE DATA — These scores are synthetic examples for demonstration purposes only. They do not represent actual measured values.</p>
<p class="section-sub" style="background:#fff3cd;border:2px solid #ffc107;border-radius: var(--radius-md);padding:12px 16px;color:#856404;font-weight:600;"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> ILLUSTRATIVE SAMPLE DATA — These scores are synthetic examples for demonstration purposes only. They do not represent actual measured values.</p>
<div class="scores-table-wrap">
<table class="scores-table">
<thead>
@@ -622,8 +622,8 @@
<p class="section-sub" style="color:rgba(0,0,0,.75);margin:0 auto;max-width:600px">When official municipal development plans diverge from observed commercial activity, Landvex surfaces the conflict &mdash; not just the consensus.</p>
</div>
<div style="background:#fff3cd;border:1px solid #ffc107;border-radius:8px;padding:12px 16px;margin-bottom:20px;font-size:13px;font-weight:600;color:#856404;text-align:center">
ILLUSTRATIVE SAMPLE — not real intelligence
<div style="background:#fff3cd;border:1px solid #ffc107;border-radius: var(--radius-md);padding:12px 16px;margin-bottom:20px;font-size:13px;font-weight:600;color:#856404;text-align:center">
ILLUSTRATIVE SAMPLE — not real intelligence
</div>
<div class="contradiction-box">
<div class="conflict-header">
@@ -640,18 +640,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; Municipal investment zone: Järva priority growth</li>
<li>&#10003; Norrmalm: stable CBD with anchor tenants</li>
<li>&#10003; Development permits: 14 approved 2024&ndash;2025</li>
<li>&#100003 Municipal investment zone: Järva priority growth</li>
<li>&#100003 Norrmalm: stable CBD with anchor tenants</li>
<li>&#100003 Development permits: 14 approved 2024&ndash;2025</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; Järva commercial vacancy above city average</li>
<li>&#10007; Norrmalm: 3 anchor retailers downsizing footprint</li>
<li>&#10007; Development activity below permit volume</li>
<li>&#10007; Nacka outperforming planned growth projections</li>
<li>&#100007 Järva commercial vacancy above city average</li>
<li>&#100007 Norrmalm: 3 anchor retailers downsizing footprint</li>
<li>&#100007 Development activity below permit volume</li>
<li>&#100007 Nacka outperforming planned growth projections</li>
</ul>
</div>
</div>
+13 -13
View File
@@ -181,7 +181,7 @@
display: flex; align-items: center; gap: 10px;
}
.district-chip .chip-dot {
width: 8px; height: 8px; border-radius: 50%;
width: 8px; height: 8px; border-radius: var(--radius-full);
background: var(--blue); flex-shrink: 0;
}
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
@@ -252,7 +252,7 @@
.contradiction-box {
background: rgba(0,0,0,.05);
border: 1px solid rgba(0,0,0,.1);
border-radius: 16px; padding: 36px 40px; margin-bottom: 32px;
border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px;
}
.conflict-header {
@@ -266,7 +266,7 @@
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
@@ -300,7 +300,7 @@
.ce-feature p { color: rgba(0,0,0,.75); font-size: .8125rem; }
.cta-banner {
background: #e8f0fe; border-radius: 16px; padding: 40px 48px;
background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px;
display: flex; align-items: center; gap: 48px; flex-wrap: wrap;
margin-top: 48px;
}
@@ -350,7 +350,7 @@
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social {
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px;
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
@@ -385,7 +385,7 @@
.scores-grid { grid-template-columns: 1fr !important; }
}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius:8px;line-height:1.6;max-width:800px}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius: var(--radius-md);line-height:1.6;max-width:800px}
.lv-disclaimer a{color:rgba(0,0,0,.65);text-decoration:underline}
</style>
@@ -613,18 +613,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; Innere Stadt: UNESCO heritage zone with stable premium retail</li>
<li>&#10003; Favoriten: designated affordable housing growth corridor</li>
<li>&#10003; Donaustadt: smart city expansion proceeding to plan</li>
<li>&#100003 Innere Stadt: UNESCO heritage zone with stable premium retail</li>
<li>&#100003 Favoriten: designated affordable housing growth corridor</li>
<li>&#100003 Donaustadt: smart city expansion proceeding to plan</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; Innere Stadt: international tourism dependency creating fragility</li>
<li>&#10007; Favoriten: development pace below municipal targets</li>
<li>&#10007; Commercial vacancy emerging in secondary Favoriten strips</li>
<li>&#10007; Donaustadt tech cluster attracting investment ahead of schedule</li>
<li>&#100007 Innere Stadt: international tourism dependency creating fragility</li>
<li>&#100007 Favoriten: development pace below municipal targets</li>
<li>&#100007 Commercial vacancy emerging in secondary Favoriten strips</li>
<li>&#100007 Donaustadt tech cluster attracting investment ahead of schedule</li>
</ul>
</div>
</div>
+13 -13
View File
@@ -181,7 +181,7 @@
display: flex; align-items: center; gap: 10px;
}
.district-chip .chip-dot {
width: 8px; height: 8px; border-radius: 50%;
width: 8px; height: 8px; border-radius: var(--radius-full);
background: var(--blue); flex-shrink: 0;
}
.district-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
@@ -252,7 +252,7 @@
.contradiction-box {
background: rgba(0,0,0,.05);
border: 1px solid rgba(0,0,0,.1);
border-radius: 16px; padding: 36px 40px; margin-bottom: 32px;
border-radius: var(--radius-lg); padding: 36px 40px; margin-bottom: 32px;
}
.conflict-header {
@@ -266,7 +266,7 @@
.conflict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.conflict-side { border-radius: 10px; padding: 16px; }
.conflict-side { border-radius: var(--radius-md); padding: 16px; }
.conflict-official { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.2); }
.conflict-observed { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.2); }
@@ -300,7 +300,7 @@
.ce-feature p { color: rgba(0,0,0,.75); font-size: .8125rem; }
.cta-banner {
background: #e8f0fe; border-radius: 16px; padding: 40px 48px;
background: #e8f0fe; border-radius: var(--radius-lg); padding: 40px 48px;
display: flex; align-items: center; gap: 48px; flex-wrap: wrap;
margin-top: 48px;
}
@@ -350,7 +350,7 @@
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social {
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px;
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
@@ -385,7 +385,7 @@
.scores-grid { grid-template-columns: 1fr !important; }
}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius:8px;line-height:1.6;max-width:800px}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius: var(--radius-md);line-height:1.6;max-width:800px}
.lv-disclaimer a{color:rgba(0,0,0,.65);text-decoration:underline}
</style>
@@ -613,18 +613,18 @@
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul>
<li>&#10003; City / Altstadt: premium commercial core, rated fully stable</li>
<li>&#10003; Schwamendingen: affordable district with long-term growth mandate</li>
<li>&#10003; Infrastructure maintenance rated best-in-class nationally</li>
<li>&#100003 City / Altstadt: premium commercial core, rated fully stable</li>
<li>&#100003 Schwamendingen: affordable district with long-term growth mandate</li>
<li>&#100003 Infrastructure maintenance rated best-in-class nationally</li>
</ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul>
<li>&#10007; City / Altstadt: office vacancy ticking upward post-remote shift</li>
<li>&#10007; Schwamendingen: investment slower than mandate implies</li>
<li>&#10007; Altstetten emerging as commercial overflow beneficiary</li>
<li>&#10007; Retail footfall in Altstadt below 2019 baseline</li>
<li>&#100007 City / Altstadt: office vacancy ticking upward post-remote shift</li>
<li>&#100007 Schwamendingen: investment slower than mandate implies</li>
<li>&#100007 Altstetten emerging as commercial overflow beneficiary</li>
<li>&#100007 Retail footfall in Altstadt below 2019 baseline</li>
</ul>
</div>
</div>