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
@@ -69,16 +69,16 @@
.decisions-bg { background: var(--surface); }
.decisions-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; margin-top: 40px; }
.decision-chip { background: var(--bg-dark); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 18px 22px; display: flex; align-items: center; gap: 12px; }
.decision-chip .chip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--blue); flex-shrink: 0; }
.decision-chip .chip-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--blue); flex-shrink: 0; }
.decision-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
.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 { font-size: 1.5rem; color: #f87171; }
.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; }
@@ -92,13 +92,13 @@
.breadcrumb a { color: var(--text-muted); }
.breadcrumb a:hover { color: #1d1d1f; }
.breadcrumb .sep { color: var(--border); }
.comparison-table { width: 100%; border-collapse: collapse; margin-top: 32px; border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.08); }
.comparison-table { width: 100%; border-collapse: collapse; margin-top: 32px; border-radius: var(--radius-md); overflow: hidden; border: 1px solid rgba(0,0,0,0.08); }
.comparison-table th { padding: 14px 16px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em; text-align: left; background: rgba(0,0,0,0.04); color: var(--text-dim); }
.cta-section { background: #e8f0fe; }
.cta-box { border-radius: 16px; padding: 56px 48px; text-align: center; max-width: 680px; margin: 0 auto; }
.cta-box { border-radius: var(--radius-lg); padding: 56px 48px; text-align: center; max-width: 680px; margin: 0 auto; }
.cta-box h2 { font-size: clamp(24px,3vw,36px); font-weight: 800; color: #1d1d1f; margin-bottom: 16px; }
.cta-box p { font-size: 17px; color: rgba(0,0,0,.7); margin-bottom: 36px; line-height: 1.65; }
.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; }
footer { border-top: 1px solid var(--border); padding: 40px; }
.footer-inner { max-width: 1140px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 20px; }
@@ -108,7 +108,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; } .use-case-grid { grid-template-columns:1fr; } .decisions-grid { grid-template-columns:1fr 1fr; } .conflict-grid { grid-template-columns:1fr; } }
@media (max-width:600px) { section { padding:72px 20px; } .decisions-grid { grid-template-columns:1fr; } nav { padding:0 16px; } .hero h1 { letter-spacing:-1px; } .contradiction-box { padding:24px 20px; } }
@@ -150,7 +150,7 @@
<p class="section-sub">Every engagement is anchored to a specific decision. Select a capability to discuss how Landvex applies it to your context.</p>
<div class="use-case-grid">
<a class="use-case-card" href="/contact/" style="display:block;text-decoration:none;">
<span class="uc-icon" style="font-size:22px;display:block;margin-bottom:16px;color:#5599ff;">&#9650;</span>
<span class="uc-icon" style="font-size:22px;display:block;margin-bottom:16px;color:#5599ff;">&#965050</span>
<h3>IQRN/IQROA Condition Pre-Screening</h3>
<p>Field observations structured to support IQRN and IQROA assessment workflows. Identify priority sections before formal survey deployment, enabling targeted use of CEREMA and DIR survey resources.</p>
<div style="margin-top:14px;font-size:13px;font-weight:700;color:#0066ff;">Learn more &rarr;</div>
@@ -270,16 +270,16 @@
<div class="conflict-grid">
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; IQROA survey: 20 months ago, score C</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; Departmental maintenance programme: within schedule</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; No emergency repair orders active</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 IQROA survey: 20 months ago, score C</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 Departmental maintenance programme: within schedule</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 No emergency repair orders active</li></ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Longitudinal cracking on 4.2km inconsistent with C-score parameters</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Pothole density on 1.8km exceeds D-score threshold</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Drainage failures at 11 culverts outside maintenance programme</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Road marking fading below minimum retroreflectivity on 28km</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Longitudinal cracking on 4.2km inconsistent with C-score parameters</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Pothole density on 1.8km exceeds D-score threshold</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Drainage failures at 11 culverts outside maintenance programme</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Road marking fading below minimum retroreflectivity on 28km</li></ul>
</div>
</div>
<div class="conflict-footer">
@@ -69,16 +69,16 @@
.decisions-bg { background: var(--surface); }
.decisions-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; margin-top: 40px; }
.decision-chip { background: var(--bg-dark); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 18px 22px; display: flex; align-items: center; gap: 12px; }
.decision-chip .chip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--blue); flex-shrink: 0; }
.decision-chip .chip-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--blue); flex-shrink: 0; }
.decision-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
.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 { font-size: 1.5rem; color: #f87171; }
.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; }
@@ -92,13 +92,13 @@
.breadcrumb a { color: var(--text-muted); }
.breadcrumb a:hover { color: #1d1d1f; }
.breadcrumb .sep { color: var(--border); }
.comparison-table { width: 100%; border-collapse: collapse; margin-top: 32px; border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.08); }
.comparison-table { width: 100%; border-collapse: collapse; margin-top: 32px; border-radius: var(--radius-md); overflow: hidden; border: 1px solid rgba(0,0,0,0.08); }
.comparison-table th { padding: 14px 16px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em; text-align: left; background: rgba(0,0,0,0.04); color: var(--text-dim); }
.cta-section { background: #e8f0fe; }
.cta-box { border-radius: 16px; padding: 56px 48px; text-align: center; max-width: 680px; margin: 0 auto; }
.cta-box { border-radius: var(--radius-lg); padding: 56px 48px; text-align: center; max-width: 680px; margin: 0 auto; }
.cta-box h2 { font-size: clamp(24px,3vw,36px); font-weight: 800; color: #1d1d1f; margin-bottom: 16px; }
.cta-box p { font-size: 17px; color: rgba(0,0,0,.7); margin-bottom: 36px; line-height: 1.65; }
.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; }
footer { border-top: 1px solid var(--border); padding: 40px; }
.footer-inner { max-width: 1140px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 20px; }
@@ -108,7 +108,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; } .use-case-grid { grid-template-columns:1fr; } .decisions-grid { grid-template-columns:1fr 1fr; } .conflict-grid { grid-template-columns:1fr; } }
@media (max-width:600px) { section { padding:72px 20px; } .decisions-grid { grid-template-columns:1fr; } nav { padding:0 16px; } .hero h1 { letter-spacing:-1px; } .contradiction-box { padding:24px 20px; } }
@@ -150,7 +150,7 @@
<p class="section-sub">Every engagement is anchored to a specific decision. Select a capability to discuss how Landvex applies it to your context.</p>
<div class="use-case-grid">
<a class="use-case-card" href="/contact/" style="display:block;text-decoration:none;">
<span class="uc-icon" style="font-size:22px;display:block;margin-bottom:16px;color:#5599ff;">&#9650;</span>
<span class="uc-icon" style="font-size:22px;display:block;margin-bottom:16px;color:#5599ff;">&#965050</span>
<h3>ZEB-Aligned Pavement Assessment</h3>
<p>Field observations structured to supplement ZEB survey data. Identify priority sections for Autobahn GmbH and Landesstrassenbehoerden between formal ZEB cycles — without deploying survey vehicles.</p>
<div style="margin-top:14px;font-size:13px;font-weight:700;color:#0066ff;">Learn more &rarr;</div>
@@ -270,16 +270,16 @@
<div class="conflict-grid">
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; ZEB survey: 31 months ago, PCR value 1.6</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; Maintenance programme: within ZEB cycle</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; No emergency orders active</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 ZEB survey: 31 months ago, PCR value 1.6</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 Maintenance programme: within ZEB cycle</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 No emergency orders active</li></ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Rut depth exceeding ZEB trigger threshold on 5.4km</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Active cracking patterns consistent with PCR 2.5+ on three sections</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Safety barrier corrosion failure at six locations</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Leitpfosten damage on 12km unreported in maintenance register</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Rut depth exceeding ZEB trigger threshold on 5.4km</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Active cracking patterns consistent with PCR 2.5+ on three sections</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Safety barrier corrosion failure at six locations</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Leitpfosten damage on 12km unreported in maintenance register</li></ul>
</div>
</div>
<div class="conflict-footer">
@@ -69,16 +69,16 @@
.decisions-bg { background: var(--surface); }
.decisions-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; margin-top: 40px; }
.decision-chip { background: var(--bg-dark); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 18px 22px; display: flex; align-items: center; gap: 12px; }
.decision-chip .chip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--blue); flex-shrink: 0; }
.decision-chip .chip-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--blue); flex-shrink: 0; }
.decision-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
.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 { font-size: 1.5rem; color: #f87171; }
.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; }
@@ -92,13 +92,13 @@
.breadcrumb a { color: var(--text-muted); }
.breadcrumb a:hover { color: #1d1d1f; }
.breadcrumb .sep { color: var(--border); }
.comparison-table { width: 100%; border-collapse: collapse; margin-top: 32px; border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.08); }
.comparison-table { width: 100%; border-collapse: collapse; margin-top: 32px; border-radius: var(--radius-md); overflow: hidden; border: 1px solid rgba(0,0,0,0.08); }
.comparison-table th { padding: 14px 16px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em; text-align: left; background: rgba(0,0,0,0.04); color: var(--text-dim); }
.cta-section { background: #e8f0fe; }
.cta-box { border-radius: 16px; padding: 56px 48px; text-align: center; max-width: 680px; margin: 0 auto; }
.cta-box { border-radius: var(--radius-lg); padding: 56px 48px; text-align: center; max-width: 680px; margin: 0 auto; }
.cta-box h2 { font-size: clamp(24px,3vw,36px); font-weight: 800; color: #1d1d1f; margin-bottom: 16px; }
.cta-box p { font-size: 17px; color: rgba(0,0,0,.7); margin-bottom: 36px; line-height: 1.65; }
.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; }
footer { border-top: 1px solid var(--border); padding: 40px; }
.footer-inner { max-width: 1140px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 20px; }
@@ -108,7 +108,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; } .use-case-grid { grid-template-columns:1fr; } .decisions-grid { grid-template-columns:1fr 1fr; } .conflict-grid { grid-template-columns:1fr; } }
@media (max-width:600px) { section { padding:72px 20px; } .decisions-grid { grid-template-columns:1fr; } nav { padding:0 16px; } .hero h1 { letter-spacing:-1px; } .contradiction-box { padding:24px 20px; } }
@@ -150,7 +150,7 @@
<p class="section-sub">Every engagement is anchored to a specific decision. Select a capability to discuss how Landvex applies it to your context.</p>
<div class="use-case-grid">
<a class="use-case-card" href="/contact/" style="display:block;text-decoration:none;">
<span class="uc-icon" style="font-size:22px;display:block;margin-bottom:16px;color:#5599ff;">&#9650;</span>
<span class="uc-icon" style="font-size:22px;display:block;margin-bottom:16px;color:#5599ff;">&#965050</span>
<h3>NEN 2767 Road Condition Assessment</h3>
<p>Field observations structured to support NEN 2767 condition assessment workflows across the Dutch road network. Identify priority sections for Rijkswaterstaat and provincial authorities between formal inspection intervals.</p>
<div style="margin-top:14px;font-size:13px;font-weight:700;color:#0066ff;">Learn more &rarr;</div>
@@ -270,16 +270,16 @@
<div class="conflict-grid">
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; NEN 2767 inspection: 24 months ago, condition score 2</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; Provincial maintenance programme: within MJOP</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; No emergency repair orders active</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 NEN 2767 inspection: 24 months ago, condition score 2</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 Provincial maintenance programme: within MJOP</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 No emergency repair orders active</li></ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Rut depth exceeding NEN 2767 score 3 threshold on 2.8km</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Edge cracking patterns consistent with score 3+ on three sections</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Drainage failure at 9 culverts outside MJOP schedule</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Fietspad surface deterioration above score 3 on 5.6km</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Rut depth exceeding NEN 2767 score 3 threshold on 2.8km</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Edge cracking patterns consistent with score 3+ on three sections</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Drainage failure at 9 culverts outside MJOP schedule</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Fietspad surface deterioration above score 3 on 5.6km</li></ul>
</div>
</div>
<div class="conflict-footer">
@@ -69,16 +69,16 @@
.decisions-bg { background: var(--surface); }
.decisions-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; margin-top: 40px; }
.decision-chip { background: var(--bg-dark); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 18px 22px; display: flex; align-items: center; gap: 12px; }
.decision-chip .chip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--blue); flex-shrink: 0; }
.decision-chip .chip-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--blue); flex-shrink: 0; }
.decision-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
.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 { font-size: 1.5rem; color: #f87171; }
.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; }
@@ -92,13 +92,13 @@
.breadcrumb a { color: var(--text-muted); }
.breadcrumb a:hover { color: #1d1d1f; }
.breadcrumb .sep { color: var(--border); }
.comparison-table { width: 100%; border-collapse: collapse; margin-top: 32px; border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.08); }
.comparison-table { width: 100%; border-collapse: collapse; margin-top: 32px; border-radius: var(--radius-md); overflow: hidden; border: 1px solid rgba(0,0,0,0.08); }
.comparison-table th { padding: 14px 16px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em; text-align: left; background: rgba(0,0,0,0.04); color: var(--text-dim); }
.cta-section { background: #e8f0fe; }
.cta-box { border-radius: 16px; padding: 56px 48px; text-align: center; max-width: 680px; margin: 0 auto; }
.cta-box { border-radius: var(--radius-lg); padding: 56px 48px; text-align: center; max-width: 680px; margin: 0 auto; }
.cta-box h2 { font-size: clamp(24px,3vw,36px); font-weight: 800; color: #1d1d1f; margin-bottom: 16px; }
.cta-box p { font-size: 17px; color: rgba(0,0,0,.7); margin-bottom: 36px; line-height: 1.65; }
.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; }
footer { border-top: 1px solid var(--border); padding: 40px; }
.footer-inner { max-width: 1140px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 20px; }
@@ -108,7 +108,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; } .use-case-grid { grid-template-columns:1fr; } .decisions-grid { grid-template-columns:1fr 1fr; } .conflict-grid { grid-template-columns:1fr; } }
@media (max-width:600px) { section { padding:72px 20px; } .decisions-grid { grid-template-columns:1fr; } nav { padding:0 16px; } .hero h1 { letter-spacing:-1px; } .contradiction-box { padding:24px 20px; } }
@@ -150,7 +150,7 @@
<p class="section-sub">Every engagement is anchored to a specific decision. Select a capability to discuss how Landvex applies it to your context.</p>
<div class="use-case-grid">
<a class="use-case-card" href="/contact/" style="display:block;text-decoration:none;">
<span class="uc-icon" style="font-size:22px;display:block;margin-bottom:16px;color:#5599ff;">&#9650;</span>
<span class="uc-icon" style="font-size:22px;display:block;margin-bottom:16px;color:#5599ff;">&#965050</span>
<h3>Pavement Condition Assessment</h3>
<p>Systematic documentation of Swedish road surface conditions: cracking, rutting, surface defects, and IRI indicators across national and municipal networks. AI-reviewed, geo-tagged, and delivered as structured data.</p>
<div style="margin-top:14px;font-size:13px;font-weight:700;color:#0066ff;">Learn more &rarr;</div>
@@ -270,16 +270,16 @@
<div class="conflict-grid">
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; VTI road condition survey: 13 months ago, IRI 2.1</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; Maintenance programme: within standard</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; No emergency interventions logged</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 VTI road condition survey: 13 months ago, IRI 2.1</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 Maintenance programme: within standard</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 No emergency interventions logged</li></ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Transverse cracking on 6.8km not captured in last VTI survey</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Rutting exceeding Swedish standard on two 1.2km sections</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Gully blockages at 17 locations outside maintenance schedule</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Road marking retroreflectivity below NVV threshold on 14km</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Transverse cracking on 6.8km not captured in last VTI survey</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Rutting exceeding Swedish standard on two 1.2km sections</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Gully blockages at 17 locations outside maintenance schedule</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Road marking retroreflectivity below NVV threshold on 14km</li></ul>
</div>
</div>
<div class="conflict-footer">
@@ -69,16 +69,16 @@
.decisions-bg { background: var(--surface); }
.decisions-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; margin-top: 40px; }
.decision-chip { background: var(--bg-dark); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 18px 22px; display: flex; align-items: center; gap: 12px; }
.decision-chip .chip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--blue); flex-shrink: 0; }
.decision-chip .chip-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--blue); flex-shrink: 0; }
.decision-chip span { font-size: 14px; font-weight: 600; color: var(--text-light); }
.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 { font-size: 1.5rem; color: #f87171; }
.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; }
@@ -92,13 +92,13 @@
.breadcrumb a { color: var(--text-muted); }
.breadcrumb a:hover { color: #1d1d1f; }
.breadcrumb .sep { color: var(--border); }
.comparison-table { width: 100%; border-collapse: collapse; margin-top: 32px; border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.08); }
.comparison-table { width: 100%; border-collapse: collapse; margin-top: 32px; border-radius: var(--radius-md); overflow: hidden; border: 1px solid rgba(0,0,0,0.08); }
.comparison-table th { padding: 14px 16px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em; text-align: left; background: rgba(0,0,0,0.04); color: var(--text-dim); }
.cta-section { background: #e8f0fe; }
.cta-box { border-radius: 16px; padding: 56px 48px; text-align: center; max-width: 680px; margin: 0 auto; }
.cta-box { border-radius: var(--radius-lg); padding: 56px 48px; text-align: center; max-width: 680px; margin: 0 auto; }
.cta-box h2 { font-size: clamp(24px,3vw,36px); font-weight: 800; color: #1d1d1f; margin-bottom: 16px; }
.cta-box p { font-size: 17px; color: rgba(0,0,0,.7); margin-bottom: 36px; line-height: 1.65; }
.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; }
footer { border-top: 1px solid var(--border); padding: 40px; }
.footer-inner { max-width: 1140px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 20px; }
@@ -108,7 +108,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; } .use-case-grid { grid-template-columns:1fr; } .decisions-grid { grid-template-columns:1fr 1fr; } .conflict-grid { grid-template-columns:1fr; } }
@media (max-width:600px) { section { padding:72px 20px; } .decisions-grid { grid-template-columns:1fr; } nav { padding:0 16px; } .hero h1 { letter-spacing:-1px; } .contradiction-box { padding:24px 20px; } }
@@ -150,7 +150,7 @@
<p class="section-sub">Every engagement is anchored to a specific decision. Select a capability to discuss how Landvex applies it to your context.</p>
<div class="use-case-grid">
<a class="use-case-card" href="/contact/" style="display:block;text-decoration:none;">
<span class="uc-icon" style="font-size:22px;display:block;margin-bottom:16px;color:#5599ff;">&#9650;</span>
<span class="uc-icon" style="font-size:22px;display:block;margin-bottom:16px;color:#5599ff;">&#965050</span>
<h3>SCANNER and CVI Pre-Screening</h3>
<p>Field observations structured to support SCANNER, SCRIM, and CVI inspection workflows. Identify priority sections before formal survey deployment, enabling targeted allocation of highway inspection resources.</p>
<div style="margin-top:14px;font-size:13px;font-weight:700;color:#0066ff;">Learn more &rarr;</div>
@@ -270,16 +270,16 @@
<div class="conflict-grid">
<div class="conflict-side conflict-official">
<div class="conflict-side-label">Official narrative</div>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; SCANNER survey: 16 months ago, RCI green</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; Highway maintenance programme: within budget</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; No section 38 emergency repair orders</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 SCANNER survey: 16 months ago, RCI green</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 Highway maintenance programme: within budget</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 No section 38 emergency repair orders</li></ul>
</div>
<div class="conflict-side conflict-observed">
<div class="conflict-side-label">Observed reality</div>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Carriageway cracking at 15 locations exceeding CVI defect threshold</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Edge deterioration on 9.2km inconsistent with green RCI rating</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Gully blockages at 31 locations outside maintenance schedule</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Road marking retroreflectivity below DMRB threshold on 17km</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Carriageway cracking at 15 locations exceeding CVI defect threshold</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Edge deterioration on 9.2km inconsistent with green RCI rating</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Gully blockages at 31 locations outside maintenance schedule</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Road marking retroreflectivity below DMRB threshold on 17km</li></ul>
</div>
</div>
<div class="conflict-footer">