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
+101 -101
View File
@@ -11,10 +11,10 @@
<title>LandveX Admin — Infrastrukturportal</title>
<link rel="stylesheet" >
<style>
/* ═══════════════════════════════════════════
/*
LANDVEX ADMIN PORTAL — iOS Native
Wavult Group · 2026
═══════════════════════════════════════════ */
*/
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-font-smoothing: antialiased; }
@@ -25,7 +25,7 @@ body {
min-height: 100vh;
}
/* ── NAVIGATION BAR ── */
/* NAVIGATION BAR */
.nav-bar {
position: sticky;
top: 0;
@@ -60,7 +60,7 @@ body {
width: 28px;
height: 28px;
background: linear-gradient(135deg, #007AFF 0%, #0055CC 100%);
border-radius: 7px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
@@ -76,18 +76,18 @@ body {
padding: 3px 8px;
background: rgba(255, 149, 0, 0.15);
color: var(--ios-orange);
border-radius: 20px;
border-radius: var(--radius-xl);
letter-spacing: 0.3px;
}
/* ── CONTENT ── */
/* CONTENT */
.content {
padding: 0 16px 120px;
max-width: 1100px;
margin: 0 auto;
}
/* ── SECTION HEADER ── */
/* SECTION HEADER */
.section-header {
padding: 20px 0 8px;
font-family: var(--font-display);
@@ -111,7 +111,7 @@ body {
padding-bottom: 12px;
}
/* ── METRICS GRID ── */
/* METRICS GRID */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
@@ -121,7 +121,7 @@ body {
.metric-card {
background: var(--background-primary);
border-radius: 16px;
border-radius: var(--radius-lg);
padding: 16px;
border: 0.5px solid var(--separator);
}
@@ -156,10 +156,10 @@ body {
.metric-orange .metric-value { color: var(--ios-orange); }
.metric-red .metric-value { color: var(--ios-red); }
/* ── TABLE ── */
/* TABLE */
.table-card {
background: var(--background-primary);
border-radius: 16px;
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
overflow: hidden;
margin-bottom: 12px;
@@ -237,7 +237,7 @@ tbody td {
margin-top: 2px;
}
/* ── STATUS BADGES ── */
/* STATUS BADGES */
.status-badge {
display: inline-flex;
align-items: center;
@@ -245,7 +245,7 @@ tbody td {
font-size: 12px;
font-weight: 600;
padding: 4px 10px;
border-radius: 20px;
border-radius: var(--radius-xl);
}
.status-ok {
@@ -271,11 +271,11 @@ tbody td {
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
border-radius: var(--radius-full);
background: currentColor;
}
/* ── RISK SCORE ── */
/* RISK SCORE */
.risk-bar-wrap {
display: flex;
align-items: center;
@@ -286,14 +286,14 @@ tbody td {
flex: 1;
height: 4px;
background: var(--fill-tertiary);
border-radius: 2px;
border-radius: var(--radius-sm);
overflow: hidden;
min-width: 60px;
}
.risk-fill {
height: 100%;
border-radius: 2px;
border-radius: var(--radius-sm);
transition: width 0.3s;
}
@@ -308,10 +308,10 @@ tbody td {
text-align: right;
}
/* ── MAP PLACEHOLDER ── */
/* MAP PLACEHOLDER */
.map-card {
background: var(--background-primary);
border-radius: 16px;
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
overflow: hidden;
margin-bottom: 12px;
@@ -355,7 +355,7 @@ tbody td {
position: absolute;
width: 28px;
height: 28px;
border-radius: 50% 50% 50% 0;
border-radius: var(--radius-full) 50% 50% 0;
transform: rotate(-45deg);
display: flex;
align-items: center;
@@ -383,7 +383,7 @@ tbody td {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-radius: 12px;
border-radius: var(--radius-md);
padding: 8px 12px;
border: 0.5px solid var(--separator);
display: flex;
@@ -401,10 +401,10 @@ tbody td {
.legend-dot {
width: 8px;
height: 8px;
border-radius: 50%;
border-radius: var(--radius-full);
}
/* ── FAB BUTTON ── */
/* FAB BUTTON */
.fab {
position: fixed;
bottom: calc(24px + env(safe-area-inset-bottom));
@@ -437,10 +437,10 @@ tbody td {
box-shadow: 0 2px 12px rgba(0, 122, 255, 0.35);
}
/* ── INSPECTION CELLS ── */
/* INSPECTION CELLS */
.inspection-list {
background: var(--background-primary);
border-radius: 16px;
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
overflow: hidden;
}
@@ -461,7 +461,7 @@ tbody td {
.inspection-icon {
width: 40px;
height: 40px;
border-radius: 10px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
@@ -492,7 +492,7 @@ tbody td {
flex-shrink: 0;
}
/* ── MODAL ── */
/* MODAL */
.modal-overlay {
position: fixed;
inset: 0;
@@ -514,7 +514,7 @@ tbody td {
.modal-sheet {
background: var(--background-primary);
border-radius: 20px 20px 0 0;
border-radius: var(--radius-xl) 20px 0 0;
width: 100%;
max-width: 640px;
padding: 8px 0 env(safe-area-inset-bottom);
@@ -530,7 +530,7 @@ tbody td {
width: 36px;
height: 4px;
background: var(--fill-primary);
border-radius: 2px;
border-radius: var(--radius-sm);
margin: 8px auto 16px;
}
@@ -559,7 +559,7 @@ tbody td {
padding: 10px 12px;
background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: 10px;
border-radius: var(--radius-md);
font-size: 15px;
font-family: var(--font-text);
color: var(--label-primary);
@@ -576,7 +576,7 @@ tbody td {
padding: 10px 12px;
background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: 10px;
border-radius: var(--radius-md);
font-size: 15px;
font-family: var(--font-text);
color: var(--label-primary);
@@ -597,7 +597,7 @@ tbody td {
background: var(--ios-blue);
color: white;
border: none;
border-radius: 14px;
border-radius: var(--radius-lg);
font-size: 17px;
font-weight: 600;
font-family: var(--font-text);
@@ -613,7 +613,7 @@ tbody td {
background: var(--fill-secondary);
color: var(--label-primary);
border: none;
border-radius: 14px;
border-radius: var(--radius-lg);
font-size: 17px;
font-weight: 600;
font-family: var(--font-text);
@@ -623,7 +623,7 @@ tbody td {
.btn-secondary:active { opacity: 0.7; }
/* ── TAB BAR ── */
/* TAB BAR */
.tab-bar {
position: fixed;
bottom: 0;
@@ -661,12 +661,12 @@ tbody td {
.tab-item.active .tab-label { color: var(--ios-blue); }
.tab-item.active .tab-icon { filter: none; }
/* ── DARK MODE ── */
/* DARK MODE */
@media (prefers-color-scheme: dark) {
.map-container { background: #1A1E23; }
}
/* ── RESPONSIVE ── */
/* RESPONSIVE */
@media (min-width: 768px) {
.metrics-grid { grid-template-columns: repeat(4, 1fr); }
.content { padding: 0 24px 80px; }
@@ -710,7 +710,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div style="display:flex;align-items:center;gap:8px;">
<span class="badge-admin">ADMIN</span>
<div style="width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#007AFF,#0055CC);display:flex;align-items:center;justify-content:center;color:white;font-size:14px;font-weight:700;">A</div>
<div style="width:32px;height:32px;border-radius: var(--radius-full);background:linear-gradient(135deg,#007AFF,#0055CC);display:flex;align-items:center;justify-content:center;color:white;font-size:14px;font-weight:700;">A</div>
</div>
</nav>
@@ -752,37 +752,37 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- LIVE MAP WITH ZOOMER POSITIONS -->
<div class="map-card" style="margin-bottom:12px;">
<div class="table-header">
<span class="table-title">📍 Realtids zoomers + objekt</span>
<span class="table-title"> Realtids zoomers + objekt</span>
<button class="table-action" onclick="cycleZoomerPos()">↻ Uppdatera</button>
</div>
<div class="map-container" id="map" style="position:relative;min-height:260px;">
<div class="map-grid"></div>
<!-- Object pins -->
<div class="map-pin map-pin-ok" style="left:22%;top:30%;" title="Gamla Stan Bron · OK"><div class="map-pin-inner"></div></div>
<div class="map-pin map-pin-ok" style="left:22%;top:30%;" title="Gamla Stan Bron · OK"><div class="map-pin-inner"><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></div></div>
<div class="map-pin map-pin-warn" style="left:45%;top:45%;" title="E4 Avsnitt 12 · Varning"><div class="map-pin-inner">!</div></div>
<div class="map-pin map-pin-critical" style="left:68%;top:25%;" title="E4 Älvsjö · Kritisk"><div class="map-pin-inner"></div></div>
<div class="map-pin map-pin-ok" style="left:35%;top:65%;" title="Arlanda Terminal 5 · OK"><div class="map-pin-inner"></div></div>
<div class="map-pin map-pin-critical" style="left:68%;top:25%;" title="E4 Älvsjö · Kritisk"><div class="map-pin-inner"></div></div>
<div class="map-pin map-pin-ok" style="left:35%;top:65%;" title="Arlanda Terminal 5 · OK"><div class="map-pin-inner"><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></div></div>
<div class="map-pin map-pin-warn" style="left:80%;top:55%;" title="Öresundsbron · Varning"><div class="map-pin-inner">!</div></div>
<div class="map-pin map-pin-ok" style="left:55%;top:72%;" title="Göta Älvbron · OK"><div class="map-pin-inner"></div></div>
<div class="map-pin map-pin-ok" style="left:55%;top:72%;" title="Göta Älvbron · OK"><div class="map-pin-inner"><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></div></div>
<!-- Live zoomer pins -->
<div id="zoomer-lx01" style="position:absolute;left:48%;top:42%;z-index:10;transition:all 2s ease;">
<div style="background:var(--ios-blue);color: #1d1d1f;border-radius:50%;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;box-shadow:0 2px 8px rgba(0,122,255,0.5);animation:zoomer-pulse 2s infinite;">🚁</div>
<div style="font-size:9px;font-weight:700;color:var(--ios-blue);text-align:center;margin-top:2px;background:rgba(0,0,0,0.9);border-radius:6px;padding:1px 4px;">LX-01</div>
<div style="background:var(--ios-blue);color: #1d1d1f;border-radius: var(--radius-full);width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;box-shadow:0 2px 8px rgba(0,122,255,0.5);animation:zoomer-pulse 2s infinite;"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg></div>
<div style="font-size:9px;font-weight:700;color:var(--ios-blue);text-align:center;margin-top:2px;background:rgba(0,0,0,0.9);border-radius: var(--radius-sm);padding:1px 4px;">LX-01</div>
</div>
<div id="zoomer-lx02" style="position:absolute;left:24%;top:27%;z-index:10;transition:all 2s ease;">
<div style="background:var(--ios-green);color: #1d1d1f;border-radius:50%;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;box-shadow:0 2px 8px rgba(52,199,89,0.5);">🚁</div>
<div style="font-size:9px;font-weight:700;color:var(--ios-green);text-align:center;margin-top:2px;background:rgba(0,0,0,0.9);border-radius:6px;padding:1px 4px;">LX-02</div>
<div style="background:var(--ios-green);color: #1d1d1f;border-radius: var(--radius-full);width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;box-shadow:0 2px 8px rgba(52,199,89,0.5);"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg></div>
<div style="font-size:9px;font-weight:700;color:var(--ios-green);text-align:center;margin-top:2px;background:rgba(0,0,0,0.9);border-radius: var(--radius-sm);padding:1px 4px;">LX-02</div>
</div>
<div id="zoomer-lx03" style="position:absolute;left:72%;top:58%;z-index:10;transition:all 2s ease;">
<div style="background:var(--ios-orange);color: #1d1d1f;border-radius:50%;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;box-shadow:0 2px 8px rgba(255,149,0,0.5);">🚁</div>
<div style="font-size:9px;font-weight:700;color:var(--ios-orange);text-align:center;margin-top:2px;background:rgba(0,0,0,0.9);border-radius:6px;padding:1px 4px;">LX-03 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
<div style="background:var(--ios-orange);color: #1d1d1f;border-radius: var(--radius-full);width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;box-shadow:0 2px 8px rgba(255,149,0,0.5);"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg></div>
<div style="font-size:9px;font-weight:700;color:var(--ios-orange);text-align:center;margin-top:2px;background:rgba(0,0,0,0.9);border-radius: var(--radius-sm);padding:1px 4px;">LX-03 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
</div>
<!-- Live indicator -->
<div style="position:absolute;top:10px;left:10px;z-index:11;display:flex;align-items:center;gap:5px;background:rgba(0,0,0,0.9);border-radius:20px;padding:4px 10px;font-size:11px;font-weight:600;color:var(--ios-green);">
<div style="width:6px;height:6px;border-radius:50%;background:var(--ios-green);animation:zoomer-pulse 2s infinite;"></div>
<div style="position:absolute;top:10px;left:10px;z-index:11;display:flex;align-items:center;gap:5px;background:rgba(0,0,0,0.9);border-radius: var(--radius-xl);padding:4px 10px;font-size:11px;font-weight:600;color:var(--ios-green);">
<div style="width:6px;height:6px;border-radius: var(--radius-full);background:var(--ios-green);animation:zoomer-pulse 2s infinite;"></div>
LIVE
</div>
@@ -790,7 +790,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="legend-item"><div class="legend-dot" style="background:var(--ios-green)"></div>OK (189)</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--ios-orange)"></div>Varning (41)</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--ios-red)"></div>Kritisk (17)</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--ios-blue)"></div>🚁 Zoomer (3)</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--ios-blue)"></div><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg> Zoomer (3)</div>
</div>
</div>
</div>
@@ -801,7 +801,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="metric-card metric-green">
<div class="metric-value">14</div>
<div class="metric-label">Pågående</div>
<div class="metric-trend trend-up">🚁 Aktiva uppdrag</div>
<div class="metric-trend trend-up"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg> Aktiva uppdrag</div>
</div>
<div class="metric-card metric-blue">
<div class="metric-value">218</div>
@@ -818,7 +818,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- ACTIVE MISSIONS TABLE -->
<div class="table-card" style="margin-bottom:12px;">
<div class="table-header">
<span class="table-title"> Pågående uppdrag</span>
<span class="table-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> Pågående uppdrag</span>
<button class="table-action" onclick="openDispatch()">Snabb-dispatch →</button>
</div>
<div class="table-scroll">
@@ -828,12 +828,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</tr></thead>
<tbody>
<tr>
<td><span style="font-weight:600;color:var(--ios-blue);">🚁 LX-01</span></td>
<td><span style="font-weight:600;color:var(--ios-blue);"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg> LX-01</span></td>
<td>E4 Avsnitt 12</td>
<td><span style="background:rgba(52,199,89,0.12);color:var(--ios-green);font-size:11px;font-weight:700;padding:3px 8px;border-radius:20px;">PÅGÅR</span></td>
<td><span style="background:rgba(52,199,89,0.12);color:var(--ios-green);font-size:11px;font-weight:700;padding:3px 8px;border-radius: var(--radius-xl);">PÅGÅR</span></td>
<td>
<div style="width:80px;height:5px;background:var(--fill-tertiary);border-radius:3px;overflow:hidden;">
<div style="width:72%;height:100%;background:var(--ios-blue);border-radius:3px;"></div>
<div style="width:80px;height:5px;background:var(--fill-tertiary);border-radius: var(--radius-sm);overflow:hidden;">
<div style="width:72%;height:100%;background:var(--ios-blue);border-radius: var(--radius-sm);"></div>
</div>
<div style="font-size:10px;color:var(--label-tertiary);margin-top:2px;">72%</div>
</td>
@@ -841,12 +841,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<td><button style="font-size:12px;color:var(--ios-blue);background:none;border:none;cursor:pointer;padding:4px 0;">Följ live</button></td>
</tr>
<tr>
<td><span style="font-weight:600;color:var(--ios-green);">🚁 LX-02</span></td>
<td><span style="font-weight:600;color:var(--ios-green);"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg> LX-02</span></td>
<td>Gamla Stan Bron</td>
<td><span style="background:rgba(52,199,89,0.12);color:var(--ios-green);font-size:11px;font-weight:700;padding:3px 8px;border-radius:20px;">PÅGÅR</span></td>
<td><span style="background:rgba(52,199,89,0.12);color:var(--ios-green);font-size:11px;font-weight:700;padding:3px 8px;border-radius: var(--radius-xl);">PÅGÅR</span></td>
<td>
<div style="width:80px;height:5px;background:var(--fill-tertiary);border-radius:3px;overflow:hidden;">
<div style="width:35%;height:100%;background:var(--ios-green);border-radius:3px;"></div>
<div style="width:80px;height:5px;background:var(--fill-tertiary);border-radius: var(--radius-sm);overflow:hidden;">
<div style="width:35%;height:100%;background:var(--ios-green);border-radius: var(--radius-sm);"></div>
</div>
<div style="font-size:10px;color:var(--label-tertiary);margin-top:2px;">35%</div>
</td>
@@ -854,12 +854,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<td><button style="font-size:12px;color:var(--ios-blue);background:none;border:none;cursor:pointer;padding:4px 0;">Följ live</button></td>
</tr>
<tr>
<td><span style="font-weight:600;color:var(--ios-orange);">🚁 LX-03</span></td>
<td><span style="font-weight:600;color:var(--ios-orange);"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg> LX-03</span></td>
<td>Öresundsbron S3</td>
<td><span style="background:rgba(255,149,0,0.12);color:var(--ios-orange);font-size:11px;font-weight:700;padding:3px 8px;border-radius:20px;">AVVIKELSE</span></td>
<td><span style="background:rgba(255,149,0,0.12);color:var(--ios-orange);font-size:11px;font-weight:700;padding:3px 8px;border-radius: var(--radius-xl);">AVVIKELSE</span></td>
<td>
<div style="width:80px;height:5px;background:var(--fill-tertiary);border-radius:3px;overflow:hidden;">
<div style="width:58%;height:100%;background:var(--ios-orange);border-radius:3px;"></div>
<div style="width:80px;height:5px;background:var(--fill-tertiary);border-radius: var(--radius-sm);overflow:hidden;">
<div style="width:58%;height:100%;background:var(--ios-orange);border-radius: var(--radius-sm);"></div>
</div>
<div style="font-size:10px;color:var(--label-tertiary);margin-top:2px;">58%</div>
</td>
@@ -911,27 +911,27 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<tbody>
<tr>
<td style="font-weight:600;">Trafikverket</td><td>E4 Älvsjö · Kritisk</td><td>Fullständig</td>
<td><span style="background:rgba(255,59,48,0.12);color:var(--ios-red);font-size:11px;font-weight:700;padding:2px 7px;border-radius:20px;">KRITISK</span></td>
<td><span style="background:rgba(255,59,48,0.12);color:var(--ios-red);font-size:11px;font-weight:700;padding:2px 7px;border-radius: var(--radius-xl);">KRITISK</span></td>
<td style="font-size:12px;color:var(--label-tertiary);">09:12</td>
<td><button style="font-size:12px;color: #1d1d1f;background:var(--ios-red);border:none;border-radius:12px;padding:4px 12px;cursor:pointer;font-weight:600;" onclick="openDispatch()">Dispatch nu</button></td>
<td><button style="font-size:12px;color: #1d1d1f;background:var(--ios-red);border:none;border-radius: var(--radius-md);padding:4px 12px;cursor:pointer;font-weight:600;" onclick="openDispatch()">Dispatch nu</button></td>
</tr>
<tr>
<td style="font-weight:600;">Region Stockholm</td><td>Gamla Stan Bro</td><td>Ytinspektion</td>
<td><span style="background:rgba(255,149,0,0.12);color:var(--ios-orange);font-size:11px;font-weight:700;padding:2px 7px;border-radius:20px;">HÖG</span></td>
<td><span style="background:rgba(255,149,0,0.12);color:var(--ios-orange);font-size:11px;font-weight:700;padding:2px 7px;border-radius: var(--radius-xl);">HÖG</span></td>
<td style="font-size:12px;color:var(--label-tertiary);">08:45</td>
<td><button style="font-size:12px;color: #1d1d1f;background:var(--ios-orange);border:none;border-radius:12px;padding:4px 12px;cursor:pointer;font-weight:600;" onclick="openDispatch()">Dispatch</button></td>
<td><button style="font-size:12px;color: #1d1d1f;background:var(--ios-orange);border:none;border-radius: var(--radius-md);padding:4px 12px;cursor:pointer;font-weight:600;" onclick="openDispatch()">Dispatch</button></td>
</tr>
<tr>
<td style="font-weight:600;">NCC Sverige</td><td>Rv 40 Borås</td><td>Strukturkontroll</td>
<td><span style="background:rgba(0,122,255,0.10);color:var(--ios-blue);font-size:11px;font-weight:700;padding:2px 7px;border-radius:20px;">NORMAL</span></td>
<td><span style="background:rgba(0,122,255,0.10);color:var(--ios-blue);font-size:11px;font-weight:700;padding:2px 7px;border-radius: var(--radius-xl);">NORMAL</span></td>
<td style="font-size:12px;color:var(--label-tertiary);">Igår</td>
<td><button style="font-size:12px;color:var(--ios-blue);background:none;border:1px solid var(--ios-blue);border-radius:12px;padding:4px 12px;cursor:pointer;font-weight:600;">Schemalägg</button></td>
<td><button style="font-size:12px;color:var(--ios-blue);background:none;border:1px solid var(--ios-blue);border-radius: var(--radius-md);padding:4px 12px;cursor:pointer;font-weight:600;">Schemalägg</button></td>
</tr>
<tr>
<td style="font-weight:600;">Ellevio AB</td><td>Kraftledning N32</td><td>Optisk scanning</td>
<td><span style="background:rgba(0,122,255,0.10);color:var(--ios-blue);font-size:11px;font-weight:700;padding:2px 7px;border-radius:20px;">NORMAL</span></td>
<td><span style="background:rgba(0,122,255,0.10);color:var(--ios-blue);font-size:11px;font-weight:700;padding:2px 7px;border-radius: var(--radius-xl);">NORMAL</span></td>
<td style="font-size:12px;color:var(--label-tertiary);">Igår</td>
<td><button style="font-size:12px;color:var(--ios-blue);background:none;border:1px solid var(--ios-blue);border-radius:12px;padding:4px 12px;cursor:pointer;font-weight:600;">Schemalägg</button></td>
<td><button style="font-size:12px;color:var(--ios-blue);background:none;border:1px solid var(--ios-blue);border-radius: var(--radius-md);padding:4px 12px;cursor:pointer;font-weight:600;">Schemalägg</button></td>
</tr>
</tbody>
</table>
@@ -963,7 +963,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<tbody>
<tr onclick="alert('Öppnar objektdetaljer: E4 Norrtull')">
<td><div class="td-name">E4 Norrtull Avsnitt 12</div><div class="td-sub">REF-001 · Stockholm</div></td>
<td>🛣️ Väg</td>
<td> Väg</td>
<td><span class="status-badge status-warn"><span class="status-dot"></span>Kräver åtgärd</span></td>
<td>
<div class="risk-bar-wrap risk-medium">
@@ -976,7 +976,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</tr>
<tr onclick="alert('Öppnar objektdetaljer: Gamla Stan')">
<td><div class="td-name">Gamla Stan Bro</div><div class="td-sub">REF-002 · Stockholm</div></td>
<td>🌉 Bro</td>
<td> Bro</td>
<td><span class="status-badge status-ok"><span class="status-dot"></span>OK</span></td>
<td>
<div class="risk-bar-wrap risk-low">
@@ -989,7 +989,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</tr>
<tr onclick="alert('Öppnar objektdetaljer: Kungliga Slottet')">
<td><div class="td-name">Kungliga Slottet Tak</div><div class="td-sub">REF-003 · Stockholm</div></td>
<td>🏛️ Byggnad</td>
<td> Byggnad</td>
<td><span class="status-badge status-critical"><span class="status-dot"></span>Kritisk</span></td>
<td>
<div class="risk-bar-wrap risk-high">
@@ -998,11 +998,11 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</td>
<td>2026-05-01</td>
<td>🔴 Omedelbart</td>
<td> Omedelbart</td>
</tr>
<tr onclick="alert('Öppnar objektdetaljer: Arlanda Terminal 5')">
<td><div class="td-name">Arlanda Terminal 5</div><div class="td-sub">REF-004 · Sigtuna</div></td>
<td>✈️ Byggnad</td>
<td> Byggnad</td>
<td><span class="status-badge status-ok"><span class="status-dot"></span>OK</span></td>
<td>
<div class="risk-bar-wrap risk-low">
@@ -1015,7 +1015,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</tr>
<tr onclick="alert('Öppnar objektdetaljer: Öresundsbron')">
<td><div class="td-name">Öresundsbron Sektion 3</div><div class="td-sub">REF-005 · Malmö</div></td>
<td>🌉 Bro</td>
<td> Bro</td>
<td><span class="status-badge status-warn"><span class="status-dot"></span>Kräver åtgärd</span></td>
<td>
<div class="risk-bar-wrap risk-medium">
@@ -1035,7 +1035,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Senaste inspektioner</div>
<div class="inspection-list">
<div class="inspection-item">
<div class="inspection-icon" style="background:rgba(52,199,89,0.15)">🏗️</div>
<div class="inspection-icon" style="background:rgba(52,199,89,0.15)"><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>
<div class="inspection-body">
<div class="inspection-title">E4 Norrtull — Ytinspektion</div>
<div class="inspection-meta">Anna Lindqvist · 2026-05-10 · <span style="color:var(--ios-green)">Godkänd</span></div>
@@ -1043,7 +1043,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="inspection-chevron"></div>
</div>
<div class="inspection-item">
<div class="inspection-icon" style="background:rgba(255,149,0,0.15)">🌉</div>
<div class="inspection-icon" style="background:rgba(255,149,0,0.15)"></div>
<div class="inspection-body">
<div class="inspection-title">Öresundsbron — Strukturkontroll</div>
<div class="inspection-meta">Marcus Holm · 2026-04-28 · <span style="color:var(--ios-orange)">Åtgärder krävs</span></div>
@@ -1051,7 +1051,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="inspection-chevron"></div>
</div>
<div class="inspection-item">
<div class="inspection-icon" style="background:rgba(255,59,48,0.15)">🏛️</div>
<div class="inspection-icon" style="background:rgba(255,59,48,0.15)"></div>
<div class="inspection-body">
<div class="inspection-title">Kungliga Slottet — Takinspektion</div>
<div class="inspection-meta">Sofia Berg · 2026-05-01 · <span style="color:var(--ios-red)">Kritisk rapport</span></div>
@@ -1059,7 +1059,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="inspection-chevron"></div>
</div>
<div class="inspection-item">
<div class="inspection-icon" style="background:rgba(0,122,255,0.15)">✈️</div>
<div class="inspection-icon" style="background:rgba(0,122,255,0.15)"></div>
<div class="inspection-body">
<div class="inspection-title">Arlanda T5 — Fasadinspektion</div>
<div class="inspection-meta">Erik Svensson · 2026-05-16 · <span style="color:var(--ios-green)">Godkänd</span></div>
@@ -1160,15 +1160,15 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- TAB BAR -->
<nav class="tab-bar">
<div class="tab-item active">
<div class="tab-icon">🏛️</div>
<div class="tab-icon"></div>
<div class="tab-label">Översikt</div>
</div>
<div class="tab-item" onclick="window.location.href='landvex-admin.html#objects'">
<div class="tab-icon">📋</div>
<div class="tab-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></div>
<div class="tab-label">Objekt</div>
</div>
<div class="tab-item" onclick="window.location.href='landvex-admin.html#inspections'">
<div class="tab-icon">🔍</div>
<div class="tab-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="#666" stroke-width="2"/><path d="M11 11L14 14" stroke="#666" stroke-width="2" stroke-linecap="round"/></svg></div>
<div class="tab-label">Inspektioner</div>
</div>
<div class="tab-item">
@@ -1176,7 +1176,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="tab-label">Rapporter</div>
</div>
<div class="tab-item">
<div class="tab-icon">⚙️</div>
<div class="tab-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" stroke="#666" stroke-width="1.5"/><path d="M8 2V4M8 12V14M2 8H4M12 8H14M4.3 4.3L5.7 5.7M10.3 10.3L11.7 11.7M4.3 11.7L5.7 10.3M10.3 5.7L11.7 4.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></div>
<div class="tab-label">Inställningar</div>
</div>
</nav>
@@ -1197,32 +1197,32 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- DISPATCH MODAL -->
<div id="dispatch-modal" style="position:fixed;inset:0;background: rgba(242,242,247,0.5);display:flex;align-items:flex-end;z-index:1001;opacity:0;pointer-events:none;transition:opacity 0.25s;">
<div style="background:var(--background-primary);border-radius:20px 20px 0 0;padding:20px 20px 40px;width:100%;max-height:80vh;overflow-y:auto;transform:translateY(100%);transition:transform 0.35s cubic-bezier(0.34,1.56,0.64,1);" id="dispatch-sheet">
<div style="width:36px;height:4px;background:var(--fill-secondary);border-radius:2px;margin:0 auto 16px;"></div>
<div style="font-family:var(--font-display);font-size:22px;font-weight:700;letter-spacing:-0.4px;margin-bottom:16px;"> Snabb-dispatch</div>
<div style="background:var(--background-primary);border-radius: var(--radius-xl) 20px 0 0;padding:20px 20px 40px;width:100%;max-height:80vh;overflow-y:auto;transform:translateY(100%);transition:transform 0.35s cubic-bezier(0.34,1.56,0.64,1);" id="dispatch-sheet">
<div style="width:36px;height:4px;background:var(--fill-secondary);border-radius: var(--radius-sm);margin:0 auto 16px;"></div>
<div style="font-family:var(--font-display);font-size:22px;font-weight:700;letter-spacing:-0.4px;margin-bottom:16px;"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> Snabb-dispatch</div>
<div style="background:var(--background-secondary);border-radius:12px;padding:14px;margin-bottom:12px;">
<div style="background:var(--background-secondary);border-radius: var(--radius-md);padding:14px;margin-bottom:12px;">
<div style="font-size:13px;font-weight:600;color:var(--label-secondary);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:10px;">Närmaste lediga zoomer</div>
<div style="display:flex;align-items:center;gap:12px;padding:10px;background:rgba(52,199,89,0.08);border-radius:10px;border:0.5px solid rgba(52,199,89,0.3);margin-bottom:8px;">
<span style="font-size:24px;">🚁</span>
<div style="display:flex;align-items:center;gap:12px;padding:10px;background:rgba(52,199,89,0.08);border-radius: var(--radius-md);border:0.5px solid rgba(52,199,89,0.3);margin-bottom:8px;">
<span style="font-size:24px;"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg></span>
<div style="flex:1;">
<div style="font-size:16px;font-weight:600;">LX-04 · Stockholm N</div>
<div style="font-size:12px;color:var(--label-secondary);">4.2 km bort · Laddad 100% · HD + Multispektral</div>
</div>
<button onclick="confirmDispatch('LX-04')" style="background:var(--ios-green);color: #1d1d1f;border:none;border-radius:12px;padding:8px 14px;font-size:14px;font-weight:600;cursor:pointer;">Skicka</button>
<button onclick="confirmDispatch('LX-04')" style="background:var(--ios-green);color: #1d1d1f;border:none;border-radius: var(--radius-md);padding:8px 14px;font-size:14px;font-weight:600;cursor:pointer;">Skicka</button>
</div>
<div style="display:flex;align-items:center;gap:12px;padding:10px;background:var(--fill-quaternary);border-radius:10px;margin-bottom:8px;">
<span style="font-size:24px;">🚁</span>
<div style="display:flex;align-items:center;gap:12px;padding:10px;background:var(--fill-quaternary);border-radius: var(--radius-md);margin-bottom:8px;">
<span style="font-size:24px;"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg></span>
<div style="flex:1;">
<div style="font-size:16px;font-weight:600;">LX-07 · Nacka</div>
<div style="font-size:12px;color:var(--label-secondary);">8.7 km bort · Laddad 87% · HD</div>
</div>
<button onclick="confirmDispatch('LX-07')" style="background:var(--ios-blue);color: #1d1d1f;border:none;border-radius:12px;padding:8px 14px;font-size:14px;font-weight:600;cursor:pointer;">Skicka</button>
<button onclick="confirmDispatch('LX-07')" style="background:var(--ios-blue);color: #1d1d1f;border:none;border-radius: var(--radius-md);padding:8px 14px;font-size:14px;font-weight:600;cursor:pointer;">Skicka</button>
</div>
</div>
<div id="dispatch-success" style="display:none;text-align:center;padding:16px;background:rgba(52,199,89,0.1);border-radius:14px;color:var(--ios-green);font-size:16px;font-weight:600;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#00C853" stroke-width="3"><path d="M20 6L9 17l-5-5"/></svg> Dispatch bekräftad! Zoomer anländer inom 12 min.</div>
<button onclick="closeDispatch()" style="width:100%;padding:14px;background:var(--fill-tertiary);color:var(--label-primary);border:none;border-radius:14px;font-size:17px;font-weight:600;cursor:pointer;margin-top:8px;">Stäng</button>
<div id="dispatch-success" style="display:none;text-align:center;padding:16px;background:rgba(52,199,89,0.1);border-radius: var(--radius-lg);color:var(--ios-green);font-size:16px;font-weight:600;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#00C853" stroke-width="3"><path d="M20 6L9 17l-5-5"/></svg> Dispatch bekräftad! Zoomer anländer inom 12 min.</div>
<button onclick="closeDispatch()" style="width:100%;padding:14px;background:var(--fill-tertiary);color:var(--label-primary);border:none;border-radius: var(--radius-lg);font-size:17px;font-weight:600;cursor:pointer;margin-top:8px;">Stäng</button>
</div>
</div>
@@ -1241,8 +1241,8 @@ function submitInspection() {
document.getElementById('modal').classList.remove('open');
// Toast feedback
const toast = document.createElement('div');
toast.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:var(--ios-green);color:white;padding:12px 20px;border-radius:20px;font-size:15px;font-weight:600;z-index:500;box-shadow:0 4px 16px rgba(52,199,89,0.4);';
toast.textContent = ' Inspektion skapad';
toast.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:var(--ios-green);color:white;padding:12px 20px;border-radius: var(--radius-xl);font-size:15px;font-weight:600;z-index:500;box-shadow:0 4px 16px rgba(52,199,89,0.4);';
toast.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> Inspektion skapad';
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2500);
}