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>IQOA Condition Pre-Screening</h3>
<p>Field observations structured to support IQOA classification workflows. Identify 2E, 3 and 3U priority candidates before formal inspection deployment, enabling efficient use of CEREMA and DIR 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; IQOA last survey: 28 months ago, classification 2</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; Maintenance programme: on schedule</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; No restriction orders active</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 IQOA last survey: 28 months ago, classification 2</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 Maintenance programme: on schedule</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 No restriction 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; Advanced carbonation of concrete at three pier bases</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Pavement deformation above central span consistent with deck movement</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Joint sealant failure on four of six expansion joints</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Corrosion staining on soffit indicates active reinforcement attack</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Advanced carbonation of concrete at three pier bases</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Pavement deformation above central span consistent with deck movement</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Joint sealant failure on four of six expansion joints</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Corrosion staining on soffit indicates active reinforcement attack</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>DIN 1076 Condition Assessment Support</h3>
<p>Field observation data structured to complement DIN 1076 inspection requirements. Identify priority assets before the next mandatory Hauptpruefung cycle without deploying physical inspection teams.</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; Hauptpruefung completed 22 months ago: Zustandsnote 2.7</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; Maintenance programme: within schedule</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; No load restriction active</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 Hauptpruefung completed 22 months ago: Zustandsnote 2.7</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 Maintenance programme: within schedule</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 No load restriction 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; Chloride-induced rebar corrosion visible on east parapet</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Expansion joint displacement 18mm above threshold on two spans</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Active water seepage through deck on south abutment</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Bearing deterioration consistent with grade 3.5+ condition</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Chloride-induced rebar corrosion visible on east parapet</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Expansion joint displacement 18mm above threshold on two spans</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Active water seepage through deck on south abutment</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Bearing deterioration consistent with grade 3.5+ condition</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 Condition Monitoring Support</h3>
<p>Field observations structured to support NEN 2767 condition assessment workflows. Identify deterioration trends and priority assets between formal inspection intervals without physical inspection deployment.</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 last inspection: 19 months ago, condition score 2</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; RWS maintenance programme: within cycle</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; No restrictions active</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 NEN 2767 last inspection: 19 months ago, condition score 2</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 RWS maintenance programme: within cycle</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 No restrictions 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; Visible concrete cracking on three viaduct spans above threshold</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Bridge deck waterproofing failure on two sections</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Expansion joint displacement beyond tolerance at four locations</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Sediment accumulation blocking drainage outlets at two abutments</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Visible concrete cracking on three viaduct spans above threshold</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Bridge deck waterproofing failure on two sections</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Expansion joint displacement beyond tolerance at four locations</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Sediment accumulation blocking drainage outlets at two abutments</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>Structural Condition Assessment</h3>
<p>Systematic visual surveys of bridge decks, bearings, expansion joints, parapets, and approach slabs across the Swedish national and municipal bridge inventory. 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; Last inspection: 14 months ago, passed</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; Maintenance schedule: on track</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; No load restrictions active</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 Last inspection: 14 months ago, passed</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 Maintenance schedule: on track</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 No load restrictions 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; Visible spalling on three pier caps</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Expansion joint separation exceeds specification on two spans</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Approach slab subsidence visible on northbound carriageway</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Drainage outfall blockage confirmed at abutment E4-107</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Visible spalling on three pier caps</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Expansion joint separation exceeds specification on two spans</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Approach slab subsidence visible on northbound carriageway</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Drainage outfall blockage confirmed at abutment E4-107</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>DMRB-Aligned Condition Pre-Screening</h3>
<p>Field observations structured to support BD 63 and CS 450 inspection workflows. Identify priority assets before formal Principal or General Inspections without deploying physical teams to every structure.</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; Last Principal Inspection: 38 months ago, condition grade B</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; Maintenance programme: within programme</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10003; No interim restrictions active</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 Last Principal Inspection: 38 months ago, condition grade B</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 Maintenance programme: within programme</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100003 No interim restrictions 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; Visible concrete delamination on three spans approaching carriageway edge</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Bearing shelf corrosion consistent with grade C/D condition</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Half-joint cracking at two locations exceeds intervention threshold</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#10007; Retaining wall settlement visible at southbound approach</li></ul>
<ul><li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Visible concrete delamination on three spans approaching carriageway edge</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Bearing shelf corrosion consistent with grade C/D condition</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Half-joint cracking at two locations exceeds intervention threshold</li>
<li style='color:rgba(0,0,0,.75);font-size:.875rem;padding:4px 0;'>&#100007 Retaining wall settlement visible at southbound approach</li></ul>
</div>
</div>
<div class="conflict-footer">