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
@@ -167,7 +167,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;
}
@@ -187,7 +187,7 @@
[style*="grid-template-columns:repeat(auto-fit,minmax(260px"] { 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}
@@ -263,32 +263,32 @@
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(300px,100%),1fr));gap:20px">
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px">🏗️</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 14H14M4 14V8L8 4L12 8V14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 14V10H10V14" stroke="#666" stroke-width="1.5"/></svg></div>
<h3 style="font-size:1rem;font-weight:700;color:#0a0a0a;margin-bottom:6px">Construction activity</h3>
<p style="font-size:.875rem;color:#6b7280;line-height:1.6">Volume and intensity of active construction, scaffolding, site preparation and demolition observed in the field.</p>
</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px">📄</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px"></div>
<h3 style="font-size:1rem;font-weight:700;color:#0a0a0a;margin-bottom:6px">Permit signals</h3>
<p style="font-size:.875rem;color:#6b7280;line-height:1.6">Observable evidence of planning permissions — site hoardings, notices, contractor presence and groundworks.</p>
</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px">🏪</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px"></div>
<h3 style="font-size:1rem;font-weight:700;color:#0a0a0a;margin-bottom:6px">Commercial openings</h3>
<p style="font-size:.875rem;color:#6b7280;line-height:1.6">New business formations and commercial fitouts as a leading indicator of investment confidence.</p>
</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px">💰</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px"></div>
<h3 style="font-size:1rem;font-weight:700;color:#0a0a0a;margin-bottom:6px">Capital flows</h3>
<p style="font-size:.875rem;color:#6b7280;line-height:1.6">Indirect signals of capital allocation — scaffolding, premium renovations, branded development sites.</p>
</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px">🚶</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px"></div>
<h3 style="font-size:1rem;font-weight:700;color:#0a0a0a;margin-bottom:6px">Population movement</h3>
<p style="font-size:.875rem;color:#6b7280;line-height:1.6">Observable demographic shifts — new residents, changing consumer mix, service infrastructure evolution.</p>
</div>
@@ -303,9 +303,9 @@
<div class="section-label">Use cases</div>
<h2 class="section-title section-title-light" style="margin-bottom:24px">Who uses this index.</h2>
<div style="display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin-bottom:40px">
<span style="background:#e0eaff;color:#0052cc;font-size:.8125rem;font-weight:600;padding:6px 14px;border-radius:100px">Real estate development</span>
<span style="background:#e0eaff;color:#0052cc;font-size:.8125rem;font-weight:600;padding:6px 14px;border-radius:100px">Investment allocation</span>
<span style="background:#e0eaff;color:#0052cc;font-size:.8125rem;font-weight:600;padding:6px 14px;border-radius:100px">Urban planning</span>
<span style="background:#e0eaff;color:#0052cc;font-size:.8125rem;font-weight:600;padding:6px 14px;border-radius: 100px">Real estate development</span>
<span style="background:#e0eaff;color:#0052cc;font-size:.8125rem;font-weight:600;padding:6px 14px;border-radius: 100px">Investment allocation</span>
<span style="background:#e0eaff;color:#0052cc;font-size:.8125rem;font-weight:600;padding:6px 14px;border-radius: 100px">Urban planning</span>
</div>
</div>
</div>
@@ -318,7 +318,7 @@
<div class="section-label">Sample output</div>
<h2 class="section-title" style="margin-bottom:8px">What you receive.</h2>
<p style="color:var(--text-dim);margin-bottom:48px">Development score + cycle stage + investment confidence</p>
<div style="background:var(--surface-2);border:1px solid var(--border);border-radius:16px;padding:36px 40px;text-align:left">
<div style="background:var(--surface-2);border:1px solid var(--border);border-radius: var(--radius-lg);padding:36px 40px;text-align:left">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
<span style="color:var(--text-dim);font-size:.875rem;font-weight:600">Sample location · Placeholder score</span>
<span style="color:#4ade80;font-size:.875rem;font-weight:700">↑ Cycle accelerating</span>
@@ -328,8 +328,8 @@
<span style="font-size:1.5rem;color:var(--text-dim)">/100</span>
</div>
<div style="background:#f3f4f6;border-radius:8px;height:12px;width:100%;margin:12px 0 8px;overflow:hidden">
<div style="height:100%;width:67%;background:#fbbf24;border-radius:8px;transition:width 1s ease"></div>
<div style="background:#f3f4f6;border-radius: var(--radius-md);height:12px;width:100%;margin:12px 0 8px;overflow:hidden">
<div style="height:100%;width:67%;background:#fbbf24;border-radius: var(--radius-md);transition:width 1s ease"></div>
</div>
<div style="display:flex;justify-content:space-between;font-size:.75rem;color:var(--text-dim)">
@@ -352,29 +352,29 @@
<h2 class="section-title">How this index detects narrative conflicts.</h2>
<p style="color:rgba(255,255,255,.75);max-width:600px;margin:0 auto;line-height:1.7">Development Forecast detects when official planning data diverges from observed ground activity. A district listed as "under development" in permits with no observable site activity, or conversely, rapid development activity ahead of formal permissions — both trigger narrative conflicts that change the forecast score.</p>
</div>
<div style="background:rgba(0,0,0,.05);border:1px solid rgba(0,0,0,.1);border-radius:16px;padding:32px 36px">
<div style="background:rgba(0,0,0,.05);border:1px solid rgba(0,0,0,.1);border-radius: var(--radius-lg);padding:32px 36px">
<div style="display:flex;align-items:flex-start;gap:16px;margin-bottom:24px">
<span style="font-size:1.5rem">🚩</span>
<span style="font-size:1.5rem"></span>
<div>
<div style="color:#f87171;font-weight:800;font-size:.9375rem;margin-bottom:4px">Contradiction Engine — How it works</div>
<div style="color:rgba(255,255,255,.75);font-size:.875rem;line-height:1.6">Field observations are continuously cross-referenced against official records, registers, and published statistics. Divergence above threshold triggers a conflict flag.</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<div style="background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.2);border-radius:10px;padding:16px">
<div style="background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.2);border-radius: var(--radius-md);padding:16px">
<div style="color:#86efac;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px">Official record</div>
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px">
<li style="color:rgba(255,255,255,.7);font-size:.875rem"> Published condition ratings</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"> Official statistics</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"> Register data</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Published condition ratings</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Official statistics</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Register data</li>
</ul>
</div>
<div style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.2);border-radius:10px;padding:16px">
<div style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.2);border-radius: var(--radius-md);padding:16px">
<div style="color:#fca5a5;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px">Observed reality</div>
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px">
<li style="color:rgba(255,255,255,.7);font-size:.875rem"> Field observations</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"> Real-time signals</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"> Contradiction flags</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Field observations</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Real-time signals</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Contradiction flags</li>
</ul>
</div>
</div>
@@ -392,20 +392,20 @@
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));gap:20px;max-width:900px;margin:0 auto">
<a href="/intelligence/urban-growth-index/" style="display:block;background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:20px 24px;text-decoration:none;color:inherit;transition:border-color .2s,transform .15s" onmouseover="this.style.borderColor='#0066ff';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='#e5e7eb';this.style.transform='none'">
<div style="font-size:1.25rem;margin-bottom:8px">🌱</div>
<a href="/intelligence/urban-growth-index/" style="display:block;background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:20px 24px;text-decoration:none;color:inherit;transition:border-color .2s,transform .15s" onmouseover="this.style.borderColor='#0066ff';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='#e5e7eb';this.style.transform='none'">
<div style="font-size:1.25rem;margin-bottom:8px"></div>
<div style="font-weight:700;color:#0a0a0a;font-size:.9375rem;margin-bottom:4px">Urban Growth Index</div>
<div style="color:#0066ff;font-size:.8125rem;font-weight:700">View index →</div>
</a>
<a href="/intelligence/investment-attractiveness-index/" style="display:block;background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:20px 24px;text-decoration:none;color:inherit;transition:border-color .2s,transform .15s" onmouseover="this.style.borderColor='#0066ff';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='#e5e7eb';this.style.transform='none'">
<div style="font-size:1.25rem;margin-bottom:8px">💎</div>
<a href="/intelligence/investment-attractiveness-index/" style="display:block;background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:20px 24px;text-decoration:none;color:inherit;transition:border-color .2s,transform .15s" onmouseover="this.style.borderColor='#0066ff';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='#e5e7eb';this.style.transform='none'">
<div style="font-size:1.25rem;margin-bottom:8px"></div>
<div style="font-weight:700;color:#0a0a0a;font-size:.9375rem;margin-bottom:4px">Investment Attractiveness Index</div>
<div style="color:#0066ff;font-size:.8125rem;font-weight:700">View index →</div>
</a>
<a href="/intelligence/commercial-opportunity-index/" style="display:block;background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:20px 24px;text-decoration:none;color:inherit;transition:border-color .2s,transform .15s" onmouseover="this.style.borderColor='#0066ff';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='#e5e7eb';this.style.transform='none'">
<div style="font-size:1.25rem;margin-bottom:8px">📈</div>
<a href="/intelligence/commercial-opportunity-index/" style="display:block;background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:20px 24px;text-decoration:none;color:inherit;transition:border-color .2s,transform .15s" onmouseover="this.style.borderColor='#0066ff';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='#e5e7eb';this.style.transform='none'">
<div style="font-size:1.25rem;margin-bottom:8px"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 12L6 8L9 11L14 5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 5H14V9" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<div style="font-weight:700;color:#0a0a0a;font-size:.9375rem;margin-bottom:4px">Commercial Opportunity Index</div>
<div style="color:#0066ff;font-size:.8125rem;font-weight:700">View index →</div>
</a>
@@ -503,7 +503,7 @@
});
if (res.ok || res.status === 201) {
msgEl.className = 'success';
msgEl.textContent = " Request received. We'll be in touch within one business day.";
msgEl.textContent = "<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Request received. We'll be in touch within one business day.";
msgEl.style.display = 'block';
this.reset();
} else { throw new Error('Server error ' + res.status); }