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
+24 -24
View File
@@ -30,12 +30,12 @@
.nav-subtitle { font-size: 12px; color: var(--label-secondary); margin-top: 2px; }
.status-pill {
display: flex; align-items: center; gap: 6px;
padding: 4px 12px; border-radius: 20px;
padding: 4px 12px; border-radius: var(--radius-xl);
font-size: 12px; font-weight: 600;
}
.status-pill.online { background: rgba(52,199,89,0.15); color: var(--ios-green); border: 1px solid rgba(52,199,89,0.3); }
.status-pill.offline { background: rgba(255,59,48,0.15); color: var(--ios-red); border: 1px solid rgba(255,59,48,0.3); }
.pulse { width: 7px; height: 7px; border-radius: 50%; background: currentColor; animation: blink 1.5s ease-in-out infinite; }
.pulse { width: 7px; height: 7px; border-radius: var(--radius-full); background: currentColor; animation: blink 1.5s ease-in-out infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }
.content { padding: 20px 16px; max-width: 500px; margin: 0 auto; }
@@ -49,7 +49,7 @@
.card {
background: var(--grouped-background-secondary);
border-radius: 12px; overflow: hidden;
border-radius: var(--radius-md); overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.card-row {
@@ -59,7 +59,7 @@
}
.card-row:last-child { border-bottom: none; }
.row-icon {
width: 36px; height: 36px; border-radius: 8px;
width: 36px; height: 36px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center; font-size: 18px;
flex-shrink: 0;
}
@@ -71,7 +71,7 @@
/* Atlas Viewer placeholder */
.atlas-viewer {
background: linear-gradient(135deg, #0a0a1a 0%, #0d1a2e 50%, #0a0a1a 100%);
border-radius: 12px; overflow: hidden;
border-radius: var(--radius-md); overflow: hidden;
aspect-ratio: 16/9; position: relative;
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
}
@@ -90,7 +90,7 @@
.scan-points { position: absolute; inset: 0; }
.scan-point {
position: absolute; width: 8px; height: 8px;
border-radius: 50%; background: var(--ios-blue);
border-radius: var(--radius-full); background: var(--ios-blue);
box-shadow: 0 0 12px var(--ios-blue);
animation: scan-pulse 2s ease-in-out infinite;
}
@@ -112,7 +112,7 @@
}
.atlas-btn {
background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.2);
color: white; border-radius: 8px; padding: 6px 12px;
color: white; border-radius: var(--radius-md); padding: 6px 12px;
font-size: 13px; font-weight: 500; cursor: pointer;
backdrop-filter: blur(10px);
transition: background 0.2s;
@@ -125,7 +125,7 @@
}
.hud-chip {
background: rgba(242,242,247,0.5); border: 1px solid rgba(255,255,255,0.1);
color: rgba(255,255,255,0.8); border-radius: 6px;
color: rgba(255,255,255,0.8); border-radius: var(--radius-sm);
padding: 3px 8px; font-size: 11px; font-family: var(--font-mono);
backdrop-filter: blur(8px);
}
@@ -133,7 +133,7 @@
/* Coverage meter */
.coverage-card {
background: var(--grouped-background-secondary);
border-radius: 12px; padding: 16px;
border-radius: var(--radius-md); padding: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.coverage-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
@@ -141,10 +141,10 @@
.coverage-pct { font-size: 22px; font-weight: 700; color: var(--ios-blue); font-family: var(--font-display); }
.progress-track {
height: 8px; background: var(--fill-tertiary);
border-radius: 4px; overflow: hidden;
border-radius: var(--radius-sm); overflow: hidden;
}
.progress-fill {
height: 100%; border-radius: 4px;
height: 100%; border-radius: var(--radius-sm);
background: linear-gradient(90deg, var(--ios-blue) 0%, var(--ios-indigo) 100%);
transition: width 1s ease;
}
@@ -154,7 +154,7 @@
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.stat-card {
background: var(--grouped-background-secondary);
border-radius: 12px; padding: 14px 12px; text-align: center;
border-radius: var(--radius-md); padding: 14px 12px; text-align: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.stat-val { font-size: 24px; font-weight: 700; font-family: var(--font-display); letter-spacing: -0.5px; }
@@ -167,11 +167,11 @@
border-bottom: 0.5px solid var(--separator);
}
.region-row:last-child { border-bottom: none; }
.region-color { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.region-color { width: 10px; height: 10px; border-radius: var(--radius-full); flex-shrink: 0; }
.region-name { flex: 1; font-size: 15px; font-weight: 500; }
.region-count { font-size: 13px; color: var(--label-secondary); }
.region-bar-wrap { width: 80px; height: 6px; background: var(--fill-tertiary); border-radius: 3px; overflow: hidden; }
.region-bar { height: 100%; border-radius: 3px; }
.region-bar-wrap { width: 80px; height: 6px; background: var(--fill-tertiary); border-radius: var(--radius-sm); overflow: hidden; }
.region-bar { height: 100%; border-radius: var(--radius-sm); }
.footer { text-align: center; padding: 32px 0 16px; }
.footer-text { font-size: 12px; color: var(--label-tertiary); }
@@ -242,12 +242,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="hud-chip" id="hud-scans">Scans: —</div>
</div>
<div style="position:relative;z-index:2;text-align:center;">
<div class="atlas-label-main">🌐 3D Atlas</div>
<div class="atlas-label-main"> 3D Atlas</div>
<div class="atlas-label" id="atlas-status">Connecting to atlas service...</div>
</div>
<div class="atlas-controls">
<button class="atlas-btn" onclick="zoomIn()"></button>
<button class="atlas-btn" onclick="zoomOut()"></button>
<button class="atlas-btn" onclick="zoomIn()"></button>
<button class="atlas-btn" onclick="zoomOut()"></button>
<button class="atlas-btn" onclick="resetView()"></button>
</div>
</div>
@@ -287,7 +287,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Service Details</div>
<div class="card">
<div class="card-row">
<div class="row-icon" style="background:rgba(0,122,255,0.12);">🔌</div>
<div class="row-icon" style="background:rgba(0,122,255,0.12);"></div>
<div class="row-body">
<div class="row-label">Endpoint</div>
<div class="row-sub">Internal · port 7002</div>
@@ -295,7 +295,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="row-value">localhost:7002</div>
</div>
<div class="card-row">
<div class="row-icon" style="background:rgba(88,86,214,0.12);">📦</div>
<div class="row-icon" style="background:rgba(88,86,214,0.12);"></div>
<div class="row-body">
<div class="row-label">3D Engine</div>
<div class="row-sub">Point cloud · LIDAR</div>
@@ -303,7 +303,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="row-value" id="engine-ver"></div>
</div>
<div class="card-row">
<div class="row-icon" style="background:rgba(52,199,89,0.12);">🗺</div>
<div class="row-icon" style="background:rgba(52,199,89,0.12);"></div>
<div class="row-body">
<div class="row-label">Map Format</div>
<div class="row-sub">Compressed tile format</div>
@@ -311,7 +311,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="row-value">3DGS v2</div>
</div>
<div class="card-row">
<div class="row-icon" style="background:rgba(255,149,0,0.12);"></div>
<div class="row-icon" style="background:rgba(255,149,0,0.12);"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg></div>
<div class="row-body">
<div class="row-label">Render API</div>
<div class="row-sub">WebGL 2 · GPU-accelerated</div>
@@ -401,12 +401,12 @@ async function refresh() {
if (health.online) {
pill.className = 'status-pill online';
statusText.textContent = 'Online';
document.getElementById('atlas-status').textContent = ' Atlas service connected · 3D render ready';
document.getElementById('atlas-status').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> Atlas service connected · 3D render ready';
document.getElementById('engine-ver').textContent = health.data?.version || 'v1.0';
} else {
pill.className = 'status-pill offline';
statusText.textContent = 'Offline';
document.getElementById('atlas-status').textContent = ' Atlas service unreachable';
document.getElementById('atlas-status').textContent = ' Atlas service unreachable';
}
const stats = await fetchAtlasStats();
+79
View File
@@ -0,0 +1,79 @@
# quiXzoom Asia — Multilingual Landing Pages
## Overview
Regional hub for quiXzoom in Asia with support for 9 languages.
## Languages
| Language | Code | URL | Currency |
|----------|------|-----|----------|
| Simplified Chinese | zh-CN | quixzoom.asia/zh-cn/ | CNY |
| Traditional Chinese | zh-TW | quixzoom.asia/zh-tw/ | TWD |
| Japanese | ja | quixzoom.asia/ja/ | JPY |
| Korean | ko | quixzoom.asia/ko/ | KRW |
| Thai | th | quixzoom.asia/th/ | THB |
| Vietnamese | vi | quixzoom.asia/vi/ | VND |
| Indonesian | id | quixzoom.asia/id/ | IDR |
| Malay | ms | quixzoom.asia/ms/ | MYR |
| Hindi | hi | quixzoom.asia/hi/ | INR |
## Structure
```
quixzoom-asia-pages/
├── index.html # Language selector (root)
├── zh-cn/
│ └── index.html # Simplified Chinese
├── zh-tw/
│ └── index.html # Traditional Chinese
├── ja/
│ └── index.html # Japanese
├── ko/
│ └── index.html # Korean
├── th/
│ └── index.html # Thai
├── vi/
│ └── index.html # Vietnamese
├── id/
│ └── index.html # Indonesian
├── ms/
│ └── index.html # Malay
├── hi/
│ └── index.html # Hindi
├── favicon.svg # quiXzoom favicon
└── apple-touch-icon.svg # iOS icon
```
## SEO
- Proper hreflang tags for all languages
- Canonical URLs per language
- Localized meta descriptions
- Schema.org structured data ready
## Deployment
```bash
# Update CloudFront distribution ID in deploy.sh
# Run deployment
./deploy.sh
```
## Notes
- All pages use quiXzoom brand colors (dark theme, blue accent)
- Responsive design for mobile-first
- SVG icons only (no emojis)
- Localized currency displays
- Links to main registration at quixzoom.com
## Next Steps
1. [ ] Update CloudFront distribution ID in deploy.sh
2. [ ] Configure S3 bucket for static hosting
3. [ ] Set up DNS for quixzoom.asia
4. [ ] Add Google Analytics / tracking
5. [ ] Localize images and hero content
6. [ ] Add local payment methods (Alipay, WeChat Pay, etc.)
7. [ ] Create localized FAQ pages
+2 -2
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About quiXzoom — Global Field Intelligence Network</title>
<meta name="description" content="quiXzoom is a global field intelligence network. Learn about our mission, team, and how we're building the world's largest observation network.">
<link rel="canonical" href="https://www.quixzoom.com/about.html">
<link rel="canonical" href="https://www.quixzoom.com/about">
<style>
:root {
--bg-dark: #f5f5f7;
@@ -57,7 +57,7 @@
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
border-radius: var(--radius-lg);
padding: 32px;
}
.card h3 { font-size: 1.25rem; font-weight: 700; margin-bottom: 12px; }
+8 -8
View File
@@ -6,23 +6,23 @@
body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#F2F2F7;color:#000}
.header{position:sticky;top:0;background:rgba(242,242,247,.9);backdrop-filter:blur(20px);border-bottom:1px solid rgba(60,60,67,.12);padding:12px 16px;display:flex;align-items:center;gap:12px}
.header h1{font-size:17px;font-weight:600}
.search{flex:1;background:rgba(120,120,128,.12);border:none;border-radius:10px;padding:7px 12px;font-size:15px;outline:none}
.search{flex:1;background:rgba(120,120,128,.12);border:none;border-radius: var(--radius-md);padding:7px 12px;font-size:15px;outline:none}
.stats{display:flex;gap:8px;padding:16px;overflow-x:auto}
.stat{background:#fff;border-radius:12px;padding:12px 16px;min-width:100px;text-align:center}
.stat{background:#fff;border-radius: var(--radius-md);padding:12px 16px;min-width:100px;text-align:center}
.stat-val{font-size:26px;font-weight:700}.stat-lbl{font-size:12px;color:rgba(60,60,67,.6);margin-top:2px}
.filters{padding:0 16px 8px;display:flex;gap:8px;overflow-x:auto}
.chip{background:rgba(120,120,128,.12);border:none;border-radius:20px;padding:6px 14px;font-size:14px;font-weight:500;cursor:pointer}
.chip{background:rgba(120,120,128,.12);border:none;border-radius: var(--radius-xl);padding:6px 14px;font-size:14px;font-weight:500;cursor:pointer}
.chip.active{background:#007AFF;color:#fff}
.list{padding:0 16px 80px}
.card{background:#fff;border-radius:12px;padding:14px;margin-bottom:8px;display:flex;align-items:center;gap:12px}
.avatar{width:42px;height:42px;border-radius:21px;background:#007AFF15;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:600;color:#007AFF;flex-shrink:0}
.card{background:#fff;border-radius: var(--radius-md);padding:14px;margin-bottom:8px;display:flex;align-items:center;gap:12px}
.avatar{width:42px;height:42px;border-radius: 21px;background:#007AFF15;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:600;color:#007AFF;flex-shrink:0}
.info{flex:1}
.name{font-size:15px;font-weight:600}.sub{font-size:13px;color:rgba(60,60,67,.6);margin-top:2px}
.badge{font-size:12px;padding:3px 8px;border-radius:6px;font-weight:500}
.badge{font-size:12px;padding:3px 8px;border-radius: var(--radius-sm);font-weight:500}
.badge.pending{background:#FF950015;color:#FF9500}
.badge.verified{background:#34C75915;color:#34C759}
.badge.active{background:#007AFF15;color:#007AFF}
.btn-verify{background:#34C759;color:#fff;border:none;border-radius:8px;padding:7px 14px;font-size:13px;font-weight:600;cursor:pointer}
.btn-verify{background:#34C759;color:#fff;border:none;border-radius: var(--radius-md);padding:7px 14px;font-size:13px;font-weight:600;cursor:pointer}
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
@@ -52,7 +52,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</style>
</head><body>
<div class="header">
<h1>🎯 Zoomer Admin</h1>
<h1> Zoomer Admin</h1>
<input class="search" type="text" placeholder="Sök namn eller email…" id="q" oninput="filter()">
</div>
<div class="stats" id="stats"></div>
+45 -45
View File
@@ -32,7 +32,7 @@ body{background:var(--bg);color:var(--text);font-family:-apple-system,'SF Pro Te
.sidebar-back a{color:var(--blue);font-size:13px;text-decoration:none;display:flex;align-items:center;gap:4px}
.sidebar-section{padding:14px 10px 6px}
.sidebar-label{font-size:10px;font-weight:700;letter-spacing:.1em;color:var(--text3);text-transform:uppercase;padding:0 8px;margin-bottom:6px}
.nav-item{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:10px;cursor:pointer;color:var(--text2);font-size:13px;font-weight:500;transition:all .15s;border:none;background:none;width:100%;text-align:left}
.nav-item{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius: var(--radius-md);cursor:pointer;color:var(--text2);font-size:13px;font-weight:500;transition:all .15s;border:none;background:none;width:100%;text-align:left}
.nav-item:hover{background:rgba(255,255,255,0.07);color:var(--text)}
.nav-item.active{background:var(--accent);color:#000}
.nav-icon{font-size:15px;width:20px;text-align:center}
@@ -43,11 +43,11 @@ body{background:var(--bg);color:var(--text);font-family:-apple-system,'SF Pro Te
.topbar{background: rgba(242,242,247,0.85);backdrop-filter:saturate(180%) blur(20px);border-bottom:0.5px solid var(--border);padding:0 24px;height:52px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100}
.topbar-title{font-size:15px;font-weight:700}
.topbar-actions{display:flex;gap:8px;align-items:center}
.btn-small{height:30px;padding:0 12px;border-radius:8px;border:none;font-size:12px;font-weight:600;cursor:pointer;background:rgba(255,255,255,.1);color:var(--text);transition:all .15s}
.btn-small{height:30px;padding:0 12px;border-radius: var(--radius-md);border:none;font-size:12px;font-weight:600;cursor:pointer;background:rgba(255,255,255,.1);color:var(--text);transition:all .15s}
.btn-small:hover{background:rgba(255,255,255,.18)}
.btn-primary{background:var(--accent);color:#000}
.btn-primary:hover{background:var(--accent2)}
.pulse{width:8px;height:8px;border-radius:50%;background:var(--green);animation:pulse 2s ease-in-out infinite}
.pulse{width:8px;height:8px;border-radius: var(--radius-full);background:var(--green);animation:pulse 2s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.view{display:none;padding:24px}
@@ -57,16 +57,16 @@ body{background:var(--bg);color:var(--text);font-family:-apple-system,'SF Pro Te
.page-sub{font-size:14px;color:var(--text3);margin-top:4px}
/* CARDS */
.card{background:var(--card);border:0.5px solid var(--border);border-radius:16px;padding:18px}
.card{background:var(--card);border:0.5px solid var(--border);border-radius: var(--radius-lg);padding:18px}
.card-title{font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text3);margin-bottom:14px}
.metric-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px;margin-bottom:24px}
.metric-card{background:var(--bg2);border:0.5px solid var(--border);border-radius:14px;padding:16px}
.metric-card{background:var(--bg2);border:0.5px solid var(--border);border-radius: var(--radius-lg);padding:16px}
.metric-label{font-size:11px;color:var(--text3);margin-bottom:6px;font-weight:600;text-transform:uppercase;letter-spacing:.06em}
.metric-value{font-size:22px;font-weight:800;letter-spacing:-.02em}
.metric-change{font-size:11px;margin-top:4px;font-weight:600}
.up{color:var(--green)} .down{color:var(--red)} .neutral{color:var(--text3)}
.badge{display:inline-flex;align-items:center;font-size:10px;font-weight:700;padding:2px 8px;border-radius:20px;text-transform:uppercase;letter-spacing:.06em}
.badge{display:inline-flex;align-items:center;font-size:10px;font-weight:700;padding:2px 8px;border-radius: var(--radius-xl);text-transform:uppercase;letter-spacing:.06em}
.badge-green{background:rgba(48,209,88,.15);color:var(--green)}
.badge-orange{background:rgba(255,159,10,.15);color:var(--orange)}
.badge-red{background:rgba(255,69,58,.15);color:var(--red)}
@@ -84,8 +84,8 @@ body{background:var(--bg);color:var(--text);font-family:-apple-system,'SF Pro Te
.bar-chart{display:flex;flex-direction:column;gap:6px}
.bar-row{display:flex;align-items:center;gap:10px;font-size:12px}
.bar-label{width:120px;color:var(--text2);font-weight:500;text-align:right;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bar-track{flex:1;background:var(--bg3);border-radius:4px;height:10px;overflow:hidden}
.bar-fill{height:100%;border-radius:4px;transition:width 1s ease}
.bar-track{flex:1;background:var(--bg3);border-radius: var(--radius-sm);height:10px;overflow:hidden}
.bar-fill{height:100%;border-radius: var(--radius-sm);transition:width 1s ease}
.bar-val{width:70px;color:var(--text2);font-size:11px;font-weight:600;text-align:right;flex-shrink:0}
/* LINE CHART */
@@ -94,22 +94,22 @@ body{background:var(--bg);color:var(--text);font-family:-apple-system,'SF Pro Te
/* HISTOGRAM */
.histogram{display:flex;align-items:flex-end;gap:2px;height:100px;padding:0 4px}
.hist-bar{flex:1;border-radius:3px 3px 0 0;transition:height .8s ease;min-width:4px;cursor:pointer;position:relative}
.hist-bar:hover::after{content:attr(data-label);position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:var(--bg3);border:0.5px solid var(--border);padding:3px 6px;border-radius:6px;font-size:10px;white-space:nowrap;color:var(--text);pointer-events:none}
.hist-bar{flex:1;border-radius: var(--radius-sm) 3px 0 0;transition:height .8s ease;min-width:4px;cursor:pointer;position:relative}
.hist-bar:hover::after{content:attr(data-label);position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:var(--bg3);border:0.5px solid var(--border);padding:3px 6px;border-radius: var(--radius-sm);font-size:10px;white-space:nowrap;color:var(--text);pointer-events:none}
/* FUNNEL */
.funnel-step{margin-bottom:8px}
.funnel-label-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;font-size:12px}
.funnel-bar-track{background:var(--bg3);border-radius:4px;height:8px;overflow:hidden}
.funnel-bar-fill{height:100%;border-radius:4px;transition:width 1.2s ease}
.funnel-bar-track{background:var(--bg3);border-radius: var(--radius-sm);height:8px;overflow:hidden}
.funnel-bar-fill{height:100%;border-radius: var(--radius-sm);transition:width 1.2s ease}
/* GEO MAP SVG */
.geo-list{display:flex;flex-direction:column;gap:6px}
.geo-row{display:flex;align-items:center;gap:10px;padding:8px;background:var(--bg2);border-radius:8px}
.geo-row{display:flex;align-items:center;gap:10px;padding:8px;background:var(--bg2);border-radius: var(--radius-md)}
.geo-rank{font-size:13px;font-weight:800;width:22px;text-align:center;color:var(--accent)}
.geo-name{flex:1;font-size:13px;font-weight:600}
.geo-bar{width:80px;background:var(--bg3);border-radius:4px;height:6px;overflow:hidden}
.geo-bar-fill{height:100%;border-radius:4px;background:var(--orange)}
.geo-bar{width:80px;background:var(--bg3);border-radius: var(--radius-sm);height:6px;overflow:hidden}
.geo-bar-fill{height:100%;border-radius: var(--radius-sm);background:var(--orange)}
.geo-missions{font-size:12px;color:var(--text3);width:50px;text-align:right}
.geo-rate{font-size:12px;font-weight:700;width:40px;text-align:right}
@@ -121,7 +121,7 @@ body{background:var(--bg);color:var(--text);font-family:-apple-system,'SF Pro Te
.data-table tr:hover td{background:rgba(255,255,255,.03)}
/* STATUS DOT */
.dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:4px}
.dot{width:8px;height:8px;border-radius: var(--radius-full);display:inline-block;margin-right:4px}
.dot-ok{background:var(--green)} .dot-warn{background:var(--orange)} .dot-err{background:var(--red)}
.loading{color:var(--text3);font-size:13px;padding:40px;text-align:center}
@@ -169,19 +169,19 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="sidebar-section">
<div class="sidebar-label">Dashboards</div>
<button class="nav-item active" onclick="showView('overview',this)"><span class="nav-icon">📊</span> Overview</button>
<button class="nav-item" onclick="showView('success',this)"><span class="nav-icon"></span> Mission Success</button>
<button class="nav-item" onclick="showView('earnings',this)"><span class="nav-icon">💰</span> Zoomer Earnings</button>
<button class="nav-item" onclick="showView('funnel',this)"><span class="nav-icon">🔽</span> KYC Funnel</button>
<button class="nav-item" onclick="showView('geo',this)"><span class="nav-icon">🗺</span> Geographic</button>
<button class="nav-item" onclick="showView('revenue',this)"><span class="nav-icon">📈</span> Revenue</button>
<button class="nav-item" onclick="showView('health',this)"><span class="nav-icon">🛡</span> Platform Health</button>
<button class="nav-item active" onclick="showView('overview',this)"><span class="nav-icon"></span> Overview</button>
<button class="nav-item" onclick="showView('success',this)"><span class="nav-icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span> Mission Success</button>
<button class="nav-item" onclick="showView('earnings',this)"><span class="nav-icon"></span> Zoomer Earnings</button>
<button class="nav-item" onclick="showView('funnel',this)"><span class="nav-icon"></span> KYC Funnel</button>
<button class="nav-item" onclick="showView('geo',this)"><span class="nav-icon"></span> Geographic</button>
<button class="nav-item" onclick="showView('revenue',this)"><span class="nav-icon"><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></span> Revenue</button>
<button class="nav-item" onclick="showView('health',this)"><span class="nav-icon"></span> Platform Health</button>
</div>
<div class="sidebar-section">
<div class="sidebar-label">Relaterat</div>
<button class="nav-item" onclick="window.location='/ouroboros/digital-twin.html'"><span class="nav-icon">🔮</span> Digital Twin</button>
<button class="nav-item" onclick="window.location='/quixzoom/dac7-reporting.html'"><span class="nav-icon">📋</span> DAC7 Report</button>
<button class="nav-item" onclick="window.location='/ouroboros/analytics-advanced.html'"><span class="nav-icon">🔭</span> Predictive AI</button>
<button class="nav-item" onclick="window.location='/ouroboros/digital-twin.html'"><span class="nav-icon"></span> Digital Twin</button>
<button class="nav-item" onclick="window.location='/quixzoom/dac7-reporting.html'"><span class="nav-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></span> DAC7 Report</button>
<button class="nav-item" onclick="window.location='/ouroboros/analytics-advanced.html'"><span class="nav-icon"></span> Predictive AI</button>
</div>
<div class="sidebar-footer">
Uppdateras var 30s<br>
@@ -192,7 +192,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- MAIN -->
<div class="main">
<div class="topbar">
<div class="topbar-title"> quiXzoom Analytics</div>
<div class="topbar-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> quiXzoom Analytics</div>
<div class="topbar-actions">
<div class="pulse" style="margin-right:4px"></div>
<span style="font-size:12px;color:var(--text3)">Live</span>
@@ -378,7 +378,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div><!-- /layout -->
<script>
/* ── Utilities ──────────────────────────────────────────────── */
/* Utilities */
function fmt(n){return n>=1e6?(n/1e6).toFixed(1)+'M':n>=1e3?(n/1e3).toFixed(1)+'K':String(n)}
function fmtSEK(n){return new Intl.NumberFormat('sv-SE').format(Math.round(n))}
function pct(v,d=1){return (v*100).toFixed(d)+'%'}
@@ -399,7 +399,7 @@ async function apiFetch(path) {
return null;
}
/* ── Inline seed generators (fallback when API unavailable) ── */
/* Inline seed generators (fallback when API unavailable) */
function seedOverview() {
return {
missions_total:18742+rnd(-50,50), missions_this_month:1284+rnd(-20,20),
@@ -477,7 +477,7 @@ async function fetchOrSeed(path, seedFn) {
return d || seedFn();
}
/* ── OVERVIEW ──────────────────────────────────────────────── */
/* OVERVIEW */
async function loadOverview() {
const d = await fetchOrSeed('https://api.quixzoom.com/api/qz/analytics/overview', seedOverview);
if(!d) return;
@@ -517,13 +517,13 @@ async function loadServicesMini() {
const d = await fetchOrSeed('https://api.quixzoom.com/api/qz/analytics/health', seedHealth);
if(!d) return;
document.getElementById('services-mini').innerHTML = `<div style="display:flex;flex-wrap:wrap;gap:8px">
${d.services.map(s=>`<span style="background:var(--bg2);border:0.5px solid var(--border);border-radius:8px;padding:6px 12px;font-size:12px;font-weight:600">
${d.services.map(s=>`<span style="background:var(--bg2);border:0.5px solid var(--border);border-radius: var(--radius-md);padding:6px 12px;font-size:12px;font-weight:600">
<span class="dot dot-ok"></span>${s.name} <span style="color:var(--text3)">${s.latency_ms}ms</span>
</span>`).join('')}
</div>`;
}
/* ── SUCCESS TREND ──────────────────────────────────────────── */
/* SUCCESS TREND */
async function loadSuccess() {
const d = await fetchOrSeed('https://api.quixzoom.com/api/qz/analytics/trend', seedTrend);
if(!d) return;
@@ -590,7 +590,7 @@ async function loadSuccess() {
</tr>`;}).join('');
}
/* ── EARNINGS ──────────────────────────────────────────────── */
/* EARNINGS */
async function loadEarnings() {
const d = await fetchOrSeed('https://api.quixzoom.com/api/qz/analytics/earnings', seedEarnings);
if(!d) return;
@@ -640,7 +640,7 @@ async function loadEarnings() {
</div>`).join('');
}
/* ── KYC FUNNEL ──────────────────────────────────────────── */
/* KYC FUNNEL */
async function loadFunnel() {
const d = await fetchOrSeed('https://api.quixzoom.com/api/qz/analytics/funnel', seedFunnel);
if(!d) return;
@@ -678,7 +678,7 @@ async function loadFunnel() {
].map(m=>`<div class="metric-card"><div class="metric-label">${m.label}</div><div class="metric-value" style="color:${m.color}">${m.value}</div></div>`).join('');
document.getElementById('dropoff-insights').innerHTML = d.drop_off_insights.map(i=>`
<div style="display:flex;align-items:center;gap:14px;padding:14px;background:var(--bg2);border-radius:12px;margin-bottom:8px">
<div style="display:flex;align-items:center;gap:14px;padding:14px;background:var(--bg2);border-radius: var(--radius-md);margin-bottom:8px">
<span class="badge badge-orange">${i.funnel}</span>
<div style="flex:1">
<div style="font-size:13px;font-weight:600">${i.step}</div>
@@ -688,7 +688,7 @@ async function loadFunnel() {
</div>`).join('');
}
/* ── GEOGRAPHIC ──────────────────────────────────────────── */
/* GEOGRAPHIC */
async function loadGeo() {
const d = await fetchOrSeed('https://api.quixzoom.com/api/qz/analytics/geographic', seedGeo);
if(!d) return;
@@ -719,7 +719,7 @@ async function loadGeo() {
</div>`).join('');
}
/* ── REVENUE ──────────────────────────────────────────────── */
/* REVENUE */
async function loadRevenue() {
const d = await fetchOrSeed('https://api.quixzoom.com/api/qz/analytics/revenue', seedRevenue);
if(!d) return;
@@ -743,7 +743,7 @@ async function loadRevenue() {
document.querySelector('#revenue-table tbody').innerHTML = types.map((t,i)=>`
<tr>
<td style="font-weight:600"><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:${cols[i%cols.length]};margin-right:8px;vertical-align:middle"></span>${t.type}</td>
<td style="font-weight:600"><span style="display:inline-block;width:10px;height:10px;border-radius: var(--radius-sm);background:${cols[i%cols.length]};margin-right:8px;vertical-align:middle"></span>${t.type}</td>
<td>${fmtSEK(t.missions)}</td>
<td style="font-weight:700">${fmtSEK(t.revenue_sek)} kr</td>
<td>${fmtSEK(t.avg_per_mission)} kr</td>
@@ -751,7 +751,7 @@ async function loadRevenue() {
</tr>`).join('');
}
/* ── HEALTH ──────────────────────────────────────────────── */
/* HEALTH */
async function loadHealth() {
const d = await fetchOrSeed('https://api.quixzoom.com/api/qz/analytics/health', seedHealth);
if(!d) return;
@@ -779,10 +779,10 @@ async function loadHealth() {
</div>`).join('');
document.getElementById('realtime-stats').innerHTML = [
{label:'Live Zoomers', value:d.active_zoomers_now, icon:'👤'},
{label:'Aktiva uppdrag', value:d.active_missions_now, icon:'📍'},
{label:'WebSocket conns', value:d.websocket_conns, icon:'🔌'},
{label:'Queue djup', value:d.queue_depth, icon:'📬'},
{label:'Live Zoomers', value:d.active_zoomers_now, icon:''},
{label:'Aktiva uppdrag', value:d.active_missions_now, icon:''},
{label:'WebSocket conns', value:d.websocket_conns, icon:''},
{label:'Queue djup', value:d.queue_depth, icon:''},
].map(x=>`
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:0.5px solid var(--border)">
<span style="font-size:13px;font-weight:500">${x.icon} ${x.label}</span>
@@ -797,7 +797,7 @@ async function loadHealth() {
</tr>`).join('');
}
/* ── LOAD ALL ──────────────────────────────────────────── */
/* LOAD ALL */
async function loadAll() {
await Promise.all([
loadOverview(),
@@ -813,7 +813,7 @@ async function loadAll() {
document.getElementById('last-updated').textContent = new Date().toLocaleTimeString('sv-SE');
}
/* ── AUTO-REFRESH ─────────────────────────────────────── */
/* AUTO-REFRESH */
window.addEventListener('load', ()=>{
setTimeout(loadAll, 80);
setInterval(loadAll, 30000);
+35 -35
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>quiXzoom API Status</title>
<style>
/* ── iOS 26 / Apple Native Design System ─────────────────────────────── */
/* iOS 26 / Apple Native Design System */
:root {
--bg: #000000;
--bg-secondary: #1c1c1e;
@@ -44,7 +44,7 @@
overscroll-behavior-y: contain;
}
/* ── Navigation Bar ───────────────────────────────────────────────────── */
/* Navigation Bar */
.nav-bar {
position: sticky;
top: 0;
@@ -70,7 +70,7 @@
color: var(--label-secondary);
}
/* ── Large Title ──────────────────────────────────────────────────────── */
/* Large Title */
.page-header {
padding: 24px 20px 8px;
}
@@ -87,7 +87,7 @@
margin-top: 4px;
}
/* ── Summary Cards ────────────────────────────────────────────────────── */
/* Summary Cards */
.summary-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
@@ -107,7 +107,7 @@
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
border-radius: 2px 2px 0 0;
border-radius: var(--radius-sm) 2px 0 0;
}
.stat-card.green::before { background: var(--green); }
.stat-card.red::before { background: var(--red); }
@@ -131,7 +131,7 @@
letter-spacing: 0.3px;
}
/* ── Section ──────────────────────────────────────────────────────────── */
/* Section */
.section {
margin-top: 28px;
}
@@ -159,7 +159,7 @@
}
.section-action:active { opacity: 0.6; }
/* ── Endpoint List ────────────────────────────────────────────────────── */
/* Endpoint List */
.endpoint-list {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
@@ -186,7 +186,7 @@
font-weight: 700;
letter-spacing: 0.4px;
padding: 3px 7px;
border-radius: 5px;
border-radius: var(--radius-sm);
font-family: "SF Mono", "Menlo", monospace;
}
.method-GET { background: rgba(48,209,88,0.2); color: var(--green); }
@@ -226,7 +226,7 @@
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
border-radius: var(--radius-full);
flex-shrink: 0;
}
.dot-green { background: var(--green); box-shadow: 0 0 6px rgba(48,209,88,0.5); }
@@ -243,7 +243,7 @@
.resp-time.fast { color: var(--green); }
.resp-time.slow { color: var(--orange); }
/* ── Test Button ──────────────────────────────────────────────────────── */
/* Test Button */
.test-btn {
display: inline-flex;
align-items: center;
@@ -272,7 +272,7 @@
pointer-events: none;
}
/* ── Detail Row (expandable) ─────────────────────────────────────────── */
/* Detail Row (expandable) */
.endpoint-detail {
display: none;
background: var(--bg-tertiary);
@@ -301,7 +301,7 @@
word-break: break-all;
}
/* ── Run All Button ───────────────────────────────────────────────────── */
/* Run All Button */
.run-all-container {
padding: 20px 16px 8px;
}
@@ -333,7 +333,7 @@
pointer-events: none;
}
/* ── Last Run ─────────────────────────────────────────────────────────── */
/* Last Run */
.last-run {
text-align: center;
font-size: 12px;
@@ -341,7 +341,7 @@
padding: 8px 20px 0;
}
/* ── Toast ────────────────────────────────────────────────────────────── */
/* Toast */
.toast {
position: fixed;
bottom: calc(var(--safe-bottom) + 20px);
@@ -354,7 +354,7 @@
font-size: 14px;
font-weight: 500;
padding: 12px 20px;
border-radius: 999px;
border-radius: var(--radius-full);
z-index: 999;
transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1), opacity 0.3s;
opacity: 0;
@@ -366,34 +366,34 @@
opacity: 1;
}
/* ── Progress bar ─────────────────────────────────────────────────────── */
/* Progress bar */
.progress-bar {
height: 3px;
background: var(--bg-tertiary);
border-radius: 2px;
border-radius: var(--radius-sm);
margin: 0 16px 0;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--tint);
border-radius: 2px;
border-radius: var(--radius-sm);
width: 0%;
transition: width 0.3s ease;
}
/* ── Spinner ──────────────────────────────────────────────────────────── */
/* Spinner */
@keyframes spin { to { transform: rotate(360deg); } }
.spinner {
width: 14px; height: 14px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
border-radius: var(--radius-full);
animation: spin 0.6s linear infinite;
flex-shrink: 0;
}
/* ── Footer ───────────────────────────────────────────────────────────── */
/* Footer */
.footer {
text-align: center;
padding: 32px 20px 8px;
@@ -468,12 +468,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Run All -->
<div class="run-all-container">
<button class="run-all-btn" id="run-all-btn" onclick="runAll()">
<span> Testa alla endpoints</span>
<span> Testa alla endpoints</span>
</button>
</div>
<div class="last-run" id="last-run">Inte testad ännu</div>
<!-- ── Endpoint Sections ───────────────────────────────────────────────── -->
<!-- Endpoint Sections -->
<!-- Zoomers / Profiles -->
<div class="section">
@@ -877,7 +877,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="toast" id="toast"></div>
<script>
// ── Endpoint definitions ───────────────────────────────────────────────
// Endpoint definitions
const BASE = '';
const ENDPOINTS = {
@@ -910,7 +910,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
const results = {};
let testingAll = false;
// ── Test single endpoint ───────────────────────────────────────────────
// Test single endpoint
async function testEndpoint(id) {
const ep = ENDPOINTS[id];
if (!ep) return;
@@ -944,7 +944,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (resp) resp.textContent = JSON.stringify(data).slice(0, 120) + (JSON.stringify(data).length > 120 ? '…' : '');
btn.classList.remove('testing');
btn.textContent = ok ? '✓' : '✗';
btn.textContent = ok ? '<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>' : '<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>';
btn.style.background = ok ? 'var(--green)' : 'var(--red)';
setTimeout(() => {
btn.textContent = 'Test';
@@ -959,14 +959,14 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
ms.textContent = `${elapsed}ms`;
if (resp) resp.textContent = String(e).slice(0, 120);
btn.classList.remove('testing');
btn.textContent = '';
btn.textContent = '<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>';
btn.style.background = 'var(--red)';
setTimeout(() => { btn.textContent = 'Test'; btn.style.background = ''; }, 2000);
updateSummary();
}
}
// ── Test a section ─────────────────────────────────────────────────────
// Test a section
async function testSection(section) {
const ids = Object.keys(ENDPOINTS).filter(id => ENDPOINTS[id].section === section);
for (const id of ids) {
@@ -975,7 +975,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
}
}
// ── Run All ────────────────────────────────────────────────────────────
// Run All
async function runAll() {
if (testingAll) return;
testingAll = true;
@@ -994,15 +994,15 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
prog.style.width = '100%';
testingAll = false;
btn.disabled = false;
btn.innerHTML = '<span> Kör igen</span>';
btn.innerHTML = '<span> Kör igen</span>';
const now = new Date().toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
document.getElementById('last-run').textContent = `Senast testad: ${now}`;
const okCount = Object.values(results).filter(r => r.ok).length;
showToast(`${okCount}/${ids.length} endpoints `);
showToast(`${okCount}/${ids.length} endpoints <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>`);
}
// ── Update summary bar ─────────────────────────────────────────────────
// Update summary bar
function updateSummary() {
const vals = Object.values(results);
const ok = vals.filter(r => r.ok).length;
@@ -1019,14 +1019,14 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
document.getElementById('nav-status').textContent = `${tested}/${total} testad`;
}
// ── Toggle detail row ──────────────────────────────────────────────────
// Toggle detail row
function toggleDetail(id) {
const el = document.getElementById(`detail-${id}`);
if (!el) return;
el.classList.toggle('open');
}
// ── Toast ──────────────────────────────────────────────────────────────
// Toast
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
@@ -1036,7 +1036,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
// ── Auto-run on load ───────────────────────────────────────────────────
// Auto-run on load
window.addEventListener('load', () => {
setTimeout(runAll, 600);
});
+50 -50
View File
@@ -32,7 +32,7 @@
.nav-badge {
font-size: 12px; font-weight: 600; padding: 4px 10px;
background: rgba(10,132,255,0.15); color: var(--ios-blue);
border-radius: 20px;
border-radius: var(--radius-xl);
}
/* Hero */
@@ -53,7 +53,7 @@
.progress-overview {
margin: 0 16px 20px;
background: var(--background-secondary);
border-radius: 16px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
padding: 16px;
}
@@ -64,10 +64,10 @@
.progress-label-text { font-size: 14px; color: var(--label-secondary); }
.progress-label-count { font-size: 14px; font-weight: 700; color: var(--ios-blue); }
.progress-track {
height: 8px; background: var(--fill-secondary); border-radius: 4px; overflow: hidden;
height: 8px; background: var(--fill-secondary); border-radius: var(--radius-sm); overflow: hidden;
}
.progress-fill {
height: 100%; border-radius: 4px;
height: 100%; border-radius: var(--radius-sm);
background: linear-gradient(90deg, var(--ios-blue), var(--ios-indigo));
transition: width 0.8s cubic-bezier(0.34,1.56,0.64,1);
}
@@ -75,7 +75,7 @@
display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap;
}
.progress-chip {
font-size: 12px; font-weight: 500; padding: 4px 10px; border-radius: 12px;
font-size: 12px; font-weight: 500; padding: 4px 10px; border-radius: var(--radius-md);
}
.chip-done { background: rgba(48,209,88,0.15); color: var(--ios-green); }
.chip-todo { background: var(--fill-secondary); color: var(--label-secondary); }
@@ -96,16 +96,16 @@
border: 0.5px solid var(--separator);
margin-bottom: -0.5px;
}
.step-card:first-child { border-radius: 14px 14px 0 0; }
.step-card:first-child { border-radius: var(--radius-lg) 14px 0 0; }
.step-card:last-child { border-radius: 0 0 14px 14px; margin-bottom: 0; }
.step-card:only-child { border-radius: 14px; }
.step-card:only-child { border-radius: var(--radius-lg); }
.step-header {
display: flex; align-items: center; gap: 14px;
padding: 16px; cursor: pointer;
}
.step-number {
width: 32px; height: 32px; border-radius: 50%;
width: 32px; height: 32px; border-radius: var(--radius-full);
display: flex; align-items: center; justify-content: center;
font-size: 14px; font-weight: 700; flex-shrink: 0;
}
@@ -128,17 +128,17 @@
.step-detail-row {
display: flex; gap: 8px; margin-bottom: 8px; align-items: flex-start;
}
.step-detail-dot { margin-top: 5px; width: 5px; height: 5px; border-radius: 50%; background: var(--ios-blue); flex-shrink: 0; }
.step-detail-dot { margin-top: 5px; width: 5px; height: 5px; border-radius: var(--radius-full); background: var(--ios-blue); flex-shrink: 0; }
.step-detail-text { flex: 1; color: var(--label-primary); }
.step-code {
font-family: var(--font-mono); font-size: 13px;
background: var(--fill-secondary); padding: 2px 7px; border-radius: 5px;
background: var(--fill-secondary); padding: 2px 7px; border-radius: var(--radius-sm);
color: var(--ios-teal);
}
.step-cta {
display: inline-flex; align-items: center; gap: 6px;
margin-top: 10px; padding: 9px 16px; border-radius: 10px;
margin-top: 10px; padding: 9px 16px; border-radius: var(--radius-md);
background: rgba(10,132,255,0.12); color: var(--ios-blue);
font-size: 14px; font-weight: 600; text-decoration: none;
transition: opacity 0.12s;
@@ -148,7 +148,7 @@
/* Legal checklist */
.legal-card {
background: var(--background-secondary);
border-radius: 14px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.legal-row {
@@ -170,7 +170,7 @@
}
.screenshot-card {
background: var(--background-secondary);
border-radius: 12px; overflow: hidden;
border-radius: var(--radius-md); overflow: hidden;
border: 0.5px solid var(--separator);
padding: 14px 10px; text-align: center;
}
@@ -182,7 +182,7 @@
.info-card {
background: rgba(10,132,255,0.08);
border: 0.5px solid rgba(10,132,255,0.2);
border-radius: 14px; padding: 14px 16px;
border-radius: var(--radius-lg); padding: 14px 16px;
margin-top: 12px;
}
.info-card-title { font-size: 13px; font-weight: 600; color: var(--ios-blue); margin-bottom: 4px; }
@@ -192,7 +192,7 @@
.warn-card {
background: rgba(255,159,10,0.08);
border: 0.5px solid rgba(255,159,10,0.2);
border-radius: 14px; padding: 14px 16px;
border-radius: var(--radius-lg); padding: 14px 16px;
margin: 16px 16px 8px;
}
.warn-card-title { font-size: 13px; font-weight: 600; color: var(--ios-orange); margin-bottom: 4px; }
@@ -247,7 +247,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Hero -->
<div class="hero">
<div class="hero-icon">📱</div>
<div class="hero-icon"></div>
<div class="hero-title">App Store Submission</div>
<div class="hero-sub">Steg-för-steg guide för quiXzoom<br>iOS App — augusti 2026</div>
</div>
@@ -262,9 +262,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="progress-fill" id="progress-fill" style="width:33%"></div>
</div>
<div class="progress-chips">
<span class="progress-chip chip-done"> Privacy Policy</span>
<span class="progress-chip chip-done"> Support URL</span>
<span class="progress-chip chip-done"> Terms of Service</span>
<span class="progress-chip chip-done"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Privacy Policy</span>
<span class="progress-chip chip-done"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Support URL</span>
<span class="progress-chip chip-done"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Terms of Service</span>
<span class="progress-chip chip-todo">⏳ SwiftUI App</span>
<span class="progress-chip chip-todo">⏳ Screenshots</span>
<span class="progress-chip chip-todo">⏳ App Store Connect</span>
@@ -284,7 +284,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="step-title">App Store Connect konto</div>
<div class="step-desc">Apple Developer Program · $99/år</div>
</div>
<span class="step-check">⚠️</span>
<span class="step-check"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></span>
</div>
<div class="step-body" id="step-1">
<div class="step-detail-row">
@@ -300,7 +300,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="step-detail-text">Navigera till "Apps" → klicka "+" → "New App"</div>
</div>
<a class="step-cta" href="https://appstoreconnect.apple.com" target="_blank">
🔗 Öppna App Store Connect
Öppna App Store Connect
</a>
</div>
</div>
@@ -313,7 +313,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="step-title">App ID & Bundle ID</div>
<div class="step-desc">Registrera unik identifierare</div>
</div>
<span class="step-check">⚠️</span>
<span class="step-check"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></span>
</div>
<div class="step-body" id="step-2">
<div class="step-detail-row">
@@ -333,7 +333,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="step-detail-text">Capabilities att aktivera: Push Notifications, Sign in with Apple, Location Services</div>
</div>
<div class="info-card">
<div class="info-card-title">💡 Bundle ID</div>
<div class="info-card-title"> Bundle ID</div>
<div class="info-card-body">Bundle ID är permanent och kan inte ändras efter publicering. Använd <code>se.wavult.quixzoom</code> exakt.</div>
</div>
</div>
@@ -347,7 +347,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="step-title">App Metadata</div>
<div class="step-desc">Namn, kategori, beskrivning</div>
</div>
<span class="step-check">⚠️</span>
<span class="step-check"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></span>
</div>
<div class="step-body" id="step-3">
<div class="step-detail-row">
@@ -389,22 +389,22 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="step-title">Screenshots</div>
<div class="step-desc">3 storlekar krävda av Apple</div>
</div>
<span class="step-check">⚠️</span>
<span class="step-check"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></span>
</div>
<div class="step-body" id="step-4">
<div class="screenshot-grid">
<div class="screenshot-card">
<div class="screenshot-icon">📱</div>
<div class="screenshot-icon"></div>
<div class="screenshot-size">6.7"</div>
<div class="screenshot-note">iPhone 15 Plus<br>1290×2796</div>
</div>
<div class="screenshot-card">
<div class="screenshot-icon">📱</div>
<div class="screenshot-icon"></div>
<div class="screenshot-size">6.1"</div>
<div class="screenshot-note">iPhone 15<br>1179×2556</div>
</div>
<div class="screenshot-card">
<div class="screenshot-icon">🖥</div>
<div class="screenshot-icon"></div>
<div class="screenshot-size">iPad 12.9"</div>
<div class="screenshot-note">iPad Pro<br>2048×2732</div>
</div>
@@ -422,7 +422,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="step-detail-text">Verktyg: Figma + iPhone Simulator (Xcode), eller Rottenwood/AppLaunchpad</div>
</div>
<div class="info-card">
<div class="info-card-title">💡 Pro tip</div>
<div class="info-card-title"> Pro tip</div>
<div class="info-card-body">Lägg till device frames och svenska texter i bilderna. Localize screenshots till SV för bättre konvertering.</div>
</div>
</div>
@@ -436,7 +436,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="step-title">App Review Information</div>
<div class="step-desc">Kontaktinfo + demouppgifter</div>
</div>
<span class="step-check">⚠️</span>
<span class="step-check"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></span>
</div>
<div class="step-body" id="step-5">
<div class="step-detail-row">
@@ -474,23 +474,23 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="step-title">Privacy Policy URL</div>
<div class="step-desc">GDPR-kompatibel · live</div>
</div>
<span class="step-check"></span>
<span class="step-check"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</div>
<div class="step-body" id="step-6">
<div class="step-detail-row">
<div class="step-detail-dot"></div>
<div class="step-detail-text">URL: <span class="step-code">https://quixzoom.com/privacy</span> Live</div>
<div class="step-detail-text">URL: <span class="step-code">https://quixzoom.com/privacy</span> <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Live</div>
</div>
<div class="step-detail-row">
<div class="step-detail-dot"></div>
<div class="step-detail-text">GDPR compliance: Täcker datainsamling, behandling, raderingsrätt</div>
<div class="step-detail-text">GDPR compliance: <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Täcker datainsamling, behandling, raderingsrätt</div>
</div>
<div class="step-detail-row">
<div class="step-detail-dot"></div>
<div class="step-detail-text">Måste vara tillgänglig utan inloggning (publikt URL)</div>
</div>
<a class="step-cta" href="https://quixzoom.com/privacy" target="_blank">
🔗 Visa Privacy Policy
Visa Privacy Policy
</a>
</div>
</div>
@@ -503,12 +503,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="step-title">Support URL</div>
<div class="step-desc">Kundservice · live</div>
</div>
<span class="step-check"></span>
<span class="step-check"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</div>
<div class="step-body" id="step-7">
<div class="step-detail-row">
<div class="step-detail-dot"></div>
<div class="step-detail-text">URL: <span class="step-code">https://quixzoom.com/support</span> Live</div>
<div class="step-detail-text">URL: <span class="step-code">https://quixzoom.com/support</span> <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Live</div>
</div>
<div class="step-detail-row">
<div class="step-detail-dot"></div>
@@ -519,7 +519,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="step-detail-text">Phone: <span class="step-code">+46 8 123 45 67</span></div>
</div>
<a class="step-cta" href="https://quixzoom.com/support" target="_blank">
🔗 Visa Support-sida
Visa Support-sida
</a>
</div>
</div>
@@ -579,7 +579,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="info-card">
<div class="info-card-title">⏱ Tidslinje</div>
<div class="info-card-body">Lämna in senast 24 juli → App Review 25 dagar → Klar 3031 juli → Launch Day 1 augusti 🚀</div>
<div class="info-card-body">Lämna in senast 24 juli → App Review 25 dagar → Klar 3031 juli → Launch Day 1 augusti <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg></div>
</div>
</div>
</div>
@@ -592,15 +592,15 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Juridisk checklista</div>
<div class="legal-card">
<div class="legal-row">
<span class="legal-icon">🇪🇺</span>
<span class="legal-icon"></span>
<div class="legal-info">
<div class="legal-title">GDPR Compliance</div>
<div class="legal-sub">Privacy policy live, dataskyddsombud</div>
</div>
<span class="legal-status"></span>
<span class="legal-status"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</div>
<div class="legal-row">
<span class="legal-icon">📊</span>
<span class="legal-icon"></span>
<div class="legal-info">
<div class="legal-title">DAC7 Rapportering</div>
<div class="legal-sub">EU-direktiv för plattformsekonomin — pågår</div>
@@ -608,43 +608,43 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="legal-status"></span>
</div>
<div class="legal-row">
<span class="legal-icon">📄</span>
<span class="legal-icon"></span>
<div class="legal-info">
<div class="legal-title">Zoomer-avtal</div>
<div class="legal-sub">Uppdragstagares villkor — behövs innan launch</div>
</div>
<span class="legal-status"></span>
<span class="legal-status"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg></span>
</div>
<div class="legal-row">
<span class="legal-icon">📱</span>
<span class="legal-icon"></span>
<div class="legal-info">
<div class="legal-title">SwiftUI App</div>
<div class="legal-sub">Native iOS-app för App Store</div>
</div>
<span class="legal-status"></span>
<span class="legal-status"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg></span>
</div>
<div class="legal-row">
<span class="legal-icon">⚖️</span>
<span class="legal-icon"></span>
<div class="legal-info">
<div class="legal-title">Terms of Service</div>
<div class="legal-sub">Användarvillkor live</div>
</div>
<span class="legal-status"></span>
<span class="legal-status"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</div>
<div class="legal-row">
<span class="legal-icon">✈️</span>
<span class="legal-icon"></span>
<div class="legal-info">
<div class="legal-title">Drönartillstånd (LFV/Transportstyrelsen)</div>
<div class="legal-sub">Zoomer ansvarar för eget tillstånd</div>
</div>
<span class="legal-status">⚠️</span>
<span class="legal-status"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></span>
</div>
</div>
</div>
<!-- Critical path warning -->
<div class="warn-card">
<div class="warn-card-title">⚠️ Kritisk väg till 1 augusti</div>
<div class="warn-card-title"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Kritisk väg till 1 augusti</div>
<div class="warn-card-body">SwiftUI-app måste vara klar senast <strong>5 juni</strong> för att hinna med screenshots, testning och App Review-processen. Det är den enda blockande faktorn för en lyckad launch.</div>
</div>
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180">
<defs>
<linearGradient id="qz-bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0A0A0A"/>
<stop offset="100%" stop-color="#1A1A1A"/>
</linearGradient>
<linearGradient id="qz-accent" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0066FF"/>
<stop offset="100%" stop-color="#0052CC"/>
</linearGradient>
</defs>
<rect width="180" height="180" rx="40" fill="url(#qz-bg)"/>
<circle cx="90" cy="90" r="56" fill="none" stroke="url(#qz-accent)" stroke-width="11"/>
<circle cx="90" cy="90" r="28" fill="url(#qz-accent)"/>
<circle cx="126" cy="54" r="8" fill="#FF3366"/>
</svg>

After

Width:  |  Height:  |  Size: 724 B

+51 -51
View File
@@ -19,12 +19,12 @@ body{font-family:-apple-system,'SF Pro Text',system-ui,sans-serif;background:#F2
.nav-inner{height:44px;display:flex;align-items:center;padding:0 16px;gap:12px}
.nav-title{flex:1;text-align:center;font-size:17px;font-weight:600;letter-spacing:-.4px}
.nav-logo{font-size:20px;font-weight:900;background:linear-gradient(135deg,#FF6B35,#F7C948);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.5px}
.nav-btn-icon{width:32px;height:32px;border-radius:16px;background:#007AFF;color:#fff;border:none;font-size:14px;display:flex;align-items:center;justify-content:center;cursor:pointer}
.nav-btn-icon{width:32px;height:32px;border-radius: var(--radius-lg);background:#007AFF;color:#fff;border:none;font-size:14px;display:flex;align-items:center;justify-content:center;cursor:pointer}
/* Customer switcher */
.customer-switcher{background:#fff;border-bottom:.33px solid rgba(60,60,67,.12);padding:10px 16px;display:flex;gap:8px;overflow-x:auto;scrollbar-width:none}
.customer-switcher::-webkit-scrollbar{display:none}
.cust-chip{padding:7px 14px;border-radius:20px;font-size:13px;font-weight:600;cursor:pointer;border:none;background:#F2F2F7;color:#000;white-space:nowrap;transition:all .15s;-webkit-tap-highlight-color:transparent}
.cust-chip{padding:7px 14px;border-radius: var(--radius-xl);font-size:13px;font-weight:600;cursor:pointer;border:none;background:#F2F2F7;color:#000;white-space:nowrap;transition:all .15s;-webkit-tap-highlight-color:transparent}
.cust-chip.active{background:#007AFF;color:#fff}
/* Large title */
@@ -46,7 +46,7 @@ body{font-family:-apple-system,'SF Pro Text',system-ui,sans-serif;background:#F2
/* KPI strip */
.kpi-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;padding:12px 16px 8px}
.kpi-box{background:#fff;border-radius:12px;padding:14px 12px;text-align:center;box-shadow:0 1px 0 rgba(0,0,0,.08)}
.kpi-box{background:#fff;border-radius: var(--radius-md);padding:14px 12px;text-align:center;box-shadow:0 1px 0 rgba(0,0,0,.08)}
.kpi-val{font-size:20px;font-weight:700;letter-spacing:-.5px}
.kpi-val-accent{color:#FF6B35}
.kpi-lbl{font-size:10px;color:#666666;font-weight:500;margin-top:2px}
@@ -56,11 +56,11 @@ body{font-family:-apple-system,'SF Pro Text',system-ui,sans-serif;background:#F2
.section-header{font-size:13px;font-weight:600;color:#555555;text-transform:uppercase;padding:0 32px 6px}
/* Card list */
.card-list{background:#fff;border-radius:12px;margin:0 16px;overflow:hidden;box-shadow:0 1px 0 rgba(0,0,0,.08)}
.card-list{background:#fff;border-radius: var(--radius-md);margin:0 16px;overflow:hidden;box-shadow:0 1px 0 rgba(0,0,0,.08)}
.card-item{display:flex;align-items:center;gap:12px;padding:13px 16px;border-bottom:.33px solid rgba(60,60,67,.12);-webkit-tap-highlight-color:transparent;transition:background .1s}
.card-item:last-child{border-bottom:none}
.card-item:active{background:#F2F2F7}
.card-icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0}
.card-icon{width:40px;height:40px;border-radius: var(--radius-md);display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0}
.ci-blue{background:#EBF5FF}.ci-green{background:#E8F8EE}.ci-orange{background:#FFF3E0}.ci-red{background:#FFE8E6}.ci-purple{background:#F4EEFF}.ci-gray{background:#F2F2F7}
.card-body{flex:1;min-width:0}
.card-title{font-size:15px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@@ -69,7 +69,7 @@ body{font-family:-apple-system,'SF Pro Text',system-ui,sans-serif;background:#F2
.chevron{color:#C7C7CC;font-size:16px}
/* Badges */
.badge{display:inline-flex;align-items:center;padding:3px 8px;border-radius:6px;font-size:12px;font-weight:600;white-space:nowrap}
.badge{display:inline-flex;align-items:center;padding:3px 8px;border-radius: var(--radius-sm);font-size:12px;font-weight:600;white-space:nowrap}
.bg-green{background:#E8F8EE;color:#1D7A3F}
.bg-orange{background:#FFF3E0;color:#C07000}
.bg-blue{background:#EBF5FF;color:#0051A8}
@@ -81,8 +81,8 @@ body{font-family:-apple-system,'SF Pro Text',system-ui,sans-serif;background:#F2
.stars{color:#FFD60A;font-size:13px}
/* Favorite zoomer card */
.fav-card{background:#fff;border-radius:14px;margin:0 16px;padding:16px;display:flex;align-items:center;gap:14px;box-shadow:0 1px 0 rgba(0,0,0,.08);margin-bottom:8px}
.fav-avatar{width:52px;height:52px;border-radius:26px;background:linear-gradient(135deg,#FF6B35,#F7C948);display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0}
.fav-card{background:#fff;border-radius: var(--radius-lg);margin:0 16px;padding:16px;display:flex;align-items:center;gap:14px;box-shadow:0 1px 0 rgba(0,0,0,.08);margin-bottom:8px}
.fav-avatar{width:52px;height:52px;border-radius: 26px;background:linear-gradient(135deg,#FF6B35,#F7C948);display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0}
.fav-name{font-size:16px;font-weight:700}
.fav-spec{font-size:13px;color:#666666}
.fav-meta{font-size:12px;color:#666666;margin-top:4px}
@@ -93,22 +93,22 @@ body{font-family:-apple-system,'SF Pro Text',system-ui,sans-serif;background:#F2
.bar-chart-title{font-size:15px;font-weight:600;margin-bottom:14px}
.bar-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.bar-label{font-size:12px;color:#555555;width:80px;flex-shrink:0}
.bar-track{flex:1;height:8px;background:#E9E9EB;border-radius:4px;overflow:hidden}
.bar-fill{height:8px;border-radius:4px;background:linear-gradient(90deg,#FF6B35,#F7C948);transition:width .7s ease}
.bar-track{flex:1;height:8px;background:#E9E9EB;border-radius: var(--radius-sm);overflow:hidden}
.bar-fill{height:8px;border-radius: var(--radius-sm);background:linear-gradient(90deg,#FF6B35,#F7C948);transition:width .7s ease}
.bar-val{font-size:12px;color:#555555;width:80px;text-align:right;flex-shrink:0}
/* Book modal */
.modal-overlay{position:fixed;inset:0;background: rgba(242,242,247,.4);z-index:300;display:none;align-items:flex-end}
.modal-overlay.open{display:flex}
.modal{background:#fff;border-radius:20px 20px 0 0;padding:20px 20px calc(20px + env(safe-area-inset-bottom,0px));width:100%;max-height:90dvh;overflow-y:auto}
.modal-handle{width:36px;height:4px;border-radius:2px;background:#C7C7CC;margin:0 auto 16px}
.modal{background:#fff;border-radius: var(--radius-xl) 20px 0 0;padding:20px 20px calc(20px + env(safe-area-inset-bottom,0px));width:100%;max-height:90dvh;overflow-y:auto}
.modal-handle{width:36px;height:4px;border-radius: var(--radius-sm);background:#C7C7CC;margin:0 auto 16px}
.modal-title{font-size:20px;font-weight:700;margin-bottom:4px}
.modal-sub{font-size:14px;color:#666666;margin-bottom:20px}
.form-group{margin-bottom:14px}
.form-label{font-size:13px;font-weight:600;color:#555555;margin-bottom:6px;display:block}
.form-input,.form-select{width:100%;background:#F2F2F7;border:none;border-radius:10px;padding:12px 14px;color:#000;font-size:15px;font-family:inherit;outline:none}
.form-input,.form-select{width:100%;background:#F2F2F7;border:none;border-radius: var(--radius-md);padding:12px 14px;color:#000;font-size:15px;font-family:inherit;outline:none}
.btn-row{display:flex;flex-direction:column;gap:10px;margin-top:20px}
.btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:13px;border-radius:12px;font-size:17px;font-weight:600;cursor:pointer;border:none;-webkit-tap-highlight-color:transparent;transition:opacity .15s}
.btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:13px;border-radius: var(--radius-md);font-size:17px;font-weight:600;cursor:pointer;border:none;-webkit-tap-highlight-color:transparent;transition:opacity .15s}
.btn:active{opacity:.7}
.btn-orange{background:linear-gradient(135deg,#FF6B35,#FF9500);color:#fff}
.btn-gray{background:#F2F2F7;color:#000}
@@ -118,7 +118,7 @@ body{font-family:-apple-system,'SF Pro Text',system-ui,sans-serif;background:#F2
.empty-icon{font-size:36px;margin-bottom:8px}
/* Toast */
.toast{position:fixed;bottom:calc(83px + 12px + env(safe-area-inset-bottom,0px));left:50%;transform:translateX(-50%) translateY(60px);background:#323232;color:#fff;padding:10px 18px;border-radius:10px;font-size:14px;font-weight:600;z-index:999;transition:transform .3s;white-space:nowrap}
.toast{position:fixed;bottom:calc(83px + 12px + env(safe-area-inset-bottom,0px));left:50%;transform:translateX(-50%) translateY(60px);background:#323232;color:#fff;padding:10px 18px;border-radius: var(--radius-md);font-size:14px;font-weight:600;z-index:999;transition:transform .3s;white-space:nowrap}
.toast.show{transform:translateX(-50%) translateY(0)}
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
@@ -156,7 +156,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="nav-inner">
<div class="nav-logo">quiX</div>
<div class="nav-title" id="nav-title">Kunddashboard</div>
<button class="btn btn-orange" style="padding:7px 14px;font-size:14px;font-weight:700;border-radius:10px;border:none;color:#fff;background:linear-gradient(135deg,#FF6B35,#FF9500);cursor:pointer" onclick="openBookModal()">+ Boka</button>
<button class="btn btn-orange" style="padding:7px 14px;font-size:14px;font-weight:700;border-radius: var(--radius-md);border:none;color:#fff;background:linear-gradient(135deg,#FF6B35,#FF9500);cursor:pointer" onclick="openBookModal()">+ Boka</button>
</div>
</div>
@@ -187,7 +187,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- HISTORY -->
<div class="content" id="tab-history">
<div style="height:12px"></div>
<div style="background:#fff;border-radius:12px;margin:0 16px" id="spending-chart"></div>
<div style="background:#fff;border-radius: var(--radius-md);margin:0 16px" id="spending-chart"></div>
<div style="height:12px"></div>
<div class="section">
<div class="section-header">Alla uppdrag</div>
@@ -208,17 +208,17 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="content" id="tab-favorites">
<div style="height:12px"></div>
<div style="padding:0 16px 12px;display:flex;justify-content:flex-end">
<button class="btn btn-orange" style="font-size:14px;padding:8px 16px;border-radius:10px;border:none;color:#fff;background:linear-gradient(135deg,#FF6B35,#FF9500);cursor:pointer" onclick="openAddFavModal()">+ Favorit</button>
<button class="btn btn-orange" style="font-size:14px;padding:8px 16px;border-radius: var(--radius-md);border:none;color:#fff;background:linear-gradient(135deg,#FF6B35,#FF9500);cursor:pointer" onclick="openAddFavModal()">+ Favorit</button>
</div>
<div id="favs-list"></div>
</div>
<!-- Tab bar -->
<nav class="tab-bar">
<div class="tab-item active" onclick="showTab('dashboard',this,'Hem')"><div class="tab-icon">🏠</div><div class="tab-label">Hem</div></div>
<div class="tab-item" onclick="showTab('history',this,'Historik')"><div class="tab-icon">📋</div><div class="tab-label">Historik</div></div>
<div class="tab-item" onclick="showTab('billing',this,'Fakturering')"><div class="tab-icon">💳</div><div class="tab-label">Fakturering</div></div>
<div class="tab-item" onclick="showTab('favorites',this,'Favoriter')"><div class="tab-icon"></div><div class="tab-label">Favoriter</div></div>
<div class="tab-item active" onclick="showTab('dashboard',this,'Hem')"><div class="tab-icon"></div><div class="tab-label">Hem</div></div>
<div class="tab-item" onclick="showTab('history',this,'Historik')"><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">Historik</div></div>
<div class="tab-item" onclick="showTab('billing',this,'Fakturering')"><div class="tab-icon"></div><div class="tab-label">Fakturering</div></div>
<div class="tab-item" onclick="showTab('favorites',this,'Favoriter')"><div class="tab-icon"></div><div class="tab-label">Favoriter</div></div>
</nav>
<!-- Book modal -->
@@ -228,13 +228,13 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="modal-title">Boka uppdrag</div>
<div class="modal-sub" id="book-sub">Beskriv ditt uppdrag</div>
<div class="form-group"><label class="form-label">Titel</label><input class="form-input" id="bk-title" placeholder="Vad behöver du hjälp med?"></div>
<div class="form-group"><label class="form-label">Typ</label><select class="form-select" id="bk-type"><option value="photography">Fotografi 📸</option><option value="video">Video 🎬</option><option value="drone">Drönare 🚁</option></select></div>
<div class="form-group"><label class="form-label">Typ</label><select class="form-select" id="bk-type"><option value="photography">Fotografi </option><option value="video">Video </option><option value="drone">Drönare <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></option></select></div>
<div class="form-group"><label class="form-label">Startdatum & tid</label><input class="form-input" type="datetime-local" id="bk-start"></div>
<div class="form-group"><label class="form-label">Duration (timmar)</label><input class="form-input" type="number" id="bk-duration" value="2" min="1"></div>
<div class="form-group"><label class="form-label">Plats</label><input class="form-input" id="bk-location" placeholder="Var ska uppdraget utföras?"></div>
<div class="form-group"><label class="form-label">Budget (SEK)</label><input class="form-input" type="number" id="bk-budget" placeholder="2500"></div>
<div class="btn-row">
<button class="btn btn-orange" onclick="submitBook()">🚀 Boka nu</button>
<button class="btn btn-orange" onclick="submitBook()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> Boka nu</button>
<button class="btn btn-gray" onclick="closeModal('book-modal')">Avbryt</button>
</div>
</div>
@@ -250,7 +250,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="form-group"><label class="form-label">Namn</label><input class="form-input" id="fav-name" placeholder="Förnamn E."></div>
<div class="form-group"><label class="form-label">Specialitet</label><select class="form-select" id="fav-spec"><option>Photography</option><option>Video</option><option>Drone</option><option>General</option></select></div>
<div class="btn-row">
<button class="btn btn-orange" onclick="submitFav()"> Lägg till</button>
<button class="btn btn-orange" onclick="submitFav()"> Lägg till</button>
<button class="btn btn-gray" onclick="closeModal('fav-modal')">Avbryt</button>
</div>
</div>
@@ -259,7 +259,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="toast" id="toast"></div>
<script>
// ── State ──────────────────────────────────────────────────────────────────
// State
const CUSTOMERS = ['cust-101','cust-102','cust-103'];
let activeCustomer = 'cust-101';
let customerCache = {};
@@ -268,20 +268,20 @@ let spendingCache = {};
let invoiceCache = {};
let favCache = {};
// ── Utils ──────────────────────────────────────────────────────────────────
// Utils
function fmtMoney(n){return new Intl.NumberFormat('sv-SE').format(n)+' kr'}
function fmtDate(s){if(!s)return'—';return new Date(s).toLocaleDateString('sv-SE',{day:'2-digit',month:'short'})}
function fmtDateLong(s){if(!s)return'—';return new Date(s).toLocaleDateString('sv-SE',{day:'2-digit',month:'long',year:'numeric'})}
function toast(msg){const el=document.getElementById('toast');el.textContent=msg;el.classList.add('show');setTimeout(()=>el.classList.remove('show'),2500)}
function statusBadge(s){return{completed:'<span class="badge bg-green">✓ Klar</span>',active:'<span class="badge bg-blue">🔄 Aktiv</span>',open:'<span class="badge bg-orange">🔍 Öppen</span>',cancelled:'<span class="badge bg-red">✗ Avbruten</span>'}[s]||`<span class="badge bg-gray">${s}</span>`}
function missionIcon(t){return{photography:'📸',video:'🎬',drone:'🚁'}[t]||'📷'}
function statusBadge(s){return{completed:'<span class="badge bg-green"><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> Klar</span>',active:'<span class="badge bg-blue"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Aktiv</span>',open:'<span class="badge bg-orange"><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> Öppen</span>',cancelled:'<span class="badge bg-red"><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> Avbruten</span>'}[s]||`<span class="badge bg-gray">${s}</span>`}
function missionIcon(t){return{photography:'',video:'',drone:'<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>'}[t]||'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg>'}
function missionIconClass(s){return{completed:'ci-green',active:'ci-blue',open:'ci-orange',cancelled:'ci-red'}[s]||'ci-gray'}
function stars(r){if(!r)return'<span style="color:#666666;font-size:12px">Väntar</span>';return'<span class="stars">'+(''.repeat(Math.round(r))+''.repeat(5-Math.round(r)))+'</span><span style="font-size:12px;color:#666666;margin-left:4px">'+r+'</span>'}
function stars(r){if(!r)return'<span style="color:#666666;font-size:12px">Väntar</span>';return'<span class="stars">'+(''.repeat(Math.round(r))+''.repeat(5-Math.round(r)))+'</span><span style="font-size:12px;color:#666666;margin-left:4px">'+r+'</span>'}
function invStatusBadge(s){return s==='paid'?'<span class="badge bg-green">Betald</span>':'<span class="badge bg-orange">Obetald</span>'}
function closeModal(id){document.getElementById(id).classList.remove('open')}
document.querySelectorAll('.modal-overlay').forEach(m=>m.addEventListener('click',e=>{if(e.target===m)m.classList.remove('open')}));
// ── Tab ────────────────────────────────────────────────────────────────────
// Tab
function showTab(id,el,title){
document.querySelectorAll('.content').forEach(c=>c.classList.remove('active'));
document.querySelectorAll('.tab-item').forEach(t=>t.classList.remove('active'));
@@ -293,7 +293,7 @@ function showTab(id,el,title){
if(id==='favorites')renderFavorites();
}
// ── Customer switcher ──────────────────────────────────────────────────────
// Customer switcher
function renderSwitcher(){
const c = Object.values(customerCache);
document.getElementById('customer-switcher').innerHTML = c.map(cust=>`
@@ -308,7 +308,7 @@ async function switchCustomer(id){
renderDashboard();
}
// ── Data loading ───────────────────────────────────────────────────────────
// Data loading
async function loadAll(){
// Load all customers
const custR=await fetch('https://api.quixzoom.com/api/qz/customers').then(r=>r.json()).catch(()=>({ok:false}));
@@ -333,7 +333,7 @@ async function loadCustomerData(id){
if(fR.ok)favCache[id]=fR.items||[];
}
// ── Dashboard ──────────────────────────────────────────────────────────────
// Dashboard
function renderDashboard(){
const cust=customerCache[activeCustomer];
const missions=missionCache[activeCustomer]||[];
@@ -355,12 +355,12 @@ function renderDashboard(){
const activeMissions=missions.filter(m=>['active','open'].includes(m.status));
document.getElementById('active-missions').innerHTML=activeMissions.length
? activeMissions.map(renderMissionCard).join('')
: '<div class="empty"><div class="empty-icon">🎯</div><div style="font-weight:600">Inga aktiva uppdrag</div><div style="font-size:13px;margin-top:4px">Boka ett nytt uppdrag!</div></div>';
: '<div class="empty"><div class="empty-icon"></div><div style="font-weight:600">Inga aktiva uppdrag</div><div style="font-size:13px;margin-top:4px">Boka ett nytt uppdrag!</div></div>';
// Recent (completed, max 3)
const recent=missions.filter(m=>m.status==='completed').slice(0,3);
document.getElementById('recent-missions').innerHTML=recent.length
? recent.map(renderMissionCard).join('')
: '<div class="empty"><div class="empty-icon">📋</div><div style="font-weight:600">Inga avslutade uppdrag</div></div>';
: '<div class="empty"><div class="empty-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 style="font-weight:600">Inga avslutade uppdrag</div></div>';
}
function renderMissionCard(m){
@@ -378,7 +378,7 @@ function renderMissionCard(m){
</div>`;
}
// ── History tab ────────────────────────────────────────────────────────────
// History tab
function renderHistory(){
const missions=missionCache[activeCustomer]||[];
const spending=spendingCache[activeCustomer]||{};
@@ -400,39 +400,39 @@ function renderHistory(){
// All missions
document.getElementById('all-missions').innerHTML=missions.length
? missions.map(renderMissionCard).join('')
: '<div class="empty"><div class="empty-icon">📋</div>Inga uppdrag</div>';
: '<div class="empty"><div class="empty-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>Inga uppdrag</div>';
}
// ── Billing tab ────────────────────────────────────────────────────────────
// Billing tab
function renderBilling(){
const invs=invoiceCache[activeCustomer]||[];
document.getElementById('invoices-list').innerHTML=invs.length
? invs.map(inv=>`
<div class="card-item">
<div class="card-icon ${inv.status==='paid'?'ci-green':'ci-orange'}">${inv.status==='paid'?'':'🧾'}</div>
<div class="card-icon ${inv.status==='paid'?'ci-green':'ci-orange'}">${inv.status==='paid'?'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>':''}</div>
<div class="card-body"><div class="card-title">${inv.number}</div><div class="card-sub">Förfaller ${fmtDate(inv.due_date)}</div></div>
<div class="card-right">${invStatusBadge(inv.status)}<div style="font-size:14px;font-weight:700;margin-left:8px">${fmtMoney(inv.amount_sek)}</div><span class="chevron"></span></div>
</div>`).join('')
: '<div class="empty"><div class="empty-icon">💳</div>Inga fakturor</div>';
: '<div class="empty"><div class="empty-icon"></div>Inga fakturor</div>';
}
// ── Favorites tab ──────────────────────────────────────────────────────────
// Favorites tab
function renderFavorites(){
const favs=favCache[activeCustomer]||[];
document.getElementById('favs-list').innerHTML=favs.length
? favs.map(f=>`
<div class="fav-card">
<div class="fav-avatar">${f.avatar||'📷'}</div>
<div class="fav-avatar">${f.avatar||'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg>'}</div>
<div>
<div class="fav-name">${f.name}</div>
<div class="fav-spec">${f.specialty}</div>
<div class="fav-meta">${f.missions_completed} uppdrag · ${f.rating?' '+f.rating:'Ny'}</div>
<div class="fav-meta">${f.missions_completed} uppdrag · ${f.rating?' '+f.rating:'Ny'}</div>
</div>
<div class="fav-actions">
<button class="btn btn-gray" style="font-size:13px;padding:7px 12px;border-radius:9px;border:none;cursor:pointer" onclick="removeFav('${f.zoomer_id}')"></button>
<button class="btn btn-gray" style="font-size:13px;padding:7px 12px;border-radius: var(--radius-md);border:none;cursor:pointer" onclick="removeFav('${f.zoomer_id}')"></button>
</div>
</div>`).join('')
: '<div class="empty"><div class="empty-icon"></div><div style="font-weight:600">Inga favoriter</div><div style="font-size:13px;margin-top:4px">Lägg till zoomers du gillar!</div></div>';
: '<div class="empty"><div class="empty-icon"></div><div style="font-weight:600">Inga favoriter</div><div style="font-size:13px;margin-top:4px">Lägg till zoomers du gillar!</div></div>';
}
async function removeFav(zid){
@@ -445,7 +445,7 @@ async function removeFav(zid){
} else toast('Fel: '+r.error);
}
// ── Book modal ─────────────────────────────────────────────────────────────
// Book modal
function openBookModal(){document.getElementById('book-modal').classList.add('open')}
async function submitBook(){
const title=document.getElementById('bk-title').value;
@@ -460,28 +460,28 @@ async function submitBook(){
};
const r=await fetch(`/api/quixzoom/customers/${activeCustomer}/book`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(r=>r.json());
if(r.ok){
toast('🚀 Uppdrag bokat!');closeModal('book-modal');
toast('<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> Uppdrag bokat!');closeModal('book-modal');
const mR=await fetch(`/api/quixzoom/customers/${activeCustomer}/missions`).then(r=>r.json());
missionCache[activeCustomer]=mR.items||[];renderDashboard();
document.getElementById('bk-title').value='';document.getElementById('bk-location').value='';document.getElementById('bk-budget').value='';
} else toast('Fel: '+r.error);
}
// ── Add favorite modal ─────────────────────────────────────────────────────
// Add favorite modal
function openAddFavModal(){document.getElementById('fav-modal').classList.add('open')}
async function submitFav(){
const body={zoomer_id:document.getElementById('fav-id').value,name:document.getElementById('fav-name').value,specialty:document.getElementById('fav-spec').value};
if(!body.zoomer_id||!body.name){toast('Fyll i ID och namn');return}
const r=await fetch(`/api/quixzoom/customers/${activeCustomer}/favorites`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(r=>r.json());
if(r.ok){
toast(' Favorit tillagd!');closeModal('fav-modal');
toast(' Favorit tillagd!');closeModal('fav-modal');
const fR=await fetch(`/api/quixzoom/customers/${activeCustomer}/favorites`).then(r=>r.json());
favCache[activeCustomer]=fR.items||[];renderFavorites();
document.getElementById('fav-id').value='';document.getElementById('fav-name').value='';
} else toast('Fel: '+r.error);
}
// ── Init ───────────────────────────────────────────────────────────────────
// Init
loadAll();
</script>
</body>
+47 -47
View File
@@ -71,7 +71,7 @@ html, body {
-webkit-font-smoothing: antialiased;
}
/* ── Navbar ─────────────────────────────────────────────────────────── */
/* Navbar */
.navbar {
position: fixed;
top: 0; left: 0; right: 0;
@@ -93,7 +93,7 @@ html, body {
.navbar-logo {
width: 28px; height: 28px;
background: linear-gradient(135deg, #007AFF 0%, #5AC8FA 100%);
border-radius: 7px;
border-radius: var(--radius-sm);
display: flex; align-items: center; justify-content: center;
font-size: 15px;
flex-shrink: 0;
@@ -113,11 +113,11 @@ html, body {
color: var(--qx-blue);
background: rgba(0,122,255,.1);
padding: 3px 8px;
border-radius: 20px;
border-radius: var(--radius-xl);
letter-spacing: 0;
}
/* ── Layout ─────────────────────────────────────────────────────────── */
/* Layout */
.scroll-content {
padding-top: calc(var(--safe-top) + 52px + 20px);
padding-bottom: calc(var(--safe-bottom) + 40px);
@@ -127,7 +127,7 @@ html, body {
margin: 0 auto;
}
/* ── Section ─────────────────────────────────────────────────────────── */
/* Section */
.section-label {
font-size: 13px;
font-weight: 600;
@@ -140,7 +140,7 @@ html, body {
}
.section-label:first-child { margin-top: 0; }
/* ── Card ─────────────────────────────────────────────────────────── */
/* Card */
.card {
background: var(--bg-card);
border-radius: var(--radius-lg);
@@ -149,7 +149,7 @@ html, body {
margin-bottom: 20px;
}
/* ── Hero summary ─────────────────────────────────────────────────── */
/* Hero summary */
.hero-card {
background: linear-gradient(160deg, #007AFF 0%, #5856D6 100%);
border-radius: var(--radius-xl);
@@ -164,7 +164,7 @@ html, body {
position: absolute;
top: -40px; right: -40px;
width: 180px; height: 180px;
border-radius: 50%;
border-radius: var(--radius-full);
background: rgba(255,255,255,.08);
}
.hero-tag {
@@ -193,7 +193,7 @@ html, body {
}
.hero-stat {
background: rgba(255,255,255,.15);
border-radius: 12px;
border-radius: var(--radius-md);
padding: 12px;
}
.hero-stat-value {
@@ -207,7 +207,7 @@ html, body {
margin-top: 2px;
}
/* ── Deadline banner ─────────────────────────────────────────────── */
/* Deadline banner */
.deadline-banner {
display: flex;
align-items: center;
@@ -245,7 +245,7 @@ html, body {
margin-top: -2px;
}
/* ── Status pill ─────────────────────────────────────────────────── */
/* Status pill */
.status-row {
display: flex;
align-items: center;
@@ -258,15 +258,15 @@ html, body {
gap: 6px;
font-size: 13px;
font-weight: 600;
border-radius: 20px;
border-radius: var(--radius-xl);
padding: 4px 12px;
}
.status-pill.ready { background: rgba(52,199,89,.15); color: #34C759; }
.status-pill.progress { background: rgba(255,149,0,.15); color: #FF9500; }
.status-pill.overdue { background: rgba(255,59,48,.15); color: #FF3B30; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.status-dot { width: 6px; height: 6px; border-radius: var(--radius-full); background: currentColor; }
/* ── List row ─────────────────────────────────────────────────────── */
/* List row */
.list-row {
display: flex;
align-items: center;
@@ -279,7 +279,7 @@ html, body {
.list-row-icon {
width: 34px; height: 34px;
border-radius: 8px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 17px;
flex-shrink: 0;
@@ -318,14 +318,14 @@ html, body {
margin-top: 1px;
}
/* ── TIN badge ─────────────────────────────────────────────────────── */
/* TIN badge */
.tin-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-weight: 600;
border-radius: 6px;
border-radius: var(--radius-sm);
padding: 2px 7px;
margin-top: 4px;
}
@@ -338,7 +338,7 @@ html, body {
color: #FF9500;
}
/* ── Threshold badge ─────────────────────────────────────────────── */
/* Threshold badge */
.threshold-badge {
display: inline-flex;
align-items: center;
@@ -346,7 +346,7 @@ html, body {
font-weight: 700;
letter-spacing: .05em;
text-transform: uppercase;
border-radius: 5px;
border-radius: var(--radius-sm);
padding: 2px 6px;
}
.threshold-badge.over {
@@ -358,7 +358,7 @@ html, body {
color: var(--label-tertiary);
}
/* ── Export buttons ─────────────────────────────────────────────── */
/* Export buttons */
.export-row {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -392,14 +392,14 @@ html, body {
color: var(--qx-green);
}
/* ── Verify button ─────────────────────────────────────────────── */
/* Verify button */
.verify-btn {
font-size: 13px;
font-weight: 600;
color: var(--qx-blue);
background: rgba(0,122,255,.1);
border: none;
border-radius: 8px;
border-radius: var(--radius-md);
padding: 5px 12px;
cursor: pointer;
transition: opacity .15s;
@@ -413,7 +413,7 @@ html, body {
cursor: default;
}
/* ── Progress bar ─────────────────────────────────────────────── */
/* Progress bar */
.progress-wrap {
padding: 12px 16px;
border-bottom: 0.5px solid var(--separator);
@@ -428,22 +428,22 @@ html, body {
.progress-bar {
height: 6px;
background: var(--fill2);
border-radius: 3px;
border-radius: var(--radius-sm);
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 3px;
border-radius: var(--radius-sm);
background: linear-gradient(90deg, #007AFF, #34C759);
transition: width .6s cubic-bezier(.25,.46,.45,.94);
}
/* ── Loading / error states ──────────────────────────────────── */
/* Loading / error states */
.skeleton {
background: linear-gradient(90deg, var(--fill2) 25%, var(--fill1) 50%, var(--fill2) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 6px;
border-radius: var(--radius-sm);
}
@keyframes shimmer {
0% { background-position: 200% 0; }
@@ -477,7 +477,7 @@ html, body {
}
.error-banner.visible { display: block; }
/* ── Footer note ─────────────────────────────────────────────── */
/* Footer note */
.footer-note {
font-size: 12px;
color: var(--label-tertiary);
@@ -487,7 +487,7 @@ html, body {
line-height: 1.5;
}
/* ── Animations ─────────────────────────────────────────────── */
/* Animations */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
@@ -530,7 +530,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Navbar -->
<nav class="navbar">
<div class="navbar-logo">📍</div>
<div class="navbar-logo"></div>
<div class="navbar-title">DAC7 Rapportering</div>
<div class="navbar-badge">quiXzoom</div>
</nav>
@@ -620,10 +620,10 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="card animate-in">
<div class="export-row">
<button class="export-btn json" onclick="exportReport('json')">
📄 JSON
JSON
</button>
<button class="export-btn xml" onclick="exportReport('xml')">
🗄️ XML (Skatteverket)
XML (Skatteverket)
</button>
</div>
<div style="padding: 0 16px 14px; font-size: 13px; color: var(--label-secondary); line-height: 1.4;">
@@ -655,7 +655,7 @@ const YEAR = new Date().getFullYear();
let allSellers = [];
let statusData = null;
// ── Format helpers ────────────────────────────────────────────────────────
// Format helpers
function formatEur(v) {
return new Intl.NumberFormat('sv-SE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(v);
@@ -692,17 +692,17 @@ function reasonLabel(reason) {
return map[reason] || reason;
}
// ── Render sellers ────────────────────────────────────────────────────────
// Render sellers
function renderSeller(s, showVerify) {
const tin = s.tin_verified
? `<span class="tin-badge verified"> TIN verifierad</span>`
: `<span class="tin-badge unverified"> TIN ej verifierad</span>`;
? `<span class="tin-badge verified"><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> TIN verifierad</span>`
: `<span class="tin-badge unverified"> TIN ej verifierad</span>`;
const verifyBtn = showVerify && !s.tin_verified
? `<button class="verify-btn" onclick="verifySeller('${s.seller_id}')">Verifiera</button>`
: showVerify && s.tin_verified
? `<button class="verify-btn done" disabled> Klar</button>`
? `<button class="verify-btn done" disabled><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> Klar</button>`
: '';
const thresholdBadge = s.reportable
@@ -712,7 +712,7 @@ function renderSeller(s, showVerify) {
return `
<div class="list-row" id="seller-${s.seller_id}">
<div class="list-row-icon" style="background:${s.reportable ? 'rgba(0,122,255,.1)' : 'var(--fill2)'}">
${s.reportable ? '👤' : '👥'}
${s.reportable ? '' : ''}
</div>
<div class="list-row-body">
<div class="list-row-title">${escHtml(s.name)}</div>
@@ -738,7 +738,7 @@ function renderAllSellers(sellers) {
const belowEl = document.getElementById('belowThresholdList');
if (reportable.length === 0) {
sellersEl.innerHTML = `<div class="empty-state"><div class="empty-icon">🎉</div><div class="empty-title">Inga rapportpliktiga Zoomers</div><div class="empty-sub">Alla under tröskelvärdena</div></div>`;
sellersEl.innerHTML = `<div class="empty-state"><div class="empty-icon"></div><div class="empty-title">Inga rapportpliktiga Zoomers</div><div class="empty-sub">Alla under tröskelvärdena</div></div>`;
} else {
sellersEl.innerHTML = reportable.map(s => renderSeller(s, true)).join('');
}
@@ -750,7 +750,7 @@ function renderAllSellers(sellers) {
}
}
// ── Load data ─────────────────────────────────────────────────────────────
// Load data
async function loadData() {
const errorBanner = document.getElementById('errorBanner');
@@ -791,13 +791,13 @@ function renderStatus(data) {
document.getElementById('deadlineDays').innerHTML = `${days}<span>dagar</span>`;
document.getElementById('deadlineDate').textContent = `Deadline: ${data.deadline}`;
if (days < 0) {
document.getElementById('deadlineIcon').textContent = '🚨';
document.getElementById('deadlineIcon').textContent = '';
document.getElementById('deadlineDays').style.color = 'var(--qx-red)';
} else if (days < 30) {
document.getElementById('deadlineIcon').textContent = '⚠️';
document.getElementById('deadlineIcon').textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg>';
document.getElementById('deadlineDays').style.color = 'var(--qx-orange)';
} else {
document.getElementById('deadlineIcon').textContent = '📅';
document.getElementById('deadlineIcon').textContent = '';
document.getElementById('deadlineDays').style.color = 'var(--qx-blue)';
}
@@ -814,7 +814,7 @@ function renderStatus(data) {
document.getElementById('statusText').textContent = statusLabel(data.status);
}
// ── Verify seller ─────────────────────────────────────────────────────────
// Verify seller
async function verifySeller(sellerId) {
const btn = document.querySelector(`#seller-${sellerId} .verify-btn`);
@@ -854,7 +854,7 @@ async function reloadStatus() {
} catch (e) { /* ignore */ }
}
// ── Export ────────────────────────────────────────────────────────────────
// Export
async function exportReport(format) {
try {
@@ -894,13 +894,13 @@ function downloadBlob(blob, filename, type) {
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 1000);
}
// ── Utils ─────────────────────────────────────────────────────────────────
// Utils
function escHtml(str) {
return String(str || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// ── Init ──────────────────────────────────────────────────────────────────
// Init
loadData();
</script>
+47 -47
View File
@@ -50,7 +50,7 @@ body {
padding-bottom: calc(var(--safe-bottom) + 20px);
}
/* ── Navigation Bar ── */
/* Navigation Bar */
.navbar {
position: fixed;
top: 0; left: 0; right: 0;
@@ -72,7 +72,7 @@ body {
.navbar-icon {
width: 30px; height: 30px;
background: linear-gradient(135deg, #007AFF, #5856D6);
border-radius: 8px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 16px;
flex-shrink: 0;
@@ -92,18 +92,18 @@ body {
color: var(--ios-blue);
background: rgba(0,122,255,.1);
padding: 3px 8px;
border-radius: 20px;
border-radius: var(--radius-xl);
}
/* ── Content ── */
/* Content */
.content { padding: 0 16px; }
/* ── Alert Banner ── */
/* Alert Banner */
.deadline-alert {
margin: 20px 0 0;
background: linear-gradient(135deg, rgba(255,59,48,.08), rgba(255,149,0,.08));
border: 1px solid rgba(255,59,48,.2);
border-radius: 16px;
border-radius: var(--radius-lg);
padding: 16px;
display: flex;
align-items: flex-start;
@@ -113,7 +113,7 @@ body {
.alert-icon {
width: 36px; height: 36px;
background: rgba(255,59,48,.12);
border-radius: 10px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 18px;
flex-shrink: 0;
@@ -148,7 +148,7 @@ body {
letter-spacing: 0.5px;
}
/* ── Section Header ── */
/* Section Header */
.section-header {
font-size: 13px;
font-weight: 600;
@@ -158,10 +158,10 @@ body {
padding: 24px 4px 8px;
}
/* ── Cards ── */
/* Cards */
.card {
background: var(--grouped-bg2);
border-radius: 16px;
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: 12px;
}
@@ -183,7 +183,7 @@ body {
line-height: 1.4;
}
/* ── Info Row ── */
/* Info Row */
.info-row {
display: flex;
align-items: center;
@@ -196,7 +196,7 @@ body {
.info-icon {
width: 32px; height: 32px;
border-radius: 8px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 15px;
flex-shrink: 0;
@@ -217,7 +217,7 @@ body {
text-align: right;
}
/* ── Stats Row ── */
/* Stats Row */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
@@ -248,7 +248,7 @@ body {
letter-spacing: 0.3px;
}
/* ── Zoomer Table ── */
/* Zoomer Table */
.zoomer-row {
display: flex;
align-items: center;
@@ -264,7 +264,7 @@ body {
.zoomer-avatar {
width: 40px; height: 40px;
border-radius: 50%;
border-radius: var(--radius-full);
display: flex; align-items: center; justify-content: center;
font-size: 16px;
font-weight: 700;
@@ -296,7 +296,7 @@ body {
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 20px;
border-radius: var(--radius-xl);
margin-top: 4px;
}
@@ -315,10 +315,10 @@ body {
color: var(--ios-orange);
}
/* ── Info Section ── */
/* Info Section */
.info-card {
background: var(--grouped-bg2);
border-radius: 16px;
border-radius: var(--radius-lg);
padding: 16px;
margin-bottom: 12px;
}
@@ -344,7 +344,7 @@ body {
font-weight: 600;
}
/* ── Export Button ── */
/* Export Button */
.export-section {
margin: 8px 0 24px;
}
@@ -359,7 +359,7 @@ body {
background: var(--ios-blue);
color: white;
border: none;
border-radius: 14px;
border-radius: var(--radius-lg);
font-size: 17px;
font-weight: 600;
letter-spacing: -0.41px;
@@ -380,7 +380,7 @@ body {
background: var(--ios-gray6);
color: var(--ios-blue);
border: none;
border-radius: 14px;
border-radius: var(--radius-lg);
font-size: 15px;
font-weight: 600;
letter-spacing: -0.23px;
@@ -391,7 +391,7 @@ body {
.btn-secondary:active { opacity: 0.7; }
/* ── Period Card ── */
/* Period Card */
.period-row {
display: flex;
align-items: center;
@@ -405,7 +405,7 @@ body {
.period-label { font-size: 15px; letter-spacing: -0.23px; }
.period-value { font-size: 15px; font-weight: 500; color: var(--label-secondary); }
/* ── Divider Text ── */
/* Divider Text */
.divider-text {
text-align: center;
font-size: 12px;
@@ -414,7 +414,7 @@ body {
line-height: 1.4;
}
/* ── Toast ── */
/* Toast */
.toast {
position: fixed;
bottom: calc(var(--safe-bottom) + 24px);
@@ -437,18 +437,18 @@ body {
.toast.show { opacity: 1; }
/* ── Progress Bar ── */
/* Progress Bar */
.progress-track {
height: 4px;
background: var(--ios-gray5);
border-radius: 2px;
border-radius: var(--radius-sm);
margin: 12px 16px 14px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 2px;
border-radius: var(--radius-sm);
background: var(--ios-blue);
transition: width 0.5s ease;
}
@@ -485,7 +485,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Navigation Bar -->
<div class="navbar">
<div class="navbar-icon">🧾</div>
<div class="navbar-icon"></div>
<div class="navbar-title">DAC7 Rapportering</div>
<div class="navbar-badge">2026</div>
</div>
@@ -508,7 +508,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- What is DAC7 -->
<div class="section-header">Om DAC7</div>
<div class="info-card">
<div class="info-card-title">📋 Vad är DAC7?</div>
<div class="info-card-title"><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> Vad är DAC7?</div>
<div class="info-card-text">
<strong>EU-direktiv 2021/514 (DAC7)</strong> kräver att digitala plattformar rapporterar säljares (zoomers) intäkter till skattemyndigheterna. quiXzoom är en <strong>rapporteringsskyldig plattformsoperatör</strong> och måste varje år lämna uppgifter om zoomers som:
<br><br>
@@ -520,7 +520,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="info-card">
<div class="info-card-title">🏛️ Vår skyldighet</div>
<div class="info-card-title"> Vår skyldighet</div>
<div class="info-card-text">
quiXzoom samlar in <strong>TIN (skatteregistreringsnummer)</strong> och personuppgifter från alla aktiva zoomers. Vi verifierar uppgifterna och rapporterar automatiskt till Skatteverket via deras API-integration. Zoomers informeras om rapporteringen i tjänstevillkoren.
</div>
@@ -597,7 +597,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="zoomer-info">
<div class="zoomer-name">Alicia Bergström</div>
<div class="zoomer-tin">TIN: 8804127654</div>
<span class="status-badge status-verified"> TIN verifierad</span>
<span class="status-badge status-verified"><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> TIN verifierad</span>
</div>
<div class="zoomer-right">
<div class="zoomer-amount">12 600 SEK</div>
@@ -611,7 +611,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="zoomer-info">
<div class="zoomer-name">Mikael Thorsson</div>
<div class="zoomer-tin">TIN: 9103284512</div>
<span class="status-badge status-verified"> TIN verifierad</span>
<span class="status-badge status-verified"><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> TIN verifierad</span>
</div>
<div class="zoomer-right">
<div class="zoomer-amount">9 750 SEK</div>
@@ -625,7 +625,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="zoomer-info">
<div class="zoomer-name">Fatima Okonkwo</div>
<div class="zoomer-tin">TIN: EJ REGISTRERAT</div>
<span class="status-badge status-missing"> TIN saknas</span>
<span class="status-badge status-missing"><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> TIN saknas</span>
</div>
<div class="zoomer-right">
<div class="zoomer-amount">7 200 SEK</div>
@@ -653,7 +653,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="zoomer-info">
<div class="zoomer-name">Sofia Wennberg</div>
<div class="zoomer-tin">TIN: 0002157890</div>
<span class="status-badge status-verified"> TIN verifierad</span>
<span class="status-badge status-verified"><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> TIN verifierad</span>
</div>
<div class="zoomer-right">
<div class="zoomer-amount">2 400 SEK</div>
@@ -671,18 +671,18 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Exportera rapport</div>
<div class="export-section">
<button class="btn-primary" onclick="exportXML()">
<span>📤</span>
<span></span>
Exportera XML till Skatteverket
</button>
<button class="btn-secondary" onclick="downloadPreview()">
<span>👁️</span>
<span></span>
Förhandsgranska XML
</button>
</div>
<!-- Footer note -->
<div class="info-card" style="margin-bottom: 24px;">
<div class="info-card-title">⚠️ Viktigt att notera</div>
<div class="info-card-title"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Viktigt att notera</div>
<div class="info-card-text">
Zoomers med <strong>saknat TIN</strong> måste kontaktas och komplettera sina uppgifter <strong>innan export</strong>. Rapporten kan inte inkludera saknade skattenummer.
<br><br>
@@ -696,7 +696,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="toast" id="toast"></div>
<script>
// ── Days Until Deadline ──
// Days Until Deadline
(function() {
const deadline = new Date('2027-01-31T23:59:59');
const now = new Date();
@@ -705,7 +705,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (el) el.textContent = diff > 0 ? diff : '0';
})();
// ── Toast Helper ──
// Toast Helper
function showToast(msg, duration = 2500) {
const t = document.getElementById('toast');
t.textContent = msg;
@@ -713,17 +713,17 @@ function showToast(msg, duration = 2500) {
setTimeout(() => t.classList.remove('show'), duration);
}
// ── Zoomer Detail ──
// Zoomer Detail
function showZoomerDetail(name, tin, amount, status) {
const statusMap = {
verified: ' TIN verifierad',
missing: ' TIN saknas',
verified: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> TIN verifierad',
missing: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> TIN saknas',
pending: '⏳ Under utredning'
};
showToast(`${name} · ${parseInt(amount).toLocaleString('sv-SE')} SEK · ${statusMap[status]}`);
}
// ── Export XML ──
// Export XML
function exportXML() {
const btn = document.querySelector('.btn-primary');
const orig = btn.innerHTML;
@@ -741,13 +741,13 @@ function exportXML() {
URL.revokeObjectURL(url);
btn.innerHTML = orig;
btn.style.opacity = '';
showToast(' XML-fil exporterad!', 3000);
showToast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> XML-fil exporterad!', 3000);
}, 1200);
}
// ── Preview XML ──
// Preview XML
function downloadPreview() {
showToast('📄 Öppnar förhandsgranskning...');
showToast(' Öppnar förhandsgranskning...');
setTimeout(() => {
const xml = generateDAC7XML();
const win = window.open('', '_blank');
@@ -761,7 +761,7 @@ function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// ── DAC7 XML Generator ──
// DAC7 XML Generator
function generateDAC7XML() {
const now = new Date().toISOString();
const zoomers = [
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# Deploy quiXzoom Asia pages to S3
# Requires: AWS CLI configured with appropriate credentials
set -e
BUCKET="quixzoom-asia"
REGION="ap-southeast-1"
DISTRIBUTION_ID="E1234567890ABC" # Update with actual CloudFront distribution ID
echo "=== quiXzoom Asia Deployment ==="
echo "Bucket: $BUCKET"
echo "Region: $REGION"
echo ""
# Sync all language pages
echo "Uploading language pages..."
aws s3 sync . s3://$BUCKET/ \
--exclude "*.sh" \
--exclude "*.md" \
--exclude ".git/*" \
--cache-control "max-age=3600" \
--region $REGION
# Set content types for HTML files
echo "Setting content types..."
aws s3 cp s3://$BUCKET/ s3://$BUCKET/ \
--recursive \
--exclude "*" \
--include "*.html" \
--content-type "text/html; charset=utf-8" \
--metadata-directive REPLACE \
--region $REGION
# Set content type for SVG
echo "Setting SVG content types..."
aws s3 cp s3://$BUCKET/ s3://$BUCKET/ \
--recursive \
--exclude "*" \
--include "*.svg" \
--content-type "image/svg+xml" \
--metadata-directive REPLACE \
--region $REGION
# Invalidate CloudFront cache
echo "Invalidating CloudFront cache..."
aws cloudfront create-invalidation \
--distribution-id $DISTRIBUTION_ID \
--paths "/*" \
--region $REGION
echo ""
echo "=== Deployment Complete ==="
echo "https://quixzoom.asia/"
+151
View File
@@ -0,0 +1,151 @@
/* Landvex Design Tokens v1.0 */
/* Standardized spacing, colors, typography, and border-radius */
:root {
/* === COLORS === */
/* Primary Brand */
--color-primary: #0066FF;
--color-primary-dark: #0052CC;
--color-primary-light: #3385FF;
/* Secondary (quiXzoom accent) */
--color-accent: #FF3366;
--color-accent-dark: #E62E5C;
/* Neutral */
--color-black: #0A0A0A;
--color-dark: #1A1A1A;
--color-gray-900: #1D1D1F;
--color-gray-800: #2D2D2F;
--color-gray-700: #4A4A4A;
--color-gray-600: #6E6E73;
--color-gray-500: #8E8E93;
--color-gray-400: #A1A1A6;
--color-gray-300: #C7C7CC;
--color-gray-200: #D1D1D6;
--color-gray-100: #E5E5EA;
--color-gray-50: #F2F2F7;
--color-white: #FFFFFF;
/* Semantic */
--color-success: #22C55E;
--color-warning: #F59E0B;
--color-error: #EF4444;
--color-info: #3B82F6;
/* === SPACING === */
/* Base unit: 4px */
--space-0: 0;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
--space-20: 80px;
--space-24: 96px;
/* === BORDER RADIUS === */
--radius-none: 0;
--radius-sm: 6px; /* Buttons, tags, small elements */
--radius-md: 10px; /* Cards, inputs, modals */
--radius-lg: 16px; /* Large cards, hero sections */
--radius-xl: 24px; /* Feature cards, containers */
--radius-full: 9999px; /* Pills, avatars, badges */
/* === TYPOGRAPHY === */
/* Font families */
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', 'Helvetica Neue', sans-serif;
--font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
/* Font sizes (rem-based) */
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 1.875rem; /* 30px */
--text-4xl: 2.25rem; /* 36px */
--text-5xl: 3rem; /* 48px */
/* Font weights */
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
--font-extrabold: 800;
/* Line heights */
--leading-none: 1;
--leading-tight: 1.25;
--leading-snug: 1.375;
--leading-normal: 1.5;
--leading-relaxed: 1.625;
--leading-loose: 2;
/* === SHADOWS === */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
--shadow-glow: 0 0 20px rgba(0, 102, 255, 0.3);
/* === TRANSITIONS === */
--transition-fast: 150ms ease;
--transition-base: 200ms ease;
--transition-slow: 300ms ease;
/* === Z-INDEX === */
--z-dropdown: 100;
--z-sticky: 200;
--z-modal: 300;
--z-popover: 400;
--z-tooltip: 500;
}
/* === UTILITY CLASSES === */
/* Use these instead of inline styles */
/* Border radius utilities */
.rounded-none { border-radius: var(--radius-none); }
.rounded-sm { border-radius: var(--radius-sm); }
.rounded-md { border-radius: var(--radius-md); }
.rounded-lg { border-radius: var(--radius-lg); }
.rounded-xl { border-radius: var(--radius-xl); }
.rounded-full { border-radius: var(--radius-full); }
/* Spacing utilities (examples) */
.p-1 { padding: var(--space-1); }
.p-2 { padding: var(--space-2); }
.p-4 { padding: var(--space-4); }
.p-6 { padding: var(--space-6); }
.p-8 { padding: var(--space-8); }
.m-1 { margin: var(--space-1); }
.m-2 { margin: var(--space-2); }
.m-4 { margin: var(--space-4); }
/* Text utilities */
.text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); }
.text-base { font-size: var(--text-base); }
.text-lg { font-size: var(--text-lg); }
.text-xl { font-size: var(--text-xl); }
.font-medium { font-weight: var(--font-medium); }
.font-semibold { font-weight: var(--font-semibold); }
.font-bold { font-weight: var(--font-bold); }
/* Color utilities */
.text-primary { color: var(--color-primary); }
.text-secondary { color: var(--color-gray-600); }
.text-muted { color: var(--color-gray-500); }
.bg-primary { background-color: var(--color-primary); }
.bg-dark { background-color: var(--color-dark); }
.bg-light { background-color: var(--color-gray-50); }
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0; url=https://www.quixzoom.com/#earnings">
<title>Earn as a Zoomer — quiXzoom</title>
<link rel="canonical" href="https://www.quixzoom.com/">
<link rel="canonical" href="https://www.quixzoom.com/earn/">
<meta name="robots" content="noindex">
<meta property="og:type" content="website">
<meta property="og:title" content="Earn as a Zoomer — quiXzoom">
+19 -4
View File
@@ -1,4 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512" font-family="Poppins,'DejaVu Sans',sans-serif">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#8E6CFF"/><stop offset="1" stop-color="#6235D9"/></linearGradient></defs>
<rect width="512" height="512" rx="114" fill="url(#g)"/>
<text x="256" y="364" text-anchor="middle" font-weight="800" font-size="300" fill="#F7F3EE">X</text></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="qz-bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0A0A0A"/>
<stop offset="100%" stop-color="#1A1A1A"/>
</linearGradient>
<linearGradient id="qz-accent" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0066FF"/>
<stop offset="100%" stop-color="#0052CC"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="512" height="512" rx="128" fill="url(#qz-bg)"/>
<!-- Camera lens shape -->
<circle cx="256" cy="256" r="160" fill="none" stroke="url(#qz-accent)" stroke-width="32"/>
<circle cx="256" cy="256" r="80" fill="url(#qz-accent)"/>
<!-- QuiXzoom dot -->
<circle cx="360" cy="152" r="24" fill="#FF3366"/>
</svg>

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 807 B

+13 -13
View File
@@ -59,7 +59,7 @@
.feature-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 20px;
border-radius: var(--radius-xl);
padding: 40px;
transition: transform 0.2s, box-shadow 0.2s;
}
@@ -70,7 +70,7 @@
.feature-icon {
width: 48px; height: 48px;
background: var(--blue-glow);
border-radius: 12px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 1.5rem; margin-bottom: 20px;
}
@@ -98,7 +98,7 @@
.feature-visual {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 20px;
border-radius: var(--radius-xl);
padding: 40px;
min-height: 300px;
display: flex; align-items: center; justify-content: center;
@@ -119,7 +119,7 @@
.btn {
display: inline-block; padding: 16px 32px;
background: var(--blue); color: #fff;
border: none; border-radius: 12px;
border: none; border-radius: var(--radius-md);
font-size: 1rem; font-weight: 600;
text-decoration: none;
cursor: pointer; transition: background 0.2s;
@@ -180,32 +180,32 @@
</div>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-icon">🤖</div>
<div class="feature-icon"></div>
<h3>AI Quality Review</h3>
<p>Every submission is automatically checked for completeness, image quality, GPS accuracy, and compliance with mission requirements before payment.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🌍</div>
<div class="feature-icon"></div>
<h3>Global Coverage</h3>
<p>Launching in 5 markets in phases — Sweden first (August 2026), with Netherlands, Thailand, Nigeria and Kenya to follow. Localized mission types, languages, and payment methods for each market.</p>
</div>
<div class="feature-card">
<div class="feature-icon"></div>
<div class="feature-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg></div>
<h3>Real-time Tracking</h3>
<p>Monitor mission progress in real-time. See which missions are active, completed, or need attention — all in one dashboard.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔒</div>
<div class="feature-icon"></div>
<h3>Enterprise Security</h3>
<p>GDPR compliant, EU data residency, SOC 2 Type II certified. Your data is encrypted at rest and in transit.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📊</div>
<div class="feature-icon"></div>
<h3>Advanced Analytics</h3>
<p>Turn raw observations into actionable insights. Trend analysis, change detection, and automated reporting.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔌</div>
<div class="feature-icon"></div>
<h3>API & Integrations</h3>
<p>RESTful API with comprehensive documentation. Webhooks for real-time updates. Integrate with your existing tools and workflows.</p>
</div>
@@ -228,7 +228,7 @@
</div>
<div class="feature-visual">
<div class="feature-visual-placeholder">
<div style="font-size:3rem;margin-bottom:12px;">🗺️</div>
<div style="font-size:3rem;margin-bottom:12px;"></div>
<div>Mission Builder Interface</div>
</div>
</div>
@@ -247,7 +247,7 @@
</div>
<div class="feature-visual">
<div class="feature-visual-placeholder">
<div style="font-size:3rem;margin-bottom:12px;">👥</div>
<div style="font-size:3rem;margin-bottom:12px;"></div>
<div>Zoomer Network</div>
</div>
</div>
@@ -266,7 +266,7 @@
</div>
<div class="feature-visual">
<div class="feature-visual-placeholder">
<div style="font-size:3rem;margin-bottom:12px;">💳</div>
<div style="font-size:3rem;margin-bottom:12px;"></div>
<div>Payment Dashboard</div>
</div>
</div>
@@ -40,13 +40,13 @@
.nav-links{display:flex;align-items:center;gap:32px}
.nav-links a{color:var(--text-muted);font-size:var(--text-base);font-weight:500;transition:color .15s}
.nav-links a:hover{color:var(--text-dark)}
.btn-nav{background:var(--blue);color:#fff;border:none;border-radius:var(--radius-lg);padding:10px 24px;font-size:var(--text-base);font-weight:700;transition:background .15s;white-space:nowrap}
.btn-nav{background:var(--blue);color:#fff;border:none;border-radius: var(--radius-lg);padding:10px 24px;font-size:var(--text-base);font-weight:700;transition:background .15s;white-space:nowrap}
.btn-nav:hover{background:var(--blue-dark)}
.mobile-menu{display:none}
@media(max-width:768px){
.nav-links{display:none}
.hamburger{display:flex;flex-direction:column;gap:5px;cursor:pointer;padding:8px;background:none;border:none}
.hamburger span{display:block;width:22px;height:2px;background:var(--text-dark);border-radius:2px}
.hamburger span{display:block;width:22px;height:2px;background:var(--text-dark);border-radius: var(--radius-sm)}
.mobile-menu{display:none;position:fixed;inset:0;top:68px;background:var(--bg-white);z-index:299;padding:24px;flex-direction:column;gap:0;border-top:1px solid rgba(0,0,0,.08)}
.mobile-menu.open{display:flex}
.mobile-menu a{color:var(--text-body);font-size:1.1rem;font-weight:500;padding:18px 0;border-bottom:1px solid rgba(0,0,0,.08);display:block}
@@ -57,16 +57,16 @@
.hero-org-inner{max-width:720px}
.hero-org h1{color:var(--text-dark);margin-bottom:24px}
.hero-org p{font-size:clamp(1.0625rem,2vw,1.25rem);color:var(--text-body);line-height:1.6;margin-bottom:32px}
.hero-badge{display:inline-flex;align-items:center;gap:8px;background:var(--blue-pale);border:1px solid rgba(0,102,255,.15);border-radius:100px;padding:8px 18px;font-size:var(--text-sm);font-weight:600;color:var(--blue);margin-bottom:28px}
.hero-badge{display:inline-flex;align-items:center;gap:8px;background:var(--blue-pale);border:1px solid rgba(0,102,255,.15);border-radius: 100px;padding:8px 18px;font-size:var(--text-sm);font-weight:600;color:var(--blue);margin-bottom:28px}
.hero-cta{display:flex;gap:12px;flex-wrap:wrap}
.btn-primary{background:var(--blue);color:#fff;border:none;border-radius:var(--radius-lg);padding:14px 32px;font-size:1rem;font-weight:700;transition:background .15s}
.btn-primary{background:var(--blue);color:#fff;border:none;border-radius: var(--radius-lg);padding:14px 32px;font-size:1rem;font-weight:700;transition:background .15s}
.btn-primary:hover{background:var(--blue-dark)}
.btn-ghost{background:transparent;border:2px solid rgba(0,0,0,.15);color:var(--text-dark);border-radius:var(--radius-lg);padding:14px 32px;font-size:1rem;font-weight:700;transition:border-color .15s,background .15s}
.btn-ghost{background:transparent;border:2px solid rgba(0,0,0,.15);color:var(--text-dark);border-radius: var(--radius-lg);padding:14px 32px;font-size:1rem;font-weight:700;transition:border-color .15s,background .15s}
.btn-ghost:hover{border-color:var(--blue);background:var(--blue-pale)}
/* USE CASES */
.usecase-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
.usecase-card{background:var(--bg-off);border-radius:var(--radius-card);padding:32px 28px;transition:transform var(--transition),box-shadow var(--transition)}
.usecase-card{background:var(--bg-off);border-radius: var(--radius-card);padding:32px 28px;transition:transform var(--transition),box-shadow var(--transition)}
.usecase-card:hover{transform:translateY(-4px);box-shadow:var(--shadow-hover)}
.usecase-icon{font-size:2rem;margin-bottom:16px}
.usecase-card h3{font-size:1.125rem;font-weight:800;color:var(--text-dark);margin-bottom:8px}
@@ -78,12 +78,12 @@
.feature-row{display:grid;grid-template-columns:1fr 1fr;gap:48px;align-items:center;margin-bottom:64px}
.feature-row.reverse{direction:rtl}
.feature-row.reverse>*{direction:ltr}
.feature-visual{background:var(--bg-off);border-radius:var(--radius-card);padding:40px;text-align:center}
.feature-visual{background:var(--bg-off);border-radius: var(--radius-card);padding:40px;text-align:center}
.feature-visual .big-icon{font-size:4rem}
.feature-text h3{font-size:var(--text-2xl);color:var(--text-dark);margin-bottom:16px}
.feature-text p{color:var(--text-body);margin-bottom:16px}
.feature-list li{display:flex;align-items:flex-start;gap:10px;font-size:var(--text-base);padding:6px 0}
.feature-list li::before{content:'';color:var(--green);font-weight:700}
.feature-list li::before{content:'<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>';color:var(--green);font-weight:700}
@media(max-width:768px){.feature-row{grid-template-columns:1fr;gap:32px}}
/* STATS */
@@ -97,14 +97,14 @@
/* PRICING */
.pricing-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
.pricing-card{background:var(--bg-off);border:2px solid #E8E8E8;border-radius:var(--radius-card);padding:36px 28px;text-align:center}
.pricing-card{background:var(--bg-off);border:2px solid #E8E8E8;border-radius: var(--radius-card);padding:36px 28px;text-align:center}
.pricing-card.featured{border-color:var(--blue);transform:scale(1.02)}
.pricing-card h3{font-size:1.5rem;font-weight:900;margin-bottom:4px;color:var(--text-dark)}
.pricing-price{font-size:2rem;font-weight:900;color:var(--blue);margin:16px 0}
.pricing-price span{font-size:var(--text-sm);color:var(--text-muted);font-weight:500}
.pricing-features{text-align:left;margin:24px 0}
.pricing-features li{display:flex;align-items:flex-start;gap:10px;font-size:var(--text-base);padding:6px 0}
.pricing-features li::before{content:'';color:var(--green);font-weight:700}
.pricing-features li::before{content:'<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>';color:var(--green);font-weight:700}
@media(max-width:900px){.pricing-grid{grid-template-columns:1fr;max-width:480px;margin:0 auto}}
/* CTA */
@@ -155,7 +155,7 @@
<section class="hero-org">
<div class="container">
<div class="hero-org-inner">
<div class="hero-badge">🏢 Enterprise Field Intelligence</div>
<div class="hero-badge"><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 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Enterprise Field Intelligence</div>
<h1>Verified reality,<br>on demand.</h1>
<p>Deploy photo missions at scale. Infrastructure inspection, property documentation, retail audits, environmental surveys — captured by verified Zoomers, reviewed by AI, delivered in hours.</p>
<div class="hero-cta">
@@ -185,32 +185,32 @@
</div>
<div class="usecase-grid">
<div class="usecase-card">
<div class="usecase-icon">🏗️</div>
<div class="usecase-icon"><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>Infrastructure Inspection</h3>
<p>Bridges, roads, facades, and utilities. Document condition without sending an inspector. Get geotagged, timestamped imagery within hours.</p>
</div>
<div class="usecase-card">
<div class="usecase-icon">🏠</div>
<div class="usecase-icon"></div>
<h3>Property Documentation</h3>
<p>Pre-purchase inspections, rental condition reports, insurance claims. Standardised photo sets from any address.</p>
</div>
<div class="usecase-card">
<div class="usecase-icon">🛒</div>
<div class="usecase-icon"></div>
<h3>Retail & OOH Audit</h3>
<p>Verify signage placement, shelf compliance, promotional displays. Competitive intelligence from real stores, not syndicated data.</p>
</div>
<div class="usecase-card">
<div class="usecase-icon">🌿</div>
<div class="usecase-icon"></div>
<h3>Environmental Survey</h3>
<p>Track vegetation, erosion, flood damage, coastal change. Repeatable observation points with GPS accuracy.</p>
</div>
<div class="usecase-card">
<div class="usecase-icon">🚗</div>
<div class="usecase-icon"></div>
<h3>Road Condition</h3>
<p>Potholes, surface degradation, signage visibility. Municipalities prioritise repairs with verified visual evidence.</p>
</div>
<div class="usecase-card">
<div class="usecase-icon"></div>
<div class="usecase-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg></div>
<h3>Event & Emergency</h3>
<p>Storm damage, accident documentation, construction progress. Rapid deployment when speed matters.</p>
</div>
@@ -225,7 +225,7 @@
<p>From mission definition to verified data delivery.</p>
</div>
<div class="feature-row">
<div class="feature-visual"><div class="big-icon">📝</div></div>
<div class="feature-visual"><div class="big-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 13V10L10 3L13 6L6 13H3Z" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 4L12 7" stroke="#666" stroke-width="1.5"/></svg></div></div>
<div class="feature-text">
<h3>1. Define your mission</h3>
<p>Set location, requirements, and compensation. Use templates or build custom briefs. Define exactly what angles, coverage, and metadata you need.</p>
@@ -238,7 +238,7 @@
</div>
</div>
<div class="feature-row reverse">
<div class="feature-visual"><div class="big-icon">📸</div></div>
<div class="feature-visual"><div class="big-icon"></div></div>
<div class="feature-text">
<h3>2. Zoomers capture</h3>
<p>Verified Zoomers in the area claim and complete missions. Every photo carries GPS, timestamp, and integrity hash at capture.</p>
@@ -251,7 +251,7 @@
</div>
</div>
<div class="feature-row">
<div class="feature-visual"><div class="big-icon"></div></div>
<div class="feature-visual"><div class="big-icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div></div>
<div class="feature-text">
<h3>3. AI review & delivery</h3>
<p>Submissions are reviewed for completeness, quality, and specification compliance within minutes. Approved data is delivered to your dashboard or API.</p>
@@ -344,7 +344,6 @@
<ul>
<li><a href="/academy/">Zoomer Academy</a></li>
<li><a href="/glossary">Glossary</a></li>
<li><a href="/developer">API docs</a></li>
<li><a href="/status">System status</a></li>
</ul>
</div>
+38 -39
View File
@@ -55,7 +55,7 @@ body {
-webkit-font-smoothing: antialiased;
}
/* ─── NAV ─────────────────────────────────────────────────── */
/* NAV */
.nav {
position: sticky;
top: 0;
@@ -112,7 +112,7 @@ body {
.nav-search input {
width: 100%;
padding: 8px 14px 8px 36px;
border-radius: 20px;
border-radius: var(--radius-xl);
border: 1px solid var(--border);
background: var(--bg-off);
color: var(--text);
@@ -154,7 +154,7 @@ body {
font-weight: 600;
color: var(--text-secondary);
text-decoration: none;
border-radius: 5px;
border-radius: var(--radius-sm);
transition: background .12s, color .12s;
line-height: 1.4;
}
@@ -162,7 +162,7 @@ body {
.nav-az a.active { background: var(--blue); color: #fff; }
.nav-az a.empty { opacity: .25; pointer-events: none; }
/* ─── MAIN ────────────────────────────────────────────────── */
/* MAIN */
main {
max-width: 860px;
margin: 0 auto;
@@ -199,7 +199,7 @@ main {
}
.search-empty.visible { display: block; }
/* ─── LETTER SECTION ──────────────────────────────────────── */
/* LETTER SECTION */
.letter-section {
margin-bottom: 48px;
}
@@ -228,7 +228,7 @@ main {
color: var(--text-secondary);
}
/* ─── TERM CARD ───────────────────────────────────────────── */
/* TERM CARD */
.term-card {
background: var(--bg-card);
border: 1px solid var(--border);
@@ -263,7 +263,7 @@ main {
.term-category {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
border-radius: var(--radius-xl);
font-size: .75rem;
font-weight: 600;
letter-spacing: .02em;
@@ -331,7 +331,7 @@ main {
.term-related a {
display: inline-block;
padding: 3px 10px;
border-radius: 20px;
border-radius: var(--radius-xl);
font-size: .8125rem;
font-weight: 500;
color: var(--blue);
@@ -345,7 +345,7 @@ main {
color: #fff;
}
/* ─── RESPONSIVE ──────────────────────────────────────────── */
/* RESPONSIVE */
@media (max-width: 600px) {
.nav { gap: 10px; padding: 0 16px; }
.nav-az { display: none; }
@@ -358,13 +358,13 @@ main {
.nav-az { max-width: 200px; }
}
/* ─── HIGHLIGHT when anchor-targeted ─────────────────────── */
/* HIGHLIGHT when anchor-targeted */
.term-card:target {
border-color: var(--blue);
box-shadow: 0 0 0 3px rgba(0,102,255,.15);
}
/* ─── HIDDEN during search ────────────────────────────────── */
/* HIDDEN during search */
.term-card.hidden { display: none; }
.letter-section.hidden { display: none; }
</style>
@@ -423,7 +423,7 @@ main {
<div class="search-empty" id="search-empty">No terms match "<span id="search-query"></span>"</div>
<!-- ═══════════════════════════════ A ═══════════════════════════════ -->
<!-- A -->
<section class="letter-section" id="a">
<div class="letter-heading"><h2>A</h2><span class="letter-count"></span></div>
@@ -627,7 +627,7 @@ main {
</section>
<!-- ═══════════════════════════════ C ═══════════════════════════════ -->
<!-- C -->
<section class="letter-section" id="c">
<div class="letter-heading"><h2>C</h2><span class="letter-count"></span></div>
@@ -752,7 +752,7 @@ main {
</section>
<!-- ═══════════════════════════════ D ═══════════════════════════════ -->
<!-- D -->
<section class="letter-section" id="d">
<div class="letter-heading"><h2>D</h2><span class="letter-count"></span></div>
@@ -876,7 +876,7 @@ main {
</section>
<!-- ═══════════════════════════════ E ═══════════════════════════════ -->
<!-- E -->
<section class="letter-section" id="e">
<div class="letter-heading"><h2>E</h2><span class="letter-count"></span></div>
@@ -902,7 +902,7 @@ main {
</section>
<!-- ═══════════════════════════════ F ═══════════════════════════════ -->
<!-- F -->
<section class="letter-section" id="f">
<div class="letter-heading"><h2>F</h2><span class="letter-count"></span></div>
@@ -928,7 +928,7 @@ main {
</section>
<!-- ═══════════════════════════════ G ═══════════════════════════════ -->
<!-- G -->
<section class="letter-section" id="g">
<div class="letter-heading"><h2>G</h2><span class="letter-count"></span></div>
@@ -1053,7 +1053,7 @@ main {
</section>
<!-- ═══════════════════════════════ H ═══════════════════════════════ -->
<!-- H -->
<section class="letter-section" id="h">
<div class="letter-heading"><h2>H</h2><span class="letter-count"></span></div>
@@ -1078,7 +1078,7 @@ main {
</section>
<!-- ═══════════════════════════════ I ═══════════════════════════════ -->
<!-- I -->
<section class="letter-section" id="i">
<div class="letter-heading"><h2>I</h2><span class="letter-count"></span></div>
@@ -1144,7 +1144,7 @@ main {
</section>
<!-- ═══════════════════════════════ J ═══════════════════════════════ -->
<!-- J -->
<section class="letter-section" id="j">
<div class="letter-heading"><h2>J</h2><span class="letter-count"></span></div>
@@ -1170,7 +1170,7 @@ main {
</section>
<!-- ═══════════════════════════════ K ═══════════════════════════════ -->
<!-- K -->
<section class="letter-section" id="k">
<div class="letter-heading"><h2>K</h2><span class="letter-count"></span></div>
@@ -1196,7 +1196,7 @@ main {
</section>
<!-- ═══════════════════════════════ L ═══════════════════════════════ -->
<!-- L -->
<section class="letter-section" id="l">
<div class="letter-heading"><h2>L</h2><span class="letter-count"></span></div>
@@ -1221,7 +1221,7 @@ main {
</section>
<!-- ═══════════════════════════════ M ═══════════════════════════════ -->
<!-- M -->
<section class="letter-section" id="m">
<div class="letter-heading"><h2>M</h2><span class="letter-count"></span></div>
@@ -1306,7 +1306,7 @@ main {
</section>
<!-- ═══════════════════════════════ O ═══════════════════════════════ -->
<!-- O -->
<section class="letter-section" id="o">
<div class="letter-heading"><h2>O</h2><span class="letter-count"></span></div>
@@ -1411,7 +1411,7 @@ main {
</section>
<!-- ═══════════════════════════════ P ═══════════════════════════════ -->
<!-- P -->
<section class="letter-section" id="p">
<div class="letter-heading"><h2>P</h2><span class="letter-count"></span></div>
@@ -1517,7 +1517,7 @@ main {
</section>
<!-- ═══════════════════════════════ Q ═══════════════════════════════ -->
<!-- Q -->
<section class="letter-section" id="q">
<div class="letter-heading"><h2>Q</h2><span class="letter-count"></span></div>
@@ -1543,7 +1543,7 @@ main {
</section>
<!-- ═══════════════════════════════ R ═══════════════════════════════ -->
<!-- R -->
<section class="letter-section" id="r">
<div class="letter-heading"><h2>R</h2><span class="letter-count"></span></div>
@@ -1646,7 +1646,7 @@ main {
</section>
<!-- ═══════════════════════════════ S ═══════════════════════════════ -->
<!-- S -->
<section class="letter-section" id="s">
<div class="letter-heading"><h2>S</h2><span class="letter-count"></span></div>
@@ -1850,7 +1850,7 @@ main {
</section>
<!-- ═══════════════════════════════ T ═══════════════════════════════ -->
<!-- T -->
<section class="letter-section" id="t">
<div class="letter-heading"><h2>T</h2><span class="letter-count"></span></div>
@@ -1916,7 +1916,7 @@ main {
</section>
<!-- ═══════════════════════════════ V ═══════════════════════════════ -->
<!-- V -->
<section class="letter-section" id="v">
<div class="letter-heading"><h2>V</h2><span class="letter-count"></span></div>
@@ -1956,13 +1956,13 @@ main {
<a href="#payout">Payout</a>
<a href="#stripe-connect">Stripe Connect</a>
<a href="#w9-w8ben">W-9 / W-8BEN</a>
<a href="#1099-k">1099-K</a>
<a href="#1099AAk">1099-K</a>
</div>
</article>
</section>
<!-- ═══════════════════════════════ W ═══════════════════════════════ -->
<!-- W -->
<section class="letter-section" id="w">
<div class="letter-heading"><h2>W</h2><span class="letter-count"></span></div>
@@ -2018,7 +2018,7 @@ main {
</div>
<div class="term-related">
<span>Related:</span>
<a href="#1099-k">1099-K</a>
<a href="#1099AAk">1099-K</a>
<a href="#stripe-connect">Stripe Connect</a>
<a href="#vat">VAT</a>
<a href="#kyc">KYC</a>
@@ -2027,7 +2027,7 @@ main {
</section>
<!-- ═══════════════════════════════ Z ═══════════════════════════════ -->
<!-- Z -->
<section class="letter-section" id="z">
<div class="letter-heading"><h2>Z</h2><span class="letter-count"></span></div>
@@ -2073,19 +2073,19 @@ main {
</section>
<!-- ═══════════ ACTIVE TIER (alphabetical under A but listed after for content flow) ══════════ -->
<!-- ACTIVE TIER (alphabetical under A but listed after for content flow) -->
</main>
<script>
// ─── Add letter counts ────────────────────────────────────────────────
// Add letter counts
document.querySelectorAll('.letter-section').forEach(section => {
const cards = section.querySelectorAll('.term-card');
const countEl = section.querySelector('.letter-count');
if (countEl) countEl.textContent = cards.length + ' term' + (cards.length !== 1 ? 's' : '');
});
// ─── Live search ──────────────────────────────────────────────────────
// Live search
const searchInput = document.getElementById('glossary-search');
const emptyMsg = document.getElementById('search-empty');
const querySpan = document.getElementById('search-query');
@@ -2118,7 +2118,7 @@ searchInput.addEventListener('input', () => {
emptyMsg.classList.toggle('visible', !anyVisible);
});
// ─── AZ active letter tracking ──────────────────────────────────────
// AZ active letter tracking
const azLinks = document.querySelectorAll('.nav-az a');
const sections = Array.from(document.querySelectorAll('.letter-section[id]'));
@@ -2223,7 +2223,6 @@ sections.forEach(s => observer.observe(s));
<nav style="display:flex;gap:20px;flex-wrap:wrap">
<a href="/" style="font-size:.8125rem;color:rgba(255,255,255,.75);transition:color .15s;text-decoration:none">For Zoomers</a>
<a href="/for-organisations/" style="font-size:.8125rem;color:rgba(255,255,255,.75);transition:color .15s;text-decoration:none">For Organisations</a>
<a href="/developer" style="font-size:.8125rem;color:rgba(255,255,255,.75);transition:color .15s;text-decoration:none">Developer</a>
<a href="/pricing" style="font-size:.8125rem;color:rgba(255,255,255,.75);transition:color .15s;text-decoration:none">Pricing</a>
<a href="/about" style="font-size:.8125rem;color:rgba(255,255,255,.75);transition:color .15s;text-decoration:none">About</a>
<a href="/status" style="font-size:.8125rem;color:rgba(255,255,255,.75);transition:color .15s;text-decoration:none">Status</a>
+149
View File
@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="hi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>quiXzoom — दुनिया को कैप्चर करें। पैसे कमाएं। | Zoomer बनें</title>
<meta name="description" content="quiXzoom आपको इंफ्रास्ट्रक्चर, प्रकृति और शहरी वातावरण की तस्वीरें लेने के लिए पैसे देता है। कोई प्लेटफॉर्म शुल्क नहीं। अगस्त 2026 में लॉन्च।">
<link rel="canonical" href="https://quixzoom.asia/hi/">
<link rel="alternate" hreflang="hi" href="https://quixzoom.asia/hi/">
<link rel="alternate" hreflang="zh-CN" href="https://quixzoom.asia/zh-cn/">
<link rel="alternate" hreflang="ja" href="https://quixzoom.asia/ja/">
<link rel="alternate" hreflang="en" href="https://www.quixzoom.com/">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Noto Sans Devanagari', sans-serif;
background: #0a0a0a;
color: #ffffff;
line-height: 1.6;
}
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
header {
padding: 24px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 24px; font-weight: 700; color: #0066ff; }
.lang-switcher { display: flex; gap: 16px; }
.lang-switcher a {
color: rgba(255,255,255,0.6);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.lang-switcher a:hover, .lang-switcher a.active { color: #fff; }
.hero {
padding: 120px 0 80px;
text-align: center;
}
.hero h1 {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 800;
line-height: 1.2;
margin-bottom: 24px;
}
.hero p {
font-size: 1.25rem;
color: rgba(255,255,255,0.7);
max-width: 600px;
margin: 0 auto 40px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 16px 32px;
background: #0066ff;
color: #fff;
text-decoration: none;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0,102,255,0.3);
}
.features {
padding: 80px 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 32px;
}
.feature {
background: rgba(255,255,255,0.05);
padding: 32px;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.1);
}
.feature h3 {
font-size: 1.25rem;
margin-bottom: 12px;
color: #fff;
}
.feature p {
color: rgba(255,255,255,0.6);
font-size: 0.9375rem;
}
.currency-note {
text-align: center;
padding: 40px 0;
color: rgba(255,255,255,0.5);
font-size: 0.875rem;
}
footer {
padding: 40px 0;
text-align: center;
color: rgba(255,255,255,0.4);
font-size: 0.875rem;
border-top: 1px solid rgba(255,255,255,0.1);
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">quiXzoom</div>
<nav class="lang-switcher">
<a href="/hi/" class="active">हिन्दी</a>
<a href="/zh-cn/"></a>
<a href="/ja/"></a>
<a href="/en/">EN</a>
</nav>
</header>
<section class="hero">
<h1>दुनिया को कैप्चर करें।<br>पैसे कमाएं।</h1>
<p>quiXzoom आपको इंफ्रास्ट्रक्चर, प्राकृतिक वातावरण और शहरी दृश्यों की तस्वीरें लेने के लिए पैसे देता है। कोई प्लेटफॉर्म शुल्क नहीं। स्वीकृत मिशन के लिए सीधा भुगतान।</p>
<a href="https://www.quixzoom.com/register" class="btn">Zoomer के रूप में पंजीकरण करें</a>
</section>
<section class="features">
<div class="feature">
<h3>तस्वीरें लें और कमाएं</h3>
<p>आस-पास के मिशन स्वीकार करें और निर्देशों के अनुसार तस्वीरें लें। स्वीकृत मिशन के लिए ₹500-5,000 कमाएं।</p>
</div>
<div class="feature">
<h3>त्वरित भुगतान</h3>
<p>स्वीकृति के 24 घंटे के भीतर सीधे बैंक खाते या Paytm में भुगतान।</p>
</div>
<div class="feature">
<h3>लचीला और स्वतंत्र</h3>
<p>कभी भी, कहीं भी काम करें। कोई न्यूनतम आवश्यकता नहीं, कोई शेड्यूल नहीं। आप तय करें।</p>
</div>
</section>
<div class="currency-note">
कीमतें भारतीय रुपये (INR) में दिखाई गई हैं। भुगतान के समय वर्तमान विनिमय दर पर परिवर्तित की जाएंगी।
</div>
<footer>
<p>quiXzoom Asia | सहायता: asia@quixzoom.com</p>
</footer>
</div>
</body>
</html>
+149
View File
@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>quiXzoom — Abadikan Dunia. Dapatkan Bayaran. | Jadi Zoomer</title>
<meta name="description" content="quiXzoom membayar Anda untuk memotret infrastruktur, alam, dan lingkungan perkotaan. Tanpa biaya platform. Diluncurkan Agustus 2026.">
<link rel="canonical" href="https://quixzoom.asia/id/">
<link rel="alternate" hreflang="id" href="https://quixzoom.asia/id/">
<link rel="alternate" hreflang="zh-CN" href="https://quixzoom.asia/zh-cn/">
<link rel="alternate" hreflang="ja" href="https://quixzoom.asia/ja/">
<link rel="alternate" hreflang="en" href="https://www.quixzoom.com/">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: #0a0a0a;
color: #ffffff;
line-height: 1.6;
}
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
header {
padding: 24px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 24px; font-weight: 700; color: #0066ff; }
.lang-switcher { display: flex; gap: 16px; }
.lang-switcher a {
color: rgba(255,255,255,0.6);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.lang-switcher a:hover, .lang-switcher a.active { color: #fff; }
.hero {
padding: 120px 0 80px;
text-align: center;
}
.hero h1 {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 800;
line-height: 1.2;
margin-bottom: 24px;
}
.hero p {
font-size: 1.25rem;
color: rgba(255,255,255,0.7);
max-width: 600px;
margin: 0 auto 40px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 16px 32px;
background: #0066ff;
color: #fff;
text-decoration: none;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0,102,255,0.3);
}
.features {
padding: 80px 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 32px;
}
.feature {
background: rgba(255,255,255,0.05);
padding: 32px;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.1);
}
.feature h3 {
font-size: 1.25rem;
margin-bottom: 12px;
color: #fff;
}
.feature p {
color: rgba(255,255,255,0.6);
font-size: 0.9375rem;
}
.currency-note {
text-align: center;
padding: 40px 0;
color: rgba(255,255,255,0.5);
font-size: 0.875rem;
}
footer {
padding: 40px 0;
text-align: center;
color: rgba(255,255,255,0.4);
font-size: 0.875rem;
border-top: 1px solid rgba(255,255,255,0.1);
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">quiXzoom</div>
<nav class="lang-switcher">
<a href="/id/" class="active">Bahasa Indonesia</a>
<a href="/zh-cn/"></a>
<a href="/ja/"></a>
<a href="/en/">EN</a>
</nav>
</header>
<section class="hero">
<h1>Abadikan Dunia.<br>Dapatkan Bayaran.</h1>
<p>quiXzoom membayar Anda untuk memotret infrastruktur, lingkungan alam, dan pemandangan kota. Tanpa biaya platform. Pembayaran langsung untuk setiap misi yang disetujui.</p>
<a href="https://www.quixzoom.com/register" class="btn">Daftar sebagai Zoomer</a>
</section>
<section class="features">
<div class="feature">
<h3>Memotret dan Menghasilkan</h3>
<p>Terima misi di dekat Anda dan potret sesuai petunjuk. Dapatkan Rp100.000-1.000.000 untuk setiap misi yang disetujui.</p>
</div>
<div class="feature">
<h3>Pembayaran Cepat</h3>
<p>Pembayaran langsung ke rekening bank atau GoPay dalam 24 jam setelah disetujui.</p>
</div>
<div class="feature">
<h3>Fleksibel dan Bebas</h3>
<p>Bekerja kapan saja, di mana saja. Tidak ada persyaratan minimum, tidak ada jadwal. Anda yang memutuskan.</p>
</div>
</section>
<div class="currency-note">
Harga ditampilkan dalam Rupiah Indonesia (IDR). Akan dikonversi sesuai kurs saat ini saat pembayaran.
</div>
<footer>
<p>quiXzoom Asia | Dukungan: asia@quixzoom.com</p>
</footer>
</div>
</body>
</html>
+5 -5
View File
@@ -129,7 +129,7 @@
.logo span { color: #0071e3; }
.nav-cta {
background: #0071e3; color: #fff;
padding: 8px 18px; border-radius: 20px;
padding: 8px 18px; border-radius: var(--radius-xl);
font-size: 14px; font-weight: 600;
text-decoration: none;
}
@@ -166,7 +166,7 @@
gap: 24px;
}
.card {
border-radius: 16px;
border-radius: var(--radius-lg);
overflow: hidden;
background: #fff;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
@@ -201,7 +201,7 @@
font-size: 11px;
font-weight: 600;
padding: 3px 10px;
border-radius: 20px;
border-radius: var(--radius-xl);
margin-top: 10px;
}
@@ -213,7 +213,7 @@
}
.featured img {
width: 100%;
border-radius: 20px;
border-radius: var(--radius-xl);
box-shadow: 0 4px 24px rgba(0,0,0,0.10);
}
@@ -239,7 +239,7 @@
}
.alt-names span {
background: #fff; border: 1px solid #d2d2d7;
border-radius: 20px; padding: 5px 14px;
border-radius: var(--radius-xl); padding: 5px 14px;
font-size: 13px; color: #3a3a3c;
}
+176 -1956
View File
@@ -3,1975 +3,195 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light">
<title>quiXzoom — Earn Money Taking Photos | Flexible Work</title>
<title>quiXzoom Asia — Capture the World. Get Paid.</title>
<meta name="description" content="quiXzoom pays you to photograph infrastructure, nature and urban environments. Available in 8 Asian languages. No platform fee.">
<link rel="canonical" href="https://quixzoom.asia/">
<link rel="alternate" hreflang="x-default" href="https://quixzoom.asia/">
<link rel="alternate" hreflang="zh-CN" href="https://quixzoom.asia/zh-cn/">
<link rel="alternate" hreflang="zh-TW" href="https://quixzoom.asia/zh-tw/">
<link rel="alternate" hreflang="ja" href="https://quixzoom.asia/ja/">
<link rel="alternate" hreflang="ko" href="https://quixzoom.asia/ko/">
<link rel="alternate" hreflang="th" href="https://quixzoom.asia/th/">
<link rel="alternate" hreflang="vi" href="https://quixzoom.asia/vi/">
<link rel="alternate" hreflang="id" href="https://quixzoom.asia/id/">
<link rel="alternate" hreflang="ms" href="https://quixzoom.asia/ms/">
<link rel="alternate" hreflang="hi" href="https://quixzoom.asia/hi/">
<link rel="alternate" hreflang="en" href="https://www.quixzoom.com/">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="192x192" href="/apple-touch-icon.png">
<meta name="description" content="quiXzoom rewards you per approved mission with QX Credits — redeemable as cash. No platform fee. No commission. Launching August 2026.">
<link rel="canonical" href="https://www.quixzoom.com/">
<!-- hreflang — canonical paths -->
<link rel="alternate" hreflang="x-default" href="https://www.quixzoom.com/" />
<link rel="alternate" hreflang="en" href="https://www.quixzoom.com/" />
<link rel="alternate" hreflang="sv" href="https://www.quixzoom.com/markets/se/" />
<link rel="alternate" hreflang="nl" href="https://www.quixzoom.com/markets/nl/" />
<meta name="keywords" content="quiXzoom, quixzoom, earn money photos, photo missions, zoomer, crowdsourced field data, tjäna pengar foton, fältdatainsamling Sverige">
<meta name="keywords" content="quiXzoom, earn money taking photos, crowdsourced field data, photo missions, zoomer platform, fältdatainsamling, tjäna pengar foton">
<meta property="og:type" content="website">
<meta property="og:title" content="quiXzoom — Capture the World. Get Paid.">
<meta property="og:description" content="Take photo missions near you. Get paid every Monday. No platform fee — you keep 100% of your earned mission pay. Launching August 1, 2026.">
<meta property="og:url" content="https://www.quixzoom.com/">
<meta property="og:image" content="https://www.quixzoom.com/images/hero-zoomer-city.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="quiXzoom — Earn per Mission photographing city infrastructure">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://www.quixzoom.com/images/hero-zoomer-city.jpg">
<meta name="theme-color" content="#F7F7F5">
<link rel="manifest" href="/site.webmanifest">
<link rel="icon" type="image/png" sizes="512x512" href="/favicon-512x512.png">
<link rel="preconnect" href="https://api.quixzoom.com">
<script type="application/ld+json">{"@context":"https://schema.org","@graph":[
{"@type":["Organization","Corporation"],"@id":"https://www.quixzoom.com/#organization","name":"quiXzoom","legalName":"Landvex AB","alternateName":["quiXzoom","quixzoom","QuiXzoom","QUIXZOOM","quick zoom app","quix zoom","qui xzoom","quiX zoom","quixzoom app","quixzoom platform","quiXzoom platform","quiXzoom Sverige","quiXzoom Sweden"],"url":"https://www.quixzoom.com/","foundingDate":"2026","description":"quiXzoom is a crowdsourced field data collection platform. Independent contractors called Zoomers photograph infrastructure, roads, buildings and urban environments on demand and earn money per approved mission.","logo":{"@type":"ImageObject","url":"https://www.quixzoom.com/favicon-512x512.png","width":512,"height":512},"brand":{"@type":"Brand","name":"quiXzoom","slogan":"Collect reality."},"disambiguatingDescription":"quiXzoom (spelled with a capital X) is a crowdsourced field data platform — not related to Zoom video conferencing or camera zoom features.","sameAs":["https://www.quixzoom.com","https://app.quixzoom.com","https://www.linkedin.com/company/quixzoom","https://www.instagram.com/quixzoom","https://twitter.com/quixzoom","https://x.com/quixzoom","https://www.crunchbase.com/organization/quixzoom"]},
{"@type":"WebSite","@id":"https://www.quixzoom.com/#website","url":"https://www.quixzoom.com/","name":"quiXzoom","description":"Crowdsourced field data collection — earn money photographing infrastructure and urban environments near you.","publisher":{"@id":"https://www.quixzoom.com/#organization"}},
{"@type":"SoftwareApplication","name":"quiXzoom","applicationCategory":"LifestyleApplication","operatingSystem":"iOS, Android","description":"Crowdsourced field data collection platform. Zoomers earn money photographing infrastructure and urban environments on demand.","offers":{"@type":"Offer","price":"0","priceCurrency":"EUR","description":"Free to join — earn per approved mission"}},
{"@type":"FAQPage","mainEntity":[
{"@type":"Question","name":"How much can I earn as a Zoomer?","acceptedAnswer":{"@type":"Answer","text":"Missions reward 30120 QX Credits each. Active Zoomers doing 812 missions per week typically accumulate 4001,200 Credits weekly. Credits are redeemable as cash via weekly payout — the exact amount depends on your local payout rate."}},
{"@type":"Question","name":"Do I need a professional camera?","acceptedAnswer":{"@type":"Answer","text":"No. iPhone 12 or later, or any Android with 12MP or better, is all you need."}},
{"@type":"Question","name":"When do I get paid?","acceptedAnswer":{"@type":"Answer","text":"QX Credits are added to your balance when each mission is approved. Once your balance reaches the minimum payout threshold (250 Credits), funds are transferred to your registered payout account via Stripe Connect. Tax treatment of payouts varies by country — you are responsible for your own tax obligations."}},
{"@type":"Question","name":"Is there a signup fee?","acceptedAnswer":{"@type":"Answer","text":"No. quiXzoom is completely free to join. quiXzoom charges no commission or platform fee — you receive 100% of your earned QX Credits."}},
{"@type":"Question","name":"How do you spell quiXzoom?","acceptedAnswer":{"@type":"Answer","text":"The correct spelling is quiXzoom — with a capital X in the middle. Common misspellings include quixzoom (all lowercase), QuiXzoom, quick zoom, and quix zoom. All refer to the same platform. The brand name quiXzoom has no relation to Zoom video conferencing software."}},
{"@type":"Question","name":"Vad heter appen quiXzoom?","acceptedAnswer":{"@type":"Answer","text":"Appen heter quiXzoom och stavas med stort X i mitten. Ibland skrivs det quixzoom eller QuiXzoom. Det är samma plattform. quiXzoom är inte relaterat till Zoom-videokonferens."}}
]}
]}</script>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"Speakable","speakableSelector":{"@type":"CssSelectorType","cssSelector":["h1",".hero-h1",".hero-tagline"]},"url":"https://www.quixzoom.com/"}</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{"@type": "ImageObject", "@id": "https://www.quixzoom.com/images/og-image.jpg", "url": "https://www.quixzoom.com/images/og-image.jpg", "name": "quiXzoom — Collect Reality. Get Paid.", "description": "Official quiXzoom brand image with logo and tagline.", "width": 1200, "height": 630, "encodingFormat": "image/jpeg", "representativeOfPage": true, "creditText": "quiXzoom"},
{"@type": "ImageObject", "@id": "https://www.quixzoom.com/images/hero-zoomer-city.jpg", "url": "https://www.quixzoom.com/images/hero-zoomer-city.jpg", "name": "quiXzoom Zoomer photographing city storefront", "description": "A quiXzoom Zoomer photographs a storefront for a field data collection mission.", "width": 1200, "height": 630, "encodingFormat": "image/jpeg", "creditText": "quiXzoom"},
{"@type": "ImageObject", "@id": "https://www.quixzoom.com/images/mission-map.jpg", "url": "https://www.quixzoom.com/images/mission-map.jpg", "name": "quiXzoom mission map", "description": "quiXzoom app real-time mission map with active photo mission pins.", "width": 1200, "height": 630, "encodingFormat": "image/jpeg", "creditText": "quiXzoom"},
{"@type": "ImageObject", "@id": "https://www.quixzoom.com/images/zoomer-earnings.jpg", "url": "https://www.quixzoom.com/images/zoomer-earnings.jpg", "name": "quiXzoom Zoomer weekly earnings", "description": "quiXzoom Zoomer earnings dashboard showing QX Credits and weekly cash payout.", "width": 1200, "height": 630, "encodingFormat": "image/jpeg", "creditText": "quiXzoom"},
{"@type": "ImageObject", "@id": "https://www.quixzoom.com/images/infrastructure-scan.jpg", "url": "https://www.quixzoom.com/images/infrastructure-scan.jpg", "name": "quiXzoom infrastructure photo mission", "description": "Zoomer holds iPhone photographing building for quiXzoom infrastructure mission.", "width": 1200, "height": 630, "encodingFormat": "image/jpeg", "creditText": "quiXzoom"},
{"@type": "ImageObject", "@id": "https://www.quixzoom.com/images/quixzoom-team.jpg", "url": "https://www.quixzoom.com/images/quixzoom-team.jpg", "name": "quiXzoom global Zoomer network", "description": "quiXzoom global network of Zoomers collecting field data in 120+ cities.", "width": 1200, "height": 630, "encodingFormat": "image/jpeg", "creditText": "quiXzoom"},
{"@type": "ImageObject", "@id": "https://www.quixzoom.com/images/app-screenshot.jpg", "url": "https://www.quixzoom.com/images/app-screenshot.jpg", "name": "quiXzoom app on iPhone showing mission list", "description": "quiXzoom app screenshot on iPhone showing active missions and QX Credit rewards.", "width": 1200, "height": 630, "encodingFormat": "image/jpeg", "creditText": "quiXzoom"},
{"@type": "ImageObject", "@id": "https://www.quixzoom.com/images/sweden-launch.jpg", "url": "https://www.quixzoom.com/images/sweden-launch.jpg", "name": "quiXzoom Sweden launch Stockholm August 2026", "description": "quiXzoom launches in Sweden August 1 2026 starting with Stockholm.", "width": 1200, "height": 630, "encodingFormat": "image/jpeg", "creditText": "quiXzoom"}
]
}
</script>
<style>
:root{
--bg-dark:#1a1a1a;--bg-white:#FFFFFF;--bg-off:#F7F7F5;
--blue:#0066FF;--blue-dark:#0052CC;--blue-pale:rgba(0,102,255,.07);
--green:#00C853;--text-dark:#1a1a1a;--text-body:#3a3a3a;--text-muted:#6B6B6B;
--radius-card:16px;--shadow-hover:0 16px 48px rgba(0,0,0,.14);
--transition:.22s cubic-bezier(.25,.46,.45,.94);
--text-xs:.75rem;
--text-sm:.875rem;
--text-base:1rem;
--text-lg:1.125rem;
--text-xl:1.375rem;
--text-2xl:1.75rem;
--radius-sm:8px;
--radius-md:14px;
--radius-lg:24px;
--radius-full:100px
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
body{font-family:-apple-system,BlinkMacSystemFont,'Inter','Helvetica Neue',sans-serif;font-size:1rem;line-height:1.65;color:var(--text-body);background:var(--bg-white);-webkit-font-smoothing:antialiased;overflow-x:hidden}
a{color:inherit;text-decoration:none}
ul{list-style:none}
button{cursor:pointer;font-family:inherit}
h1{font-size:clamp(2.75rem,6vw,5rem);font-weight:900;letter-spacing:-.05em;line-height:1}
h2{font-size:clamp(2rem,3.5vw,3rem);font-weight:800;letter-spacing:-.04em;line-height:1.1}
h3{font-size:var(--text-xl);font-weight:700;letter-spacing:-.02em;line-height:1.3}
.container{width:100%;max-width:1160px;margin:0 auto;padding:0 24px}
.section-pad{padding:96px 0}
.section-header{text-align:center;margin-bottom:64px}
.section-header h2{color:var(--text-dark);margin-bottom:16px}
.section-header p{font-size:var(--text-lg);color:var(--text-muted);max-width:580px;margin:0 auto}
/* NAV */
nav{position:sticky;top:0;z-index:300;background:var(--bg-white);border-bottom:1px solid rgba(0,0,0,.08);box-shadow:0 1px 3px rgba(0,0,0,.05)}
.nav-inner{display:flex;align-items:center;justify-content:space-between;height:68px;gap:32px}
.nav-logo{font-size:1.375rem;font-weight:900;color:var(--text-dark);letter-spacing:-.04em}
.nav-logo .x{color:var(--blue)}
.nav-links{display:flex;align-items:center;gap:32px;flex:1;justify-content:center}
.nav-links a{color:var(--text-muted);font-size:var(--text-base);font-weight:500;transition:color .15s}
.nav-links a:hover{color:var(--text-dark)}
.btn-nav{background:var(--blue);color:var(--text-dark);border:none;border-radius:var(--radius-lg);padding:10px 24px;font-size:var(--text-base);font-weight:700;transition:background .15s;white-space:nowrap;flex-shrink:0}
.btn-nav:hover{background:var(--blue-dark)}
.mobile-menu{display:none}@media(max-width:768px){.nav-links{display:none}.btn-nav{display:none}
.hamburger{display:none;flex-direction:column;gap:5px;cursor:pointer;padding:8px;background:none;border:none;z-index:999}
.hamburger span{display:block;width:22px;height:2px;background:var(--text-dark);border-radius:2px;transition:all .3s}
.hamburger.open span:nth-child(1){transform:translateY(7px) rotate(45deg)}
.hamburger.open span:nth-child(2){opacity:0}
.hamburger.open span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}
.mobile-menu{display:none;position:fixed;inset:0;top:68px;background:var(--bg-white);z-index:299;padding:24px;flex-direction:column;gap:0;border-top:1px solid rgba(0,0,0,.08);box-shadow:0 4px 20px rgba(0,0,0,.1)}
.mobile-menu.open{display:flex}
.mobile-menu a{color:var(--text-body);font-size:1.1rem;font-weight:500;padding:18px 0;border-bottom:1px solid rgba(0,0,0,.08);display:block}
.mobile-menu a:last-child{border-bottom:none}
.mobile-menu .btn-mobile{display:block;margin-top:24px;background:var(--blue);color:var(--text-dark);text-align:center;border-radius:var(--radius-lg);padding:16px 24px;font-size:1rem;font-weight:700}
@media(max-width:768px){.hamburger{display:flex}}}
/* HERO */
#hero{background:var(--bg-off);min-height:calc(100vh - 68px);display:flex;align-items:center;padding:80px 0 96px}
.hero-layout{display:grid;grid-template-columns:1fr 420px;gap:64px;align-items:center;width:100%}
.hero-inner{max-width:560px}
.hero-visual{display:flex;justify-content:center;align-items:center;position:relative;overflow:hidden}
.phone-frame{width:240px;height:480px;background:#1a1a1a;border-radius:36px;border:2px solid rgba(0,0,0,.1);box-shadow:0 40px 80px rgba(0,0,0,.15),0 0 0 1px rgba(0,0,0,.05);position:relative;overflow:hidden;flex-shrink:0}
.phone-notch{position:absolute;top:0;left:50%;transform:translateX(-50%);width:80px;height:24px;background:#2a2a2a;border-radius:0 0 16px 16px;z-index:10}
.phone-screen{position:absolute;inset:0;background:#fff;border-radius:34px;overflow:hidden;display:flex;flex-direction:column}
.ps-statusbar{height:40px;background:#f5f5f5;display:flex;align-items:flex-end;justify-content:space-between;padding:0 16px 6px;flex-shrink:0}
.ps-time{color:rgba(0,0,0,.9);font-size:.65rem;font-weight:700}
.ps-icons{display:flex;gap:4px;align-items:center}
.ps-map{flex:1;background:linear-gradient(160deg,#1a2535 0%,#1e3a2f 40%,#162030 100%);position:relative;overflow:hidden}
.ps-road{position:absolute;background:rgba(0,0,0,.08);border-radius:2px}
.ps-mission-pin{position:absolute;top:38%;left:52%;transform:translate(-50%,-50%)}
.ps-pin-outer{width:44px;height:44px;border-radius:50%;background:rgba(0,102,255,.2);display:flex;align-items:center;justify-content:center;animation:pingPulse 2s ease infinite}
.ps-pin-inner{width:24px;height:24px;border-radius:50%;background:#0066FF;display:flex;align-items:center;justify-content:center}
.ps-user-dot{position:absolute;bottom:32%;left:38%;width:14px;height:14px;border-radius:50%;background:#00C853;border:2px solid #fff;box-shadow:0 0 0 4px rgba(0,200,83,.2)}
@keyframes pingPulse{0%,100%{transform:translate(-50%,-50%) scale(1);opacity:1}50%{transform:translate(-50%,-50%) scale(1.3);opacity:.7}}
.ps-card{position:absolute;bottom:0;left:0;right:0;background:#fff;border-radius:20px 20px 0 0;padding:16px;box-shadow:0 -4px 24px rgba(0,0,0,.3)}
.ps-card-title{font-size:.7rem;font-weight:800;color:#1a1a1a;margin-bottom:2px}
.ps-card-addr{font-size:.6rem;color:#555;margin-bottom:8px}
.ps-card-row{display:flex;justify-content:space-between;align-items:center}
.ps-card-reward{font-size:.85rem;font-weight:900;color:#0066FF}
.ps-card-btn{background:#0066FF;color:var(--text-dark);border:none;border-radius:12px;padding:6px 12px;font-size:.6rem;font-weight:700}
.ps-card-meta{font-size:.58rem;color:#666;margin-top:4px}
.phone-glow{position:absolute;width:300px;height:300px;border-radius:50%;background:radial-gradient(circle,rgba(0,102,255,.15) 0%,transparent 70%);top:50%;left:50%;transform:translate(-50%,-50%);z-index:-1}
.hero-stats-pill{display:flex;gap:24px;margin-top:32px;flex-wrap:wrap}
.hstat{text-align:left}
.hstat-num{font-size:1.5rem;font-weight:900;color:var(--text-dark);letter-spacing:-.04em;line-height:1}
.hstat-lbl{font-size:var(--text-xs);color:var(--text-muted);margin-top:2px}
@media(max-width:960px){.hero-layout{grid-template-columns:1fr;gap:40px}.hero-visual{order:-1}.phone-frame{width:200px;height:400px}.hero-inner{max-width:100%}}
@media(max-width:600px){.hero-visual{display:none}}
.hero-badge{display:inline-flex;align-items:center;gap:8px;background:var(--blue-pale);border:1px solid rgba(0,102,255,.15);border-radius:var(--radius-full);padding:8px 18px;font-size:var(--text-sm);font-weight:600;color:var(--blue);margin-bottom:36px}
.hero-h1{color:var(--text-dark);margin-bottom:28px}
.hero-h1 em{font-style:normal;color:var(--blue)}
.hero-sub{font-size:clamp(1.0625rem,2vw,1.25rem);color:var(--text-body);max-width:640px;line-height:1.6;margin-bottom:44px}
.wl-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:20px}
.wl-input{flex:1;min-width:280px;background:#fff;color:var(--text-dark);border:2.5px solid #fff;border-radius:var(--radius-lg);padding:18px 28px;font-size:var(--text-base);font-family:inherit;outline:none;transition:border-color .15s,box-shadow .15s}
.wl-input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(0,102,255,.18)}
.wl-submit{background:var(--blue);color:var(--text-dark);border:none;border-radius:var(--radius-lg);padding:18px 36px;font-size:1rem;font-weight:800;font-family:inherit;cursor:pointer;transition:background .15s;white-space:nowrap;flex-shrink:0}
.wl-submit:hover{background:var(--blue-dark)}
.wl-submit:disabled{opacity:.7;cursor:not-allowed}
.wl-trust{font-size:var(--text-sm);color:var(--text-muted);margin-bottom:20px}
.wl-ok{display:flex;align-items:center;gap:12px;padding:18px 28px;background:rgba(0,200,83,.12);border:1.5px solid rgba(0,200,83,.4);border-radius:var(--radius-lg);color:var(--green);font-weight:700;font-size:1rem;margin-bottom:20px}
.wl-err{color:#FF4444;font-size:.9rem;font-weight:600;margin-bottom:12px}
.wl-counter{font-size:var(--text-base);color:var(--text-muted);margin-top:16px}
.wl-counter strong{color:var(--text-dark);font-weight:700}
.audience-split{display:grid;grid-template-columns:1fr 1fr;gap:12px;max-width:560px;margin:28px 0 20px}
.audience-card{display:flex;align-items:center;gap:14px;padding:18px 20px;border-radius:var(--radius-md);cursor:pointer;transition:transform .2s,background .2s}
.audience-zoomer{background:rgba(0,102,255,.15);border:1.5px solid rgba(0,102,255,.4)}
.audience-zoomer:hover{background:rgba(0,102,255,.25);transform:translateY(-2px)}
.audience-org{background:var(--bg-off);border:1.5px solid rgba(0,0,0,.1)}
.audience-org:hover{background:rgba(0,0,0,.05);transform:translateY(-2px)}
.audience-icon{flex-shrink:0;width:34px;height:34px;color:var(--text-dark)}
.audience-icon svg{width:100%;height:100%}
.audience-label{font-size:var(--text-sm);font-weight:700;color:var(--text-dark);display:block}
.audience-desc{font-size:.75rem;color:var(--text-muted);line-height:1.4;display:block;margin-top:2px}
.audience-arrow{color:var(--text-muted);font-size:1rem;flex-shrink:0;margin-left:auto}
@media(max-width:600px){.wl-row{flex-direction:column;align-items:stretch}.wl-input{min-width:0}.audience-split{grid-template-columns:1fr}}
/* STATS */
#stats{background:var(--blue);padding:56px 0}
.stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:0}
.stat-item{text-align:center;padding:20px 16px;position:relative}
.stat-item:not(:last-child)::after{content:'';position:absolute;right:0;top:20%;height:60%;width:1px;background:rgba(10,10,10,.4)}
.stat-value{font-size:clamp(2rem,4vw,3rem);font-weight:900;color:var(--text-dark);letter-spacing:-.04em;line-height:1;display:block;margin-bottom:8px}
.stat-label{font-size:.9rem;color:rgba(10,10,10,.9);font-weight:500}
@media(max-width:600px){.stats-grid{grid-template-columns:repeat(2,1fr);gap:1px;background:rgba(10,10,10,.3)}.stat-item{background:var(--blue)}.stat-item::after{display:none}}
/* CLICKABLE CARDS — shared */
.drill-card{cursor:pointer;position:relative;transition:transform var(--transition),box-shadow var(--transition)}
.drill-card:hover{transform:translateY(-4px);box-shadow:var(--shadow-hover)}
.drill-cta{display:inline-flex;align-items:center;gap:6px;margin-top:auto;padding-top:16px;font-size:var(--text-sm);font-weight:700;color:var(--blue)}
.drill-arrow{transition:transform .2s}
.drill-card:hover .drill-arrow{transform:translateX(4px)}
/* HOW IT WORKS */
#how{background:var(--bg-white)}
.steps-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:28px}
.step-card{background:var(--bg-off);border-radius:var(--radius-card);padding:40px 36px}
.step-number{display:inline-flex;align-items:center;justify-content:center;width:48px;height:48px;background:var(--blue);color:var(--text-dark);border-radius:50%;font-size:var(--text-xl);font-weight:900;margin-bottom:24px}
.step-card h3{color:var(--text-dark);margin-bottom:12px;font-size:var(--text-xl)}
.step-card p{color:var(--text-muted);font-size:.9625rem;line-height:1.6}
@media(max-width:768px){.steps-grid{grid-template-columns:1fr;gap:20px}.step-card{padding:32px 28px}}
/* PERSONAS */
#personas{background:var(--bg-off)}
.personas-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}
.persona-card{background:var(--bg-white);border-radius:var(--radius-card);padding:28px 24px;border:1px solid #E8E8E8;display:flex;flex-direction:column;gap:12px;min-height:200px}
.persona-avatar{width:48px;height:48px;border-radius:50%;background:var(--blue);color:var(--text-dark);font-size:var(--text-sm);font-weight:700;display:flex;align-items:center;justify-content:center}
.persona-name{font-size:1rem;font-weight:700;color:var(--text-dark)}
.persona-text{font-size:var(--text-base);color:var(--text-body);line-height:1.65}
@media(max-width:768px){.personas-grid{grid-template-columns:1fr}}
/* MISSION TYPES */
#missions{background:var(--bg-off)}
.missions-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
.mission-card{background:var(--bg-white);border:1.5px solid #E8E8E8;border-radius:var(--radius-card);padding:32px 28px}
.mission-card:hover{border-color:rgba(0,102,255,.25)}
.mission-icon{width:52px;height:52px;display:flex;align-items:center;justify-content:center;background:var(--bg-off);border-radius:var(--radius-md);margin-bottom:20px;color:var(--blue)}
.mission-icon svg{width:28px;height:28px}
.mission-card h3{color:var(--text-dark);margin-bottom:10px;font-size:var(--text-lg)}
.mission-card p{color:var(--text-muted);font-size:.9rem;line-height:1.55;margin-bottom:18px}
.mission-chip{display:inline-block;background:var(--blue-pale);color:var(--blue);border-radius:var(--radius-full);padding:5px 14px;font-size:var(--text-sm);font-weight:700}
@media(max-width:900px){.missions-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:560px){.missions-grid{grid-template-columns:1fr}}
/* EARNINGS */
#earnings{background:var(--bg-white)}
.earnings-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;margin-bottom:48px}
.tier-card{background:var(--bg-off);border:2px solid #E8E8E8;border-radius:var(--radius-card);padding:36px 28px;text-align:center}
.tier-card.featured{background:var(--bg-off);border-color:var(--blue);transform:scale(1.03)}
.tier-card.featured:hover{transform:scale(1.03) translateY(-4px) !important}
.tier-card h3{font-size:1.5rem;font-weight:900;margin-bottom:4px;color:var(--text-dark)}
.tier-card.featured h3{color:var(--text-dark)}
.tier-missions{font-size:.9rem;color:var(--text-muted);margin-bottom:20px;font-weight:500}
.tier-card.featured .tier-missions{color:var(--text-muted)}
.tier-features{text-align:left}
.tier-features li{display:flex;align-items:flex-start;gap:10px;font-size:var(--text-base);color:var(--text-body);padding:6px 0}
.tier-card.featured .tier-features li{color:var(--text-body)}
.tier-features li::before{content:'';display:block;width:18px;height:18px;min-width:18px;background:var(--green) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='8' viewBox='0 0 10 8'%3E%3Cpath d='M1 4l2.5 2.5L9 1' stroke='white' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E") no-repeat center;border-radius:50%;margin-top:2px}
.payment-accordion{border:1.5px solid #E8E8E8;border-radius:var(--radius-card);overflow:hidden}
.payment-accordion summary{display:flex;align-items:center;justify-content:space-between;padding:22px 28px;font-size:1rem;font-weight:700;color:var(--text-dark);cursor:pointer;list-style:none;transition:background .15s}
.payment-accordion summary::-webkit-details-marker{display:none}
.payment-accordion summary:hover{background:var(--bg-off)}
.payment-accordion summary::after{content:'';display:block;width:20px;height:20px;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B6B6B' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") contain no-repeat;transition:transform .15s;flex-shrink:0}
.payment-accordion[open] summary::after{transform:rotate(180deg)}
.payment-accordion-body{padding:0 28px 24px;background:var(--bg-off)}
.payment-accordion-body li{display:flex;align-items:flex-start;gap:12px;padding:10px 0;font-size:.9625rem;color:var(--text-body);border-bottom:1px solid #E8E8E8}
.payment-accordion-body li:last-child{border-bottom:none}
.payment-accordion-body .dot{display:block;width:8px;height:8px;min-width:8px;background:var(--blue);border-radius:50%;margin-top:7px}
@media(max-width:768px){.earnings-grid{grid-template-columns:1fr;gap:20px}.tier-card.featured{transform:none}}
/* FAQ */
#faq{background:var(--bg-white)}
.faq-list{max-width:760px;margin:0 auto}
.faq-item{border:1.5px solid #E8E8E8;border-radius:var(--radius-md);margin-bottom:12px;overflow:hidden;cursor:pointer;transition:border-color .15s}
.faq-item:hover{border-color:rgba(0,102,255,.3)}
.faq-item summary{cursor:pointer;display:flex;align-items:center;justify-content:space-between;padding:22px 28px;font-size:1rem;font-weight:700;color:var(--text-dark);cursor:pointer;list-style:none;gap:16px;transition:background .15s}
.faq-item summary::-webkit-details-marker{display:none}
.faq-item summary:hover{background:var(--bg-off)}
.faq-item summary::after{content:'';display:block;width:22px;height:22px;min-width:22px;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230066FF' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") contain no-repeat;transition:transform .15s;flex-shrink:0}
.faq-item[open] summary::after{transform:rotate(180deg)}
.faq-item[open] summary{background:var(--bg-off);border-bottom:1px solid #E8E8E8}
.faq-answer{padding:20px 28px 24px;font-size:.9625rem;color:var(--text-body);line-height:1.7}
.faq-drill-hint{font-size:var(--text-sm);color:var(--blue);font-weight:600;margin-top:10px;display:flex;align-items:center;gap:4px;cursor:pointer}
/* BUSINESSES */
#businesses{background:#001836}
.businesses-header{text-align:center;margin-bottom:64px}
.businesses-header h2{color:var(--text-dark);margin-bottom:16px}
.businesses-header p{font-size:var(--text-lg);color:var(--text-muted);max-width:600px;margin:0 auto}
.biz-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;margin-bottom:56px}
.biz-card{background:var(--bg-white);border:1px solid rgba(0,0,0,.1);border-radius:var(--radius-card);padding:36px 28px;transition:background .15s,border-color .15s}
.biz-card:hover{background:var(--bg-off);border-color:rgba(0,0,0,.15)}
.biz-drill{display:block;font-size:.8rem;font-weight:700;color:var(--blue);margin-top:16px}
.biz-icon{width:52px;height:52px;display:flex;align-items:center;justify-content:center;background:rgba(0,102,255,.18);border-radius:var(--radius-md);margin-bottom:20px;color:var(--blue)}
.biz-icon svg{width:26px;height:26px}
.biz-card h3{color:var(--text-dark);margin-bottom:12px;font-size:var(--text-lg)}
.biz-card p{color:var(--text-body);font-size:.9625rem;line-height:1.6}
.biz-cta{text-align:center}
.btn-ghost{display:inline-flex;align-items:center;justify-content:center;background:transparent;color:var(--text-dark);border:2px solid rgba(0,0,0,.3);border-radius:var(--radius-lg);padding:16px 46px;font-size:1rem;font-weight:700;cursor:pointer;transition:border-color .15s,background .15s}
.btn-ghost:hover{border-color:rgba(0,0,0,.6);background:rgba(0,0,0,.05)}
@media(max-width:768px){.biz-grid{grid-template-columns:1fr;gap:16px}}
.community-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;margin-bottom:48px}
.community-card{background:var(--bg-off);border-radius:var(--radius-card);padding:32px 28px}
.community-card h3{font-size:1.125rem;font-weight:800;color:var(--text-dark);margin-bottom:8px}
.community-card p{font-size:var(--text-sm);color:var(--text-body);line-height:1.6}
.community-tag{display:inline-block;font-size:.75rem;font-weight:700;color:var(--blue);background:rgba(0,102,255,.08);border-radius:4px;padding:3px 8px;margin-bottom:14px}
.field-report-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
.field-report-list li{display:flex;gap:10px;align-items:flex-start;font-size:var(--text-sm);color:var(--text-body);line-height:1.5}
.field-report-list li::before{content:'';color:var(--blue);font-weight:700;flex-shrink:0}
.community-pipeline{background:var(--bg-off);border-radius:var(--radius-card);padding:36px 40px;display:flex;gap:48px;align-items:center;margin-top:48px}
.community-pipeline-text{flex:1}
.community-pipeline-text h3{color:var(--text-dark);font-size:1.25rem;font-weight:800;margin-bottom:8px}
.community-pipeline-text p{color:var(--text-muted);font-size:var(--text-sm);line-height:1.65}
.community-pipeline-badge{flex-shrink:0;background:var(--blue);color:var(--text-dark);font-size:.8125rem;font-weight:700;padding:10px 20px;border-radius:40px;white-space:nowrap}
@media(max-width:960px){.community-grid{grid-template-columns:1fr;gap:16px}.community-pipeline{flex-direction:column;gap:24px;padding:28px 24px}}
/* CTA BOTTOM */
#cta-bottom{background:var(--bg-off)}
#cta-bottom .section-header h2{color:var(--text-dark)}
#cta-bottom .section-header p{color:var(--text-muted)}
/* FOOTER */
footer{background:var(--bg-white);border-top:1px solid rgba(0,0,0,.08);padding:64px 0 40px}
.footer-inner{display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:48px;margin-bottom:56px}
.footer-brand .nav-logo{display:block;margin-bottom:14px;font-size:1.5rem}
.footer-tagline{color:var(--text-muted);font-size:.9rem;line-height:1.55;margin-bottom:20px}
.footer-col h4{color:var(--text-dark);font-size:var(--text-sm);font-weight:800;letter-spacing:.06em;text-transform:uppercase;margin-bottom:18px}
.footer-col ul li{margin-bottom:10px}
.footer-col ul li a{color:var(--text-muted);font-size:.9rem;transition:color .15s}
.footer-col ul li a:hover{color:var(--text-dark)}
.footer-bottom{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap;padding-top:32px;border-top:1px solid rgba(0,0,0,.08)}
.footer-copyright{color:rgba(10,10,10,.6);font-size:var(--text-sm)}
.footer-copyright a{color:rgba(10,10,10,.75);text-decoration:underline;text-decoration-color:rgba(10,10,10,.5)}.footer-social{display:flex;align-items:center;gap:12px}.footer-social-link{color:rgba(10,10,10,.75);transition:color .15s;display:flex;align-items:center}.footer-social-link:hover{color:var(--text-dark)}
.lang-switcher{display:flex;align-items:center;gap:4px;flex-wrap:wrap}
.lang-switcher span{color:rgba(10,10,10,.75);font-size:var(--text-sm);margin-right:4px}
.lang-link{color:rgba(10,10,10,.75);font-size:var(--text-sm);font-weight:600;padding:4px 8px;border-radius:var(--radius-sm);transition:color .15s,background .15s}
.lang-link:hover{color:var(--text-dark);background:rgba(10,10,10,.07)}
.lang-link.active{color:var(--text-dark);background:rgba(0,102,255,.25)}
@media(max-width:900px){.footer-inner{grid-template-columns:1fr 1fr;gap:36px}.footer-brand{grid-column:1/-1}}
@media(max-width:560px){.footer-inner{grid-template-columns:1fr}.footer-brand{grid-column:auto}.footer-bottom{flex-direction:column;align-items:flex-start}}
/* ═══════════════════════════════════
DEEP DRILL OVERLAY — 7 LAYERS
═══════════════════════════════════ */
.overlay-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:400;opacity:0;pointer-events:none;transition:opacity .25s;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}
.overlay-backdrop.open{opacity:1;pointer-events:all}
.overlay-panel{position:fixed;top:0;right:0;bottom:0;width:min(760px,100vw);background:#fff;z-index:500;transform:translateX(100%);transition:transform .3s cubic-bezier(.32,.72,0,1);overflow:hidden;display:flex;flex-direction:column}
.overlay-panel.open{transform:translateX(0)}
.overlay-header{padding:20px 24px 0;border-bottom:1px solid #EBEBEB;flex-shrink:0;background:#fff;position:relative;z-index:2}
.overlay-close{position:absolute;top:18px;right:20px;width:32px;height:32px;border-radius:50%;background:#F2F2F7;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;color:#6C6C70;transition:background .15s;font-family:inherit}
.overlay-close:hover{background:#E5E5EA}
.overlay-breadcrumb{font-size:var(--text-sm);color:#555;margin-bottom:6px;display:flex;align-items:center;gap:6px}
.overlay-breadcrumb span{color:var(--blue);font-weight:600}
.overlay-type{font-size:var(--text-xs);font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--blue);margin-bottom:6px}
.overlay-title{font-size:1.625rem;font-weight:800;letter-spacing:-.04em;color:#1a1a1a;margin-bottom:18px;padding-right:40px}
.layer-nav-wrap{position:relative;margin:0 -24px;overflow-x:auto;overflow-y:hidden;border-top:1px solid #EBEBEB;scrollbar-width:none}.layer-nav-wrap::after{content:"";position:absolute;right:0;top:0;bottom:0;width:48px;background:linear-gradient(to right,transparent,#fff);pointer-events:none;z-index:1}.layer-nav{display:flex;gap:0;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;padding:0 24px;-webkit-overflow-scrolling:touch}
.layer-nav::-webkit-scrollbar{display:none}
.layer-tab{flex-shrink:0;padding:12px 16px 11px;font-size:var(--text-sm);font-weight:600;color:#8E8E93;cursor:pointer;border:none;background:transparent;font-family:inherit;border-bottom:2px solid transparent;transition:color .15s,border-color .15s;white-space:nowrap;user-select:none}.layer-nav-wrap::after{content:"";position:absolute;top:0;right:0;bottom:0;width:48px;background:linear-gradient(to right,transparent,#fff 80%);pointer-events:none;z-index:2}
.layer-tab.active{color:var(--blue);border-bottom-color:var(--blue)}
.layer-tab:hover:not(.active){color:var(--text-dark)}
@media(max-width:600px){
.layer-nav-wrap{overflow-x:visible;margin:0}
.layer-nav-wrap::after{display:none}
.layer-nav{
display:flex;
flex-direction:column;
gap:0;
overflow-x:visible;
padding:0;
}
.layer-tab{
white-space:normal;
text-align:left;
padding:12px 16px;
border-bottom:1px solid #F0F0F0;
border-left:3px solid transparent;
font-size:.875rem;
}
.layer-tab.active{
border-bottom-color:#F0F0F0;
border-left-color:var(--blue);
background:#F8F8FF;
color:var(--blue);
}
.layer-body{padding:20px 0}
}
.overlay-body{flex:1;overflow-y:auto;padding:0}
.layer-panel{display:none;padding:32px 24px 48px;animation:layerIn .2s ease}
.layer-panel.active{display:block}
@keyframes layerIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
/* Layer content */
.layer-lead{font-size:var(--text-lg);color:var(--text-dark);line-height:1.7;margin-bottom:28px}
.layer-h3{font-size:1rem;font-weight:700;color:var(--text-dark);margin:24px 0 8px;letter-spacing:-.02em}
.layer-p{font-size:var(--text-base);color:var(--text-muted);line-height:1.7;margin-bottom:12px}
.layer-ul{margin:0 0 16px;padding:0}
.layer-ul li{list-style:none;padding:8px 0 8px 24px;position:relative;font-size:var(--text-base);color:var(--text-body);line-height:1.6;border-bottom:.5px solid #F5F5F5}
.layer-ul li:last-child{border-bottom:none}
.layer-ul li::before{content:'';position:absolute;left:8px;color:var(--blue);font-weight:700}
.layer-block{background:#F7F7F5;border-radius:var(--radius-md);padding:20px 20px 4px;margin-bottom:20px}
.layer-block-title{font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#555;margin-bottom:12px}
.layer-kpi-row{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:20px}
.layer-kpi{background:#fff;border:1px solid #EBEBEB;border-radius:var(--radius-sm);padding:16px 12px;text-align:center}
.layer-kpi-val{font-size:1.375rem;font-weight:800;letter-spacing:-.04em;color:#1a1a1a;line-height:1}
.layer-kpi-lbl{font-size:.75rem;color:#555;margin-top:4px}
.layer-badge-row{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}
.layer-badge{display:inline-block;padding:5px 12px;border-radius:20px;font-size:var(--text-sm);font-weight:600;background:var(--blue-pale);color:var(--blue);border:1px solid rgba(0,102,255,.12)}
.layer-step-list{counter-reset:lstep}
.layer-step{counter-increment:lstep;display:flex;gap:14px;padding:14px 0;border-bottom:.5px solid #F5F5F5;align-items:flex-start}
.layer-step:last-child{border-bottom:none}
.layer-step-num{flex-shrink:0;width:26px;height:26px;border-radius:50%;background:var(--blue);color:var(--text-dark);font-size:.75rem;font-weight:800;display:flex;align-items:center;justify-content:center}
.layer-step-title{font-size:var(--text-base);font-weight:700;color:var(--text-dark);margin-bottom:3px}
.layer-step-desc{font-size:var(--text-sm);color:var(--text-muted);line-height:1.55}
@media(max-width:480px){
.layer-stats{grid-template-columns:1fr 1fr;gap:8px}
.layer-stat{padding:12px}
.layer-kpi-row{grid-template-columns:1fr 1fr}
}
.layer-table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;margin-bottom:16px}
.layer-table{width:100%;border-collapse:collapse;font-size:var(--text-sm)}
.layer-table th{text-align:left;padding:10px 12px;background:#F7F7F5;font-weight:700;font-size:.75rem;text-transform:uppercase;letter-spacing:.06em;color:#6C6C70}
.layer-table td{padding:12px;border-bottom:.5px solid #F5F5F5;color:var(--text-body);vertical-align:top}
.layer-table tr:last-child td{border-bottom:none}
.layer-quote{border-left:3px solid var(--blue);padding:12px 16px;background:var(--blue-pale);border-radius:0 8px 8px 0;margin-bottom:20px}
.layer-quote p{font-size:var(--text-base);color:var(--text-dark);font-style:italic;line-height:1.65}
.layer-cta-box{background:var(--bg-off);border-radius:var(--radius-md);padding:28px 24px;text-align:center;margin-top:8px}
.layer-cta-title{font-size:var(--text-lg);font-weight:800;color:var(--text-dark);margin-bottom:8px;letter-spacing:-.03em}
.layer-cta-sub{font-size:.9rem;color:rgba(10,10,10,.75);margin-bottom:20px;line-height:1.55}
.layer-cta-btn{display:inline-block;background:var(--blue);color:var(--text-dark);padding:14px 36px;border-radius:var(--radius-lg);font-size:var(--text-base);font-weight:800;cursor:pointer;transition:background .15s}
.layer-cta-btn:hover{background:var(--blue-dark)}
/* ── Glossary term triggers ────────────────────────────────────────── */
.gterm {
border-bottom: 1.5px dotted #0066FF;
cursor: pointer;
color: inherit;
text-decoration: none;
display: inline;
transition: color .15s, border-color .15s;
}
.gterm:hover { color: #0066FF; border-color: #0066FF; }
.gtooltip {
position: fixed;
z-index: 9999;
background: #1d1d1f;
color: #f5f5f7;
border-radius: 10px;
padding: 12px 16px;
max-width: 280px;
font-size: .8125rem;
line-height: 1.5;
pointer-events: none;
opacity: 0;
transform: translateY(4px);
transition: opacity .15s, transform .15s;
box-shadow: 0 8px 32px rgba(0,0,0,.25);
}
.gtooltip.visible { opacity: 1; transform: translateY(0); }
.gtooltip-term { font-weight: 700; color: var(--text-dark); display: block; margin-bottom: 4px; }
.gtooltip-def { color: rgba(10,10,10,.75); }
.gtooltip-more { display: block; margin-top: 6px; font-size: .75rem; color: #0066FF; }
/* ── Social proof ─────────────────────────────────────────────── */
#social-proof{background:#fff;padding:64px 0;border-bottom:1px solid #1a1a1a}
.proof-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;margin-bottom:48px}
.proof-card{background:var(--bg-off);border-radius:var(--radius-md);padding:28px 24px;position:relative}
.proof-card::before{content:'"';font-size:3rem;line-height:1;color:var(--blue);opacity:.15;position:absolute;top:12px;left:20px;font-family:Georgia,serif}
.proof-quote{font-size:var(--text-base);color:var(--text-body);line-height:1.65;margin-bottom:16px;padding-top:16px}
.proof-author{display:flex;align-items:center;gap:10px}
.proof-avatar{width:36px;height:36px;border-radius:50%;background:var(--blue);color:var(--text-dark);font-weight:700;font-size:var(--text-sm);display:flex;align-items:center;justify-content:center;flex-shrink:0}
.proof-name{font-size:var(--text-sm);font-weight:700;color:var(--text-body)}
.proof-meta{font-size:var(--text-xs);color:var(--text-muted)}
.proof-stars{color:#F5A623;font-size:.875rem;margin-bottom:8px;letter-spacing:1px}
.proof-logos{display:flex;align-items:center;justify-content:center;gap:40px;flex-wrap:wrap;padding-top:32px;border-top:1px solid #eee}
.proof-logo-label{font-size:var(--text-xs);font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:#777;text-align:center}
.proof-logo-name{font-size:var(--text-sm);font-weight:800;color:#555;letter-spacing:-.02em}
@media(max-width:768px){.proof-grid{grid-template-columns:1fr;gap:16px}.proof-logos{gap:24px}}
/* ── Scroll fade-in ───────────────────────────────────────────── */
.fade-in{opacity:0;transform:translateY(20px);transition:opacity .5s ease,transform .5s ease}
.fade-in.visible{opacity:1;transform:translateY(0)}
.fade-in-delay-1{transition-delay:.1s}
.fade-in-delay-2{transition-delay:.2s}
.fade-in-delay-3{transition-delay:.3s}
/* ═══════════════ NAV DROPDOWN ═══════════════ */
.nav-links > li { position:relative; list-style:none; }
.nav-links > li > a, .nav-drop-toggle {
display:flex; align-items:center; gap:4px;
font-size:.875rem; font-weight:500; color:var(--text-dark);
text-decoration:none; padding:8px 0; white-space:nowrap;
background:none; border:none; cursor:pointer; font-family:inherit;
transition:color .15s;
}
.nav-links > li > a:hover, .nav-drop-toggle:hover { color:var(--qx-blue); }
.nav-drop-toggle svg { width:12px; height:12px; transition:transform .2s; flex-shrink:0; }
.nav-links > li.open > .nav-drop-toggle svg { transform:rotate(180deg); }
.nav-dropdown {
display:none; position:absolute; top:calc(100% + 8px); left:50%;
transform:translateX(-50%); min-width:220px;
background:#fff; border:1px solid #E8E8ED; border-radius:12px;
box-shadow:0 8px 32px rgba(0,0,0,.12); padding:8px; z-index:300;
}
.nav-links > li.open .nav-dropdown { display:block; }
.nav-dropdown a {
display:flex; align-items:center; gap:10px; padding:9px 12px;
border-radius:8px; font-size:.875rem; color:var(--text-dark);
text-decoration:none; transition:background .12s;
}
.nav-dropdown a:hover { background:#F5F5F7; }
.nav-dropdown a .nd-icon { font-size:1.1rem; width:24px; text-align:center; flex-shrink:0; }
.nav-dropdown a .nd-text { display:flex; flex-direction:column; }
.nav-dropdown a .nd-title { font-weight:600; font-size:.85rem; line-height:1.3; }
.nav-dropdown a .nd-sub { font-size:.75rem; color:#555; margin-top:1px; }
.nd-divider { height:1px; background:#F2F2F7; margin:4px 0; }
/* ═══════════════ MEGA FOOTER ═══════════════ */
footer {
background:var(--bg-off); color:var(--text-dark); margin-top:80px;
padding:0; border-top:none;
}
.footer-top {
padding:56px 0 48px;
border-bottom:1px solid rgba(10,10,10,.1);
}
.footer-grid {
display:grid;
grid-template-columns:200px repeat(5, 1fr);
gap:32px;
max-width:1200px; margin:0 auto; padding:0 32px;
}
.footer-brand-col .footer-logo {
font-size:1.3rem; font-weight:800; letter-spacing:-.04em;
color:var(--text-dark); text-decoration:none; display:block; margin-bottom:12px;
}
.footer-brand-col .footer-logo .x { color:var(--qx-blue, #0066FF); }
.footer-brand-col .footer-desc {
font-size:.8rem; color:rgba(10,10,10,.75); line-height:1.6;
margin-bottom:20px;
}
.footer-social-row { display:flex; gap:10px; }
.footer-soc-btn {
width:36px; height:36px; border-radius:8px;
background:rgba(10,10,10,.08); border:1px solid rgba(10,10,10,.12);
display:flex; align-items:center; justify-content:center;
color:rgba(10,10,10,.7); text-decoration:none; font-size:.85rem;
transition:background .15s, color .15s;
}
.footer-soc-btn:hover { background:rgba(10,10,10,.16); color:var(--text-dark); }
.footer-col-title {
font-size:.7rem; font-weight:700; text-transform:uppercase; letter-spacing:.1em;
color:rgba(10,10,10,.75); margin-bottom:16px;
}
.footer-col-links { list-style:none; padding:0; margin:0; }
.footer-col-links li { margin-bottom:10px; }
.footer-col-links a {
font-size:.875rem; color:rgba(10,10,10,.65); text-decoration:none;
transition:color .15s;
}
.footer-col-links a:hover { color:var(--text-dark); }
.footer-col-links .badge-new {
font-size:.65rem; font-weight:700; background:var(--qx-blue,#0066FF);
color:var(--text-dark); padding:1px 5px; border-radius:4px; margin-left:5px;
vertical-align:middle;
}
.footer-col-links .badge-soon {
font-size:.65rem; font-weight:700; background:rgba(10,10,10,.12);
color:rgba(10,10,10,.5); padding:1px 5px; border-radius:4px; margin-left:5px;
vertical-align:middle;
}
/* Trust badges */
.footer-trust {
display:flex; align-items:center; gap:16px; margin-top:20px; flex-wrap:wrap;
}
.trust-badge {
background:rgba(10,10,10,.06); border:1px solid rgba(10,10,10,.1);
border-radius:8px; padding:8px 12px; font-size:.72rem; font-weight:700;
color:rgba(10,10,10,.7); display:flex; align-items:center; gap:6px;
letter-spacing:.02em;
}
.trust-badge .tb-icon { font-size:1rem; }
/* Footer bottom */
.footer-bottom {
max-width:1200px; margin:0 auto; padding:20px 32px;
display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px;
}
.footer-copy { font-size:.8rem; color:rgba(10,10,10,.75); }
.footer-legal-links { display:flex; gap:20px; flex-wrap:wrap;gap:8px 16px;}
.footer-legal-links a { font-size:.8rem; color:rgba(10,10,10,.75); text-decoration:none; }
.footer-legal-links a:hover { color:rgba(10,10,10,.8); }
.footer-lang-row { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
.footer-lang-row .fl-lbl { font-size:.72rem; color:rgba(10,10,10,.75); }
.flang { font-size:.78rem; color:rgba(10,10,10,.75); text-decoration:none; padding:3px 6px; border-radius:4px; }
.flang:hover, .flang.active { background:rgba(10,10,10,.1); color:var(--text-dark); }
/* Responsive */
@media(max-width:1024px){
.footer-grid { grid-template-columns:1fr 1fr 1fr; }
.footer-brand-col { grid-column:1/-1; }
}
@media(max-width:600px){
.footer-grid { grid-template-columns:1fr 1fr; padding:0 16px; }
.footer-bottom { flex-direction:column; align-items:flex-start; gap:10px; }
}
@media(max-width:480px){
.footer-grid { grid-template-columns:1fr; padding:0 12px; }
}
/* ── Global mobile fix ───────────────────────────────────────────── */
@media(max-width:480px){
img, svg { max-width:100%; }
table { overflow-x:auto; display:block; }
.container { padding:0 16px !important; box-sizing:border-box; }
}
/* AUDIT FIX m-001 safety net */
html,body{overflow-x:clip}
/* Market Selector */
.market-selector{position:relative;display:inline-flex;align-items:center}
.market-selector-btn{background:rgba(10,10,10,.08);border:1px solid rgba(10,10,10,.12);border-radius:var(--radius-full,100px);padding:6px 14px;font-size:.75rem;font-weight:700;color:rgba(10,10,10,.8);cursor:pointer;display:flex;align-items:center;gap:6px;transition:.15s;letter-spacing:.04em}
.market-selector-btn:hover{background:rgba(10,10,10,.14)}
.market-selector-current{font-variant:all-small-caps}
.market-dropdown{position:absolute;top:calc(100% + 6px);right:0;background:#fff;border:1px solid rgba(0,0,0,.1);border-radius:12px;padding:8px;min-width:160px;z-index:400;display:none;flex-direction:column;gap:2px;box-shadow:0 16px 40px rgba(0,0,0,.15)}
.market-selector.open .market-dropdown{display:flex}
.market-dropdown button{background:none;border:none;color:rgba(10,10,10,.75);font-size:.8125rem;font-weight:500;padding:8px 12px;border-radius:8px;text-align:left;cursor:pointer;transition:.12s}
.market-dropdown button:hover{background:rgba(10,10,10,.08);color:var(--text-dark)}
/* Earnings Calculator */
.qx-calc{background:var(--bg-off,#f7f7f5);border-radius:20px;padding:32px;margin-top:40px;max-width:560px;margin-left:auto;margin-right:auto}
.qx-calc h3{font-size:1.125rem;font-weight:800;color:var(--text-dark,#1a1a1a);margin-bottom:4px}
.qx-calc-sub{font-size:.875rem;color:var(--text-muted,#6b6b6b);margin-bottom:24px}
.qx-calc-row{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:20px}
.qx-calc-label{font-size:.875rem;font-weight:600;color:var(--text-dark,#1a1a1a);min-width:120px}
.qx-calc input[type=range]{flex:1;accent-color:#0066FF;height:4px;cursor:pointer}
.qx-calc-val{font-size:.875rem;font-weight:700;color:#0066FF;min-width:60px;text-align:right}
.qx-calc-result{background:var(--bg-off);border-radius:12px;padding:20px 24px;display:flex;justify-content:space-between;align-items:center;margin-top:8px}
.qx-calc-result-label{font-size:.875rem;color:rgba(10,10,10,.6)}
.qx-calc-result-amount{font-size:1.75rem;font-weight:900;color:var(--text-dark);letter-spacing:-.04em}
.qx-calc-result-period{font-size:.75rem;color:rgba(10,10,10,.75);margin-top:2px}
/* GeoIP banner */
#qx-geo-banner{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#fff;border:1px solid rgba(0,0,0,.12);border-radius:100px;padding:10px 20px 10px 16px;display:flex;align-items:center;gap:12px;font-size:.8125rem;color:rgba(0,0,0,.85);z-index:800;box-shadow:0 8px 32px rgba(0,0,0,.15);white-space:nowrap;animation:slideUp .3s ease}
@keyframes slideUp{from{opacity:0;transform:translateX(-50%) translateY(8px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
#qx-geo-banner button.geo-switch{background:#0066FF;color:var(--text-dark);border:none;border-radius:100px;padding:5px 14px;font-size:.75rem;font-weight:700;cursor:pointer}
#qx-geo-banner button.geo-dismiss{background:none;border:none;color:rgba(10,10,10,.75);font-size:1rem;cursor:pointer;padding:0 0 0 4px;line-height:1}
@media(max-width:480px){#qx-geo-banner{bottom:16px;left:16px;right:16px;transform:none;border-radius:16px;white-space:normal;flex-wrap:wrap}}
/* Tier illustrations */
.tier-ill{display:flex;justify-content:center;margin-bottom:16px}
.tier-ill svg{opacity:.85}
/* QX-ILL-08 Global map */
.qx-global-map{margin:40px auto 0;max-width:100%;background:linear-gradient(135deg,#0d1a2e 0%,#0a1628 60%,#0d1f3c 100%);border-radius:16px;padding:24px 20px 16px;border:1px solid rgba(10,10,10,.06)}
.qx-global-map-title{font-size:.6875rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:rgba(10,10,10,.75);text-align:center;margin-bottom:14px}
.qx-global-map svg{display:block;margin:0 auto;max-width:100%}
.qx-global-map-legend{display:flex;justify-content:center;gap:20px;margin-top:12px}
.qx-map-legend-item{display:flex;align-items:center;gap:6px;font-size:.6875rem;color:rgba(10,10,10,.75)}
.qx-map-legend-dot{width:8px;height:8px;border-radius:50%}
@media(max-width:480px){.tier-earnings{font-size:1.125rem!important;white-space:normal;overflow-wrap:anywhere}.tier-card{padding:20px 16px}}
@media(max-width:400px){.hero-stats-pill{flex-wrap:wrap;gap:16px 24px}}
@media(max-width:520px){.qx-calc-row{flex-wrap:wrap}.qx-calc input[type=range]{min-width:0;width:100%}.qx-calc-result{flex-direction:column;gap:8px;text-align:center}.qx-calc-result-amount{font-size:1.375rem}}
@media(max-width:360px){.market-selector{display:none}}
</style>
<script src="/locale-data.js"></script>
<style>
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0a0a0a;
color: #ffffff;
line-height: 1.6;
min-height: 100vh;
}
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
header {
padding: 24px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 24px; font-weight: 700; color: #0066ff; }
.hero {
padding: 80px 0 60px;
text-align: center;
}
.hero h1 {
font-size: clamp(2rem, 4vw, 3.5rem);
font-weight: 800;
line-height: 1.2;
margin-bottom: 16px;
}
.hero p {
font-size: 1.125rem;
color: rgba(255,255,255,0.7);
max-width: 600px;
margin: 0 auto 40px;
}
.language-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
padding: 40px 0;
}
.language-card {
background: rgba(255,255,255,0.05);
padding: 24px;
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.1);
text-decoration: none;
color: #fff;
transition: transform 0.2s, background 0.2s;
display: flex;
align-items: center;
gap: 16px;
}
.language-card:hover {
transform: translateY(-2px);
background: rgba(255,255,255,0.08);
}
.lang-flag {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
background: rgba(255,255,255,0.1);
}
.lang-info h3 {
font-size: 1.125rem;
margin-bottom: 4px;
}
.lang-info p {
font-size: 0.875rem;
color: rgba(255,255,255,0.6);
}
.region-note {
text-align: center;
padding: 60px 0 40px;
color: rgba(255,255,255,0.5);
font-size: 0.875rem;
}
footer {
padding: 40px 0;
text-align: center;
color: rgba(255,255,255,0.4);
font-size: 0.875rem;
border-top: 1px solid rgba(255,255,255,0.1);
}
</style>
</head>
<body>
<a href="#main-content" style="position:absolute;top:-100px;left:16px;background:var(--blue);color:var(--text-dark);padding:10px 20px;border-radius:0 0 8px 8px;font-weight:700;font-size:.9rem;z-index:9999;transition:top .2s" onfocus="this.style.top='0'" onblur="this.style.top='-100px'">Skip to main content</a>
<nav>
<div class="container">
<div class="nav-inner">
<a href="/" class="nav-logo">qui<span class="x">X</span>zoom</a>
<ul class="nav-links">
<header>
<div class="logo">quiXzoom</div>
</header>
<!-- Platform dropdown -->
<li>
<button class="nav-drop-toggle" onclick="toggleDrop(this)">
Platform <svg viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
</button>
<div class="nav-dropdown">
<a href="#how"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg></span><span class="nd-text"><span class="nd-title">How it works</span><span class="nd-sub">Mission flow end-to-end</span></span></a>
<a href="#missions"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg></span><span class="nd-text"><span class="nd-title">Mission types</span><span class="nd-sub">Infrastructure, real estate, environment</span></span></a>
<a href="#earnings"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg></span><span class="nd-text"><span class="nd-title">Zoomer earnings</span><span class="nd-sub">Compensation & tiers</span></span></a>
<a href="/markets/se/"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15 15 0 0 1 0 20M12 2a15 15 0 0 0 0 20"/></svg></span><span class="nd-text"><span class="nd-title">Sverige</span><span class="nd-sub">Launching August 2026</span></span></a>
</div>
</li>
<section class="hero">
<h1>Choose your language<br><br></h1>
<p>Select your preferred language to continue. quiXzoom is available in 8 Asian languages.</p>
</section>
<!-- Solutions dropdown -->
<li>
<button class="nav-drop-toggle" onclick="toggleDrop(this)">
Solutions <svg viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
</button>
<div class="nav-dropdown">
<a href="/for-organisations/"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9M15 21V9"/></svg></span><span class="nd-text"><span class="nd-title">For Organisations</span><span class="nd-sub">Municipalities, infrastructure, insurance</span></span></a>
<a href="/for-organisations/#use-cases"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span><span class="nd-text"><span class="nd-title">Use cases</span><span class="nd-sub">Facade, road, coastal, property</span></span></a>
<a href="/for-organisations/#enterprise"><span class="nd-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></span><span class="nd-text"><span class="nd-title">Enterprise</span><span class="nd-sub">Custom contracts & SLA</span></span></a>
</div>
</li>
<li><a href="/developer">Developers</a></li>
<li><a href="/pricing">Pricing</a></li>
<li><a href="/about">About</a></li>
</ul>
<div style="display:flex;align-items:center;gap:10px;flex-shrink:0">
<a href="/support" style="font-size:.875rem;color:var(--text-dark);text-decoration:none;font-weight:500">Support</a>
<button class="btn-nav" onclick="document.getElementById('wl-email').focus();document.getElementById('wl-email').scrollIntoView({behavior:'smooth'})">Register as a Zoomer →</button>
</div>
<div class="market-selector" id="market-selector">
<button class="market-selector-btn" onclick="document.getElementById('market-selector').classList.toggle('open')" aria-label="Select market" aria-haspopup="listbox">
<span class="market-selector-current">SE</span>
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
</button>
<div class="market-dropdown" role="listbox">
<button onclick="QX_setMarket('SE')" role="option">&#127480;&#127466; Sweden — Launching Aug 2026</button>
<button onclick="QX_setMarket('NL')" role="option">&#127475;&#127473; Netherlands — Launching Q1 2027</button>
<button onclick="QX_setMarket('TH')" role="option">&#127481;&#127469; Thailand — Launching Q3 2026</button>
<button onclick="QX_setMarket('NG')" role="option">&#127475;&#127468; Nigeria — Coming 2026</button>
<button onclick="QX_setMarket('KE')" role="option">&#127472;&#127466; Kenya — Coming 2026</button>
</div>
</div>
<button class="hamburger" id="hamburger" aria-label="Menu" onclick="toggleMenu()">
<span></span><span></span><span></span>
</button>
</div>
</div>
</nav>
<div class="mobile-menu" id="mobile-menu">
<a href="#how" onclick="closeMenu()">How it works</a>
<a href="#missions" onclick="closeMenu()">Missions</a>
<a href="#earnings" onclick="closeMenu()">Earnings</a>
<a href="#recognition" onclick="closeMenu()">Recognition</a>
<a href="/for-organisations/" onclick="closeMenu()">For Organisations</a>
<a href="/developer" onclick="closeMenu()">Developer API</a>
<a href="/support" onclick="closeMenu()">Support</a>
<a class="btn-mobile" onclick="closeMenu();document.getElementById('wl-email').focus();document.getElementById('wl-email').scrollIntoView({behavior:'smooth'})">Register as a Zoomer →</a>
</div>
<main id="main-content">
<!-- HERO -->
<section id="hero">
<div class="container">
<div class="hero-layout">
<div class="hero-inner">
<div class="hero-badge">Global rollout · August 2026</div>
<div style="font-size:1rem;font-weight:700;letter-spacing:.08em;color:var(--blue);text-transform:uppercase;margin-bottom:12px;font-family:-apple-system,BlinkMacSystemFont,sans-serif">quiXzoom</div>
<h1 class="hero-h1">Capture the<br>world.<br><em>Earn per mission.</em></h1>
<p class="hero-sub">Take photo missions near you. Get paid every Monday. No platform fee. Launching August 1, 2026.</p>
<form id="waitlist-form-top" onsubmit="submitWaitlist(event,'top')" novalidate>
<div class="wl-row">
<input type="tel" id="wl-phone" name="phone" class="wl-input" placeholder="Your phone number" required autocomplete="tel">
<button type="submit" id="wl-btn" class="wl-submit">Register as a Zoomer →</button>
<section class="language-grid">
<a href="/zh-cn/" class="language-card">
<div class="lang-flag"></div>
<div class="lang-info">
<h3></h3>
<p>Simplified Chinese — </p>
</div>
<div id="wl-ok" class="wl-ok" hidden>✓ You're on the list! We'll be in touch before launch.</div>
<p id="wl-err" class="wl-err" hidden>Something went wrong. Please try again.</p>
</form>
<div class="audience-split">
<a href="#waitlist-form-top" class="audience-card audience-zoomer">
<div class="audience-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="10" r="3"/><path d="M3 21c0-3.3 4-6 9-6s9 2.7 9 6"/></svg></div>
<div><span class="audience-label">I want to earn</span><span class="audience-desc">Take photo missions near me.</span></div>
<span class="audience-arrow"></span>
</a>
<a href="/for-organisations/" class="audience-card audience-org">
<div class="audience-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></div>
<div><span class="audience-label">We need field data</span><span class="audience-desc">Commission documentation at scale.</span></div>
<span class="audience-arrow"></span>
</a>
</div>
<p class="wl-trust">No signup fee · No platform fee · Paid every Monday · Identity verification required once</p>
<div class="trust-badges" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:14px">
<span class="badge" style="background:rgba(10,10,10,.07);border:1px solid rgba(10,10,10,.13);border-radius:20px;padding:5px 12px;font-size:.72rem;font-weight:600;color:rgba(10,10,10,.7)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> GDPR Compliant</span>
<span class="badge" style="background:rgba(10,10,10,.07);border:1px solid rgba(10,10,10,.13);border-radius:20px;padding:5px 12px;font-size:.72rem;font-weight:600;color:rgba(10,10,10,.7)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><rect x="1" y="4" width="22" height="16" rx="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg> Stripe Connect Verified</span>
<span class="badge" style="background:rgba(10,10,10,.07);border:1px solid rgba(10,10,10,.13);border-radius:20px;padding:5px 12px;font-size:.72rem;font-weight:600;color:rgba(10,10,10,.7)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><polyline points="9 12 12 15 16 10"/></svg> Identity Verified</span>
<span class="badge" style="background:rgba(10,10,10,.07);border:1px solid rgba(10,10,10,.13);border-radius:20px;padding:5px 12px;font-size:.72rem;font-weight:600;color:rgba(10,10,10,.7)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg> DAC7 Reporting</span>
</div>
<div style="display:flex;align-items:center;gap:12px;margin-top:20px;flex-wrap:wrap">
<a href="https://app.quixzoom.com/join" style="display:flex;align-items:center;gap:8px;background:rgba(10,10,10,.08);border:1px solid rgba(10,10,10,.15);border-radius:10px;padding:10px 16px;text-decoration:none">
<svg width="20" height="20" viewBox="0 0 24 24" fill="rgba(10,10,10,.7)"><path d="M17 3H7c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 16c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm5-4H7V5h10v10z"/></svg>
<div><div style="font-size:.7rem;color:rgba(10,10,10,.75);line-height:1">Pre-register · Join Waitlist</div><div style="font-size:.875rem;font-weight:700;color:var(--text-dark);line-height:1.2">Get notified at launch</div></div>
</a>
</div>
<div id="hero-signup-badge" style="display:flex;align-items:center;gap:8px;margin-top:16px">
<div style="display:flex">
<div style="width:28px;height:28px;border-radius:50%;background:#0066FF;border:2px solid #1a1a1a;display:flex;align-items:center;justify-content:center;font-size:.7rem;color:var(--text-dark);font-weight:700;margin-left:-6px">Z</div>
<div style="width:28px;height:28px;border-radius:50%;background:#0052CC;border:2px solid #1a1a1a;display:flex;align-items:center;justify-content:center;font-size:.7rem;color:var(--text-dark);font-weight:700;margin-left:-6px">Z</div>
<div style="width:28px;height:28px;border-radius:50%;background:#003D99;border:2px solid #1a1a1a;display:flex;align-items:center;justify-content:center;font-size:.7rem;color:var(--text-dark);font-weight:700;margin-left:-6px">+</div>
</div>
<span style="font-size:.875rem;color:rgba(10,10,10,.7)"><strong id="hero-signup-count" style="color:var(--text-dark)">2,400+</strong> Zoomers pre-registered</span>
</div>
<p class="wl-counter" id="wl-counter-wrap" style="display:block"><strong><span id="wl-count">2,400+</span></strong> <a class="gterm" data-gterm="Zoomer" href="/glossary#zoomer">Zoomers</a> already signed up</p>
<p class="wl-trust" style="color:rgba(10,10,10,.75);margin-top:12px">Launching August 1, 2026.</p>
<div class="hero-stats-pill">
<div class="hstat"><div class="hstat-num">30120</div><div class="hstat-lbl">credits per mission<br><span style="font-size:.7rem;color:rgba(10,10,10,.55);font-weight:400">1 Credit&nbsp;=&nbsp;€1 · min. payout 250 Credits</span></div></div>
<div class="hstat"><div class="hstat-num">Sverige</div><div class="hstat-lbl">Lanserar 1 aug 2026</div></div>
<div class="hstat"><div class="hstat-num">0%</div><div class="hstat-lbl">platform fee</div></div>
</div>
</div><!-- /hero-inner -->
<div class="hero-visual">
<div class="phone-glow"></div>
<div class="phone-frame">
<div class="phone-notch"></div>
<div class="phone-screen">
<!-- Status bar -->
<div class="ps-statusbar">
<span class="ps-time">09:41</span>
<div class="ps-icons">
<svg width="14" height="10" viewBox="0 0 14 10" fill="rgba(10,10,10,.8)"><rect x="0" y="3" width="2" height="7" rx="1"/><rect x="3" y="2" width="2" height="8" rx="1"/><rect x="6" y="1" width="2" height="9" rx="1"/><rect x="9" y="0" width="2" height="10" rx="1"/></svg>
<svg width="12" height="10" viewBox="0 0 12 10" fill="rgba(10,10,10,.8)"><path d="M6 2C3.5 2 1.3 3.1 0 4.8L1.5 6.3C2.4 5 4.1 4 6 4s3.6 1 4.5 2.3L12 4.8C10.7 3.1 8.5 2 6 2z"/><path d="M6 5.5C4.8 5.5 3.7 6 3 6.9L4.5 8.4C5 7.9 5.5 7.5 6 7.5s1 .4 1.5.9L9 6.9C8.3 6 7.2 5.5 6 5.5z"/><circle cx="6" cy="9.5" r="1"/></svg>
<svg width="20" height="10" viewBox="0 0 20 10" fill="none"><rect x="0" y="1" width="17" height="8" rx="2" stroke="rgba(10,10,10,.7)" stroke-width="1.2"/><rect x="1" y="2" width="12" height="6" rx="1" fill="rgba(10,10,10,.8)"/><path d="M18 3.5v3c.8-.4 1.3-1 1.3-1.5S18.8 3.9 18 3.5z" fill="rgba(10,10,10,.7)"/></svg>
</div>
</div>
<!-- Map area -->
<div class="ps-map">
<!-- Roads -->
<div class="ps-road" style="width:100%;height:8px;top:45%;left:0;transform:rotate(-5deg)"></div>
<div class="ps-road" style="width:8px;height:100%;top:0;left:40%"></div>
<div class="ps-road" style="width:8px;height:60%;top:0;left:65%;opacity:.5"></div>
<div class="ps-road" style="width:70%;height:6px;top:25%;left:30%;transform:rotate(8deg);opacity:.4"></div>
<!-- City blocks -->
<div style="position:absolute;top:15%;left:15%;width:18%;height:12%;background:rgba(10,10,10,.04);border-radius:3px"></div>
<div style="position:absolute;top:15%;left:48%;width:14%;height:10%;background:rgba(10,10,10,.04);border-radius:3px"></div>
<div style="position:absolute;top:60%;left:20%;width:16%;height:14%;background:rgba(10,10,10,.04);border-radius:3px"></div>
<div style="position:absolute;top:60%;left:50%;width:20%;height:12%;background:rgba(10,10,10,.04);border-radius:3px"></div>
<!-- Mission pin -->
<div class="ps-mission-pin">
<div class="ps-pin-outer">
<div class="ps-pin-inner">
<svg width="12" height="12" viewBox="0 0 24 24" fill="white"><path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"/><circle cx="12" cy="13" r="4"/></svg>
</div>
</div>
</div>
<!-- User location -->
<div class="ps-user-dot"></div>
<!-- Distance badge -->
<div style="position:absolute;top:18%;right:8%;background:rgba(0,0,0,.6);border-radius:8px;padding:4px 8px;backdrop-filter:blur(4px)">
<div style="font-size:.55rem;color:rgba(10,10,10,.75)">Distance</div>
<div style="font-size:.7rem;font-weight:700;color:var(--text-dark)">1.4 km</div>
</div>
<!-- Mission card -->
<div class="ps-card">
<div class="ps-card-title">Fasadinventering</div>
<div class="ps-card-addr">Storgatan 14, Stockholm</div>
<div class="ps-card-row">
<div>
<div class="ps-card-reward">285 kr</div>
<div class="ps-card-meta">3 foton · ~12 min</div>
</div>
<button class="ps-card-btn">Claim →</button>
</div>
</div>
</div>
</a>
<a href="/zh-tw/" class="language-card">
<div class="lang-flag"></div>
<div class="lang-info">
<h3></h3>
<p>Traditional Chinese — /</p>
</div>
</div>
</div><!-- /hero-visual -->
</div><!-- /hero-layout -->
</div>
</section>
<!-- STATS -->
<section id="stats">
<div class="container">
<div class="stats-grid">
<div class="stat-item fade-in" style="cursor:pointer" onclick="document.getElementById('how').scrollIntoView({behavior:'smooth'})"><span class="stat-value">~30 min</span><span class="stat-label">Average mission time</span></div>
<div class="stat-item fade-in" style="cursor:pointer" onclick="document.getElementById('how').scrollIntoView({behavior:'smooth'})"><span class="stat-value">Aug 2026</span><span class="stat-label">Launching in Sweden</span></div>
<div class="stat-item fade-in" style="cursor:pointer" onclick="document.getElementById('how').scrollIntoView({behavior:'smooth'})"><span class="stat-value">August 2026</span><span class="stat-label">Launching in Sweden</span></div>
<div class="stat-item fade-in" style="cursor:pointer" onclick="document.getElementById('how').scrollIntoView({behavior:'smooth'})"><span class="stat-value">100%</span><span class="stat-label">Paid on approval</span></div>
</div>
</div>
</section>
<!-- SOCIAL PROOF -->
<section id="social-proof">
<div class="container">
<div class="section-header" style="margin-bottom:40px">
<h2 style="font-size:var(--text-2xl)">Why Zoomers choose quiXzoom</h2>
<p>What makes the platform work for independent field contributors.</p>
</div>
<div class="proof-grid">
<div class="proof-card fade-in">
<div style="font-size:2rem;margin-bottom:12px">📱</div>
<h3 style="font-size:var(--text-lg);margin-bottom:8px">Point, shoot, submit</h3>
<p style="color:var(--text-muted);font-size:var(--text-sm)">The app guides you through each mission. No guesswork, no complex instructions.</p>
</div>
<div class="proof-card fade-in">
<div style="font-size:2rem;margin-bottom:12px"></div>
<h3 style="font-size:var(--text-lg);margin-bottom:8px">Fast approval</h3>
<p style="color:var(--text-muted);font-size:var(--text-sm)">AI reviews your submission instantly. Most missions are approved within minutes, not days.</p>
</div>
<div class="proof-card fade-in">
<div style="font-size:2rem;margin-bottom:12px">💰</div>
<h3 style="font-size:var(--text-lg);margin-bottom:8px">Weekly payouts</h3>
<p style="color:var(--text-muted);font-size:var(--text-sm)">Reach 250 Credits (€250) and get paid automatically every Monday via Stripe Connect.</p>
</div>
</div>
<div class="proof-logos">
<div style="text-align:center">
<div class="proof-logo-label">trusted by</div>
<div class="proof-logo-name">Municipalities</div>
</div>
<div style="color:#ddd;font-size:1.5rem">·</div>
<div style="text-align:center">
<div class="proof-logo-label">trusted by</div>
<div class="proof-logo-name">Property Groups</div>
</div>
<div style="color:#ddd;font-size:1.5rem">·</div>
<div style="text-align:center">
<div class="proof-logo-label">trusted by</div>
<div class="proof-logo-name">Insurance Companies</div>
</div>
<div style="color:#ddd;font-size:1.5rem">·</div>
<div style="text-align:center">
<div class="proof-logo-label">trusted by</div>
<div class="proof-logo-name">Infrastructure Operators</div>
</div>
</div>
</div>
</section>
<!-- HOW IT WORKS -->
<section id="how" class="section-pad">
<div class="container">
<div class="section-header">
<h2>How it works</h2>
<p>Three steps. Click any step to go deeper.</p>
</div>
<div class="qx-how-ill" aria-hidden="true" style="margin-bottom:32px;overflow-x:auto">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 160" role="img" aria-label="How quiXzoom works: Browse missions, claim a mission, photograph, get paid weekly">
<!-- Connector lines -->
<line x1="126" y1="80" x2="234" y2="80" stroke="#0066FF" stroke-opacity="0.3" stroke-width="1.5" stroke-dasharray="4 3"/>
<line x1="326" y1="80" x2="434" y2="80" stroke="#0066FF" stroke-opacity="0.3" stroke-width="1.5" stroke-dasharray="4 3"/>
<line x1="526" y1="80" x2="634" y2="80" stroke="#0066FF" stroke-opacity="0.3" stroke-width="1.5" stroke-dasharray="4 3"/>
<!-- Step 1: Browse -->
<circle cx="80" cy="80" r="36" fill="#0066FF" fill-opacity="0.1" stroke="#0066FF" stroke-opacity="0.3" stroke-width="1.5"/>
<!-- Map pin icon -->
<g transform="translate(80,80)">
<!-- Pin body (teardrop) -->
<path d="M0,-16 C-9,-16 -16,-9 -16,0 C-16,9 0,20 0,20 C0,20 16,9 16,0 C16,-9 9,-16 0,-16 Z"
fill="#0066FF" fill-opacity="0.7" stroke="none"/>
<!-- Pin inner circle -->
<circle cx="0" cy="0" r="5.5" fill="white" fill-opacity="0.9"/>
</g>
<text x="80" y="133" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="13" fill="#3a3a3a" font-weight="600">Browse</text>
<text x="80" y="148" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="11" fill="#6B6B6B">missions</text>
<!-- Step 2: Claim -->
<circle cx="280" cy="80" r="36" fill="#0066FF" fill-opacity="0.1" stroke="#0066FF" stroke-opacity="0.3" stroke-width="1.5"/>
<!-- Handshake / check icon: two hands clasping -->
<g transform="translate(280,80)" fill="none" stroke="#0066FF" stroke-opacity="0.85" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- Left hand -->
<path d="M-14,4 C-14,4 -10,-2 -6,-2 L0,2 L6,-2 C10,-2 14,4 14,4"/>
<!-- Right clasped fingers -->
<path d="M-10,4 C-10,8 -6,12 0,12 C6,12 10,8 10,4"/>
<!-- Check mark overlay -->
<polyline points="-7,-10 -2,-4 8,-14" stroke="#0066FF" stroke-opacity="0.85" stroke-width="2.5"/>
</g>
<text x="280" y="133" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="13" fill="#3a3a3a" font-weight="600">Claim</text>
<text x="280" y="148" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="11" fill="#6B6B6B">a mission</text>
<!-- Step 3: Photograph -->
<circle cx="480" cy="80" r="36" fill="#0066FF" fill-opacity="0.1" stroke="#0066FF" stroke-opacity="0.3" stroke-width="1.5"/>
<!-- Camera icon -->
<g transform="translate(480,80)" fill="none" stroke="#0066FF" stroke-opacity="0.85" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- Camera body -->
<rect x="-16" y="-8" width="32" height="22" rx="3" ry="3" fill="#0066FF" fill-opacity="0.15" stroke="#0066FF" stroke-opacity="0.75"/>
<!-- Lens -->
<circle cx="0" cy="4" r="7" fill="none" stroke="#0066FF" stroke-opacity="0.85"/>
<circle cx="0" cy="4" r="3.5" fill="#0066FF" fill-opacity="0.4" stroke="none"/>
<!-- Viewfinder bump -->
<path d="M-6,-8 L-6,-13 L6,-13 L6,-8"/>
<!-- Flash -->
<rect x="8" y="-7" width="4" height="3" rx="1" fill="#0066FF" fill-opacity="0.5" stroke="none"/>
</g>
<text x="480" y="133" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="13" fill="#3a3a3a" font-weight="600">Photograph</text>
<!-- Step 4: Get Paid -->
<circle cx="680" cy="80" r="36" fill="#00C853" fill-opacity="0.12" stroke="#00C853" stroke-opacity="0.4" stroke-width="1.5"/>
<!-- Coin + check icon -->
<g transform="translate(680,80)">
<!-- Coin circle -->
<circle cx="-4" cy="1" r="13" fill="#00C853" fill-opacity="0.25" stroke="#00C853" stroke-opacity="0.7" stroke-width="1.8"/>
<!-- Dollar sign on coin -->
<text x="-4" y="6" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="13" fill="#00C853" fill-opacity="0.9" font-weight="700">$</text>
<!-- Check badge -->
<circle cx="10" cy="-8" r="8" fill="#00C853" fill-opacity="0.85" stroke="white" stroke-width="1.5"/>
<polyline points="6,-8 9,-4 15,-12" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<text x="680" y="133" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="13" fill="#3a3a3a" font-weight="600">Get paid</text>
<text x="680" y="148" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="11" fill="#6B6B6B">weekly</text>
</svg>
</div>
<div class="steps-grid">
<article class="step-card drill-card" onclick="openDrill('how',1)" role="button" tabindex="0">
<div class="step-number">1</div>
<h3>Register</h3>
<p>Submit your phone number. You will be notified before the platform opens in your region.</p>
<p class="drill-cta">What happens next <span class="drill-arrow"></span></p>
<div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div></div>
</article>
<article class="step-card drill-card" onclick="openDrill('how',2)" role="button" tabindex="0">
<div class="step-number">2</div>
<h3>Select a mission</h3>
<p>Browse active missions on the map. Each specifies location, required shots, and Credit reward.</p>
<p class="drill-cta">How missions work <span class="drill-arrow"></span></p>
<div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div></div>
</article>
<article class="step-card drill-card" onclick="openDrill('how',3)" role="button" tabindex="0">
<div class="step-number">3</div>
<h3>Receive payment</h3>
<p>Submit documentation. Upon approval, QX Credits are added to your balance. Once you reach the minimum threshold, your Credits are paid out weekly at the published conversion rate via <a class="gterm" data-gterm="Stripe Connect" href="/glossary#stripe-connect">Stripe Connect</a> to your bank account.</p>
<p class="drill-cta">Payment details <span class="drill-arrow"></span></p>
<div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div></div>
</article>
</div>
</div>
</section>
<!-- PERSONAS -->
<section id="personas" class="section-pad">
<div class="container">
<div class="section-header">
<h2>Who becomes a <a class="gterm" data-gterm="Zoomer" href="/glossary#zoomer">Zoomer</a>?</h2>
<p>Anyone with a smartphone and time in their area. Click to see full profiles.</p>
</div>
<div class="personas-grid">
<article class="persona-card drill-card" onclick="openDrill('persona','student')" role="button" tabindex="0">
<div class="persona-avatar">ML</div>
<p class="persona-name">Student, 22</p>
<p class="persona-text">Three to four missions per week, between lectures. No fixed schedule. Supplementary income without affecting studies.</p>
<p class="drill-cta">Full profile <span class="drill-arrow"></span></p>
<div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div></div>
</article>
<article class="persona-card drill-card" onclick="openDrill('persona','driver')" role="button" tabindex="0">
<div class="persona-avatar">DP</div>
<p class="persona-name">Delivery driver, 34</p>
<p class="persona-text">Adds missions along existing routes. No extra travel. The missions are already on the way.</p>
<p class="drill-cta">Full profile <span class="drill-arrow"></span></p>
<div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div></div>
</article>
<article class="persona-card drill-card" onclick="openDrill('persona','retired')" role="button" tabindex="0">
<div class="persona-avatar">EH</div>
<p class="persona-name">Retired, 61</p>
<p class="persona-text">Daily walks through the neighbourhood. The same route, now with a purpose and a payment at the end of it.</p>
<p class="drill-cta">Full profile <span class="drill-arrow"></span></p>
<div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div></div>
</article>
</div>
</div>
</section>
<!-- MISSION TYPES -->
<section id="missions" class="section-pad" style="background:var(--bg-off)">
<div class="container">
<div class="section-header">
<h2>Mission categories</h2>
<p>Each mission specifies scope, required documentation and payment. Click any category to go deep.</p>
</div>
<div class="missions-grid">
<article class="mission-card drill-card" onclick="openDrill('mission','infrastructure')" role="button" tabindex="0">
<div class="mission-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18"/><path d="M3 18l4-7 4 3 4-5 4 9"/><path d="M7 11V7"/><path d="M17 9v-2"/></svg></div>
<h3>Infrastructure</h3>
<p>Bridges, roads, utility lines, foundations. Systematic documentation for public and private operators.</p>
<span class="mission-chip">Municipalities &amp; government</span>
<p class="drill-cta">Deep dive <span class="drill-arrow"></span></p>
<div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div></div>
</article>
<article class="mission-card drill-card" onclick="openDrill('mission','realestate')" role="button" tabindex="0">
<div class="mission-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V9l9-7 9 7v12"/><path d="M9 21v-6h6v6"/></svg></div>
<h3>Real estate</h3>
<p>Facades, roofs, entrances, parking lots. Visual inspection for property portfolios.</p>
<span class="mission-chip">Property &amp; insurance</span>
<p class="drill-cta">Deep dive <span class="drill-arrow"></span></p>
<div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div></div>
</article>
<article class="mission-card drill-card" onclick="openDrill('mission','coastline')" role="button" tabindex="0">
<div class="mission-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 17c2-1 4-2 6 0s4 1 6 0 4-1 6 0"/><path d="M3 13c2-1 4-2 6 0s4 1 6 0 4-1 6 0"/><path d="M12 3v6"/><path d="M9 6l3-3 3 3"/></svg></div>
<h3>Coastlines &amp; nature</h3>
<p>Coasts, beaches, waterways, natural environments. Monitoring and change detection at scale.</p>
<span class="mission-chip">Maritime &amp; environmental</span>
<p class="drill-cta">Deep dive <span class="drill-arrow"></span></p>
<div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div></div>
</article>
<article class="mission-card drill-card" onclick="openDrill('mission','incident')" role="button" tabindex="0">
<div class="mission-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg></div>
<h3>Incident &amp; damage</h3>
<p>Storm damage, flooding, vandalism. Fast-turnaround documentation for time-sensitive insurance claims.</p>
<span class="mission-chip">Insurance — fastest pay</span>
<p class="drill-cta">Deep dive <span class="drill-arrow"></span></p>
<div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div></div>
</article>
<article class="mission-card drill-card" onclick="openDrill('mission','verification')" role="button" tabindex="0">
<div class="mission-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 12l2 2 4-4"/><path d="M21 12c0 4.97-4.03 9-9 9s-9-4.03-9-9 4.03-9 9-9"/></svg></div>
<h3>Verification</h3>
<p>Confirm assets exist and look as expected. Addresses, storefronts, delivery locations and registered entities.</p>
<span class="mission-chip">E-commerce, banks &amp; agencies</span>
<p class="drill-cta">Deep dive <span class="drill-arrow"></span></p>
<div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div></div>
</article>
<article class="mission-card drill-card" onclick="openDrill('mission','urban')" role="button" tabindex="0">
<div class="mission-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="7" height="14" rx="1"/><rect x="9" y="3" width="7" height="18" rx="1"/><rect x="16" y="9" width="7" height="12" rx="1"/><path d="M2 21h20"/></svg></div>
<h3>Urban &amp; streets</h3>
<p>Street conditions, signage, accessibility, retail presence. Intelligence for city planners and retail chains.</p>
<span class="mission-chip">Municipalities &amp; retail</span>
<p class="drill-cta">Deep dive <span class="drill-arrow"></span></p>
<div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div><div class="drill-dot on"></div></div>
</article>
</div>
</div>
</section>
<!-- EARNINGS -->
<section id="earnings" class="section-pad">
<div class="container">
<div class="section-header">
<h2>Compensation structure</h2>
<p>QX Credits per approved mission. No hourly rate. No schedule. No minimum commitment. Click any <a class="gterm" data-gterm="tier" href="/glossary#tier">tier</a> to learn more.</p>
</div>
<div class="earnings-grid">
<article class="tier-card drill-card" onclick="openDrill('tier','supplementary')" role="button" tabindex="0">
<div class="tier-ill"><svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="24" cy="10" r="5" stroke="#0066FF" stroke-width="2"/><path d="M17 22c0-3.866 3.134-7 7-7s7 3.134 7 7" stroke="#0066FF" stroke-width="2" stroke-linecap="round"/><path d="M20 30l-3 8M28 30l3 8" stroke="#0066FF" stroke-width="2" stroke-linecap="round"/><path d="M17 38l4-8 3 4 3-4 4 8" stroke="#0066FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<h3>Supplementary</h3>
<p class="tier-missions">24 missions per week</p>
<p class="tier-earnings" style="font-size:1.375rem;font-weight:900;color:var(--blue);letter-spacing:-.03em;margin-bottom:16px">60480 Credits<span style="font-size:.875rem;font-weight:500;color:var(--text-muted)"> / week</span></p>
<ul class="tier-features"><li>Supplementary income alongside existing commitments</li><li>No minimum hours or availability required</li><li>Access to all standard mission categories</li></ul>
<p class="drill-cta" style="justify-content:center">See full details <span class="drill-arrow"></span></p>
</article>
<article class="tier-card featured drill-card" onclick="openDrill('tier','active')" role="button" tabindex="0">
<div class="tier-ill"><svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="20" cy="10" r="5" stroke="#fff" stroke-width="2"/><path d="M13 22c0-3.866 3.134-7 7-7s7 3.134 7 7" stroke="#fff" stroke-width="2" stroke-linecap="round"/><path d="M16 30l-3 8M24 30l3 8" stroke="#fff" stroke-width="2" stroke-linecap="round"/><circle cx="36" cy="14" r="8" fill="#0066FF"/><path d="M32 14l2.5 2.5L39 11" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<h3>Active</h3>
<p class="tier-missions">812 missions per week</p>
<p class="tier-earnings" style="font-size:1.375rem;font-weight:900;color:var(--blue);letter-spacing:-.03em;margin-bottom:16px">2401,440 Credits<span style="font-size:.875rem;font-weight:500;color:rgba(10,10,10,.75)"> / week</span></p>
<ul class="tier-features"><li>Consistent volume with predictable payment cadence</li><li>Builds platform rating, unlocking higher-value assignments</li><li>Priority access to restricted mission types</li></ul>
<p class="drill-cta" style="justify-content:center;color:rgba(10,10,10,.7)">See full details <span class="drill-arrow"></span></p>
</article>
<article class="tier-card drill-card" onclick="openDrill('tier','professional')" role="button" tabindex="0">
<div class="tier-ill"><svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="24" cy="9" r="5" stroke="#0066FF" stroke-width="2"/><path d="M17 21c0-3.866 3.134-7 7-7s7 3.134 7 7" stroke="#0066FF" stroke-width="2" stroke-linecap="round"/><path d="M20 29l-3 9M28 29l3 9" stroke="#0066FF" stroke-width="2" stroke-linecap="round"/><path d="M24 24l1.5 3 3.5.5-2.5 2.5.5 3.5L24 32l-3 1.5.5-3.5L19 27.5l3.5-.5z" fill="#0066FF" opacity="0.6"/></svg></div>
<h3>Professional</h3>
<p class="tier-missions">15+ missions per week</p>
<p class="tier-earnings" style="font-size:1.375rem;font-weight:900;color:var(--blue);letter-spacing:-.03em;margin-bottom:16px">4502,400+ Credits<span style="font-size:.875rem;font-weight:500;color:var(--text-muted)"> / week</span></p>
<ul class="tier-features"><li>Maximum assignment volume and compensation</li><li>First access to premium and restricted assignments</li><li>Performance-based compensation adjustments</li></ul>
<p class="drill-cta" style="justify-content:center">See full details <span class="drill-arrow"></span></p>
</article>
<div class="qx-calc" id="qx-calc">
<h3>Estimate your earnings</h3>
<p class="qx-calc-sub">Adjust missions per week and average reward</p>
<div class="qx-calc-row">
<span class="qx-calc-label">Missions / week</span>
<input type="range" min="1" max="25" value="5" id="calc-missions" oninput="qxCalcUpdate()"><!-- max extended to 25 -->
<span class="qx-calc-val" id="calc-missions-val">5</span>
</div>
<div class="qx-calc-row">
<span class="qx-calc-label">Avg. reward</span>
<input type="range" min="4" max="30" value="9" id="calc-reward" oninput="qxCalcUpdate()">
<span class="qx-calc-val" id="calc-reward-val">9</span>
</div>
<div class="qx-calc-result">
<div>
<div class="qx-calc-result-label">Estimated monthly earnings</div>
<div class="qx-calc-result-period">Based on 4.3 weeks/month · before tax</div>
</div>
<div>
<div class="qx-calc-result-amount" id="calc-output">195</div>
</div>
</div>
</div>
<script>
function qxCalcUpdate(){
var m=parseInt(document.getElementById('calc-missions').value);
var r=parseInt(document.getElementById('calc-reward').value);
var loc=window.QX_CURRENT_LOCALE||{symbol:'€',currency:'EUR'};
var monthly=Math.round(m*r*4.3);
document.getElementById('calc-missions-val').textContent=m;
document.getElementById('calc-reward-val').textContent=loc.symbol+r;
document.getElementById('calc-output').textContent=loc.symbol+monthly;
}
document.addEventListener('qx:market',function(){qxCalcUpdate();});
</script>
</div>
<details class="payment-accordion">
<summary>Payment and taxation</summary>
<div class="payment-accordion-body">
<ul>
<li><span class="dot"></span>QX Credits are added to your balance on mission approval. Balances above the minimum payout threshold are transferred weekly via <a class="gterm" data-gterm="Stripe Connect" href="/glossary#stripe-connect">Stripe Connect</a> to your registered bank account.</li>
<li><span class="dot"></span>quiXzoom charges no commission or platform fee to <a class="gterm" data-gterm="Zoomer" href="/glossary#zoomer">Zoomers</a>. You receive 100% of your earned Credits. Credits never expire.</li>
<li><span class="dot"></span>QX Credits converted to cash upon payout. Tax treatment varies by country and jurisdiction. You are solely responsible for complying with applicable tax and reporting obligations in your place of residence. quiXzoom does not provide tax advice.</li>
</ul>
</div>
</details>
</div>
</section>
<!-- FOR BUSINESSES -->
<section id="businesses" class="section-pad">
<div class="container">
<div class="businesses-header">
<h2>For organisations</h2>
<p>On-demand access to a verified network of field data collectors. Scope is defined by you. Execution is distributed.</p>
</div>
<div class="biz-grid">
<a class="biz-card" href="/for-organisations/" style="display:block;text-decoration:none;color:inherit;cursor:pointer"><div class="biz-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg></div><h3>Geographic reach</h3><p>Launching in Sweden August 2026, with Netherlands, Thailand, Nigeria and Kenya to follow. No local staffing or coordination infrastructure required.</p><span class="biz-drill">Learn more →</span></a>
<a class="biz-card" href="/for-organisations/#quality" style="display:block;text-decoration:none;color:inherit;cursor:pointer"><div class="biz-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg></div><h3>Quality assurance</h3><p>Every submission undergoes automated AI review for completeness, technical quality and specification compliance.</p><span class="biz-drill">Learn more →</span></a>
<a class="biz-card" href="/for-organisations/#deployment" style="display:block;text-decoration:none;color:inherit;cursor:pointer"><div class="biz-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg></div><h3>Rapid deployment</h3><p>Mission campaigns configured and deployed within hours. Define scope, requirements and compensation — network activates automatically.</p><span class="biz-drill">Learn more →</span></a>
</div>
<div class="biz-cta"><a href="/for-organisations/" class="btn-ghost">Explore for organisations →</a></div>
</div>
</section>
<!-- COMMUNITY -->
<section id="community" class="section-pad" style="background:var(--bg-off)">
<div class="container">
<div class="section-header">
<h2>The World's Observation Network</h2>
<p>quiXzoom Community launches alongside the platform. Zoomers will share field observations, local knowledge and discovery reports — building a continuously updated layer of real-world intelligence. <em style="color:var(--text-muted)">Opening 2026.</em></p>
</div>
<div class="community-grid">
<div class="community-card" style="cursor:pointer" onclick="document.getElementById('wl-email').focus()">
<div class="community-tag">Field Reports</div>
<h3>What's changing out there</h3>
<p>Real observations from the field. Zoomers report what they see — infrastructure, urban change, damage, new construction.</p>
<ul class="field-report-list">
<li><em style="color:var(--text-muted)">(Sample)</em> Road construction project emerging in Milan</li>
<li><em style="color:var(--text-muted)">(Sample)</em> New commercial district in Bangkok</li>
<li><em style="color:var(--text-muted)">(Sample)</em> Abandoned industrial site in Hamburg</li>
<li><em style="color:var(--text-muted)">(Sample)</em> Bridge inspection underway in Stockholm</li>
</ul>
</div>
<div class="community-card" style="cursor:pointer" onclick="document.getElementById('wl-email').focus()">
<div class="community-tag">Zoomer Academy</div>
<h3>Sharpen your fieldwork</h3>
<p>Guides, techniques and standards for producing high-quality, high-approval submissions.</p>
<ul class="field-report-list">
<li>Photo technique for difficult lighting</li>
<li>GPS accuracy and metadata</li>
<li>Verification best practices</li>
<li>Field safety and access rights</li>
</ul>
</div>
<div class="community-card" style="cursor:pointer" onclick="document.getElementById('wl-email').focus()">
<div class="community-tag">Regional Communities</div>
<h3>Local knowledge, global reach</h3>
<p>Every region has its own community. Posts are automatically translated — a Zoomer in Italy and a Zoomer in Sweden read the same thread in their own language.</p>
<ul class="field-report-list">
<li>Sweden</li>
<li>Germany</li>
<li>Italy</li>
<li>Thailand</li>
<li>United States</li>
</ul>
</div>
</div>
<div class="community-pipeline">
<div class="community-pipeline-text">
<h3>Community generates missions</h3>
<p>When Zoomers discuss an area or report changes, the system detects patterns and automatically generates new missions. Community activity becomes field intelligence — and field intelligence becomes work.</p>
</div>
<div class="community-pipeline-badge">Field observation → Mission → Verified data</div>
</div>
</div>
</section>
<!-- RECOGNITION -->
<section id="recognition" class="section-pad">
<div class="container">
<div class="section-header">
<h2>Recognition that means something</h2>
<p>Every milestone is acknowledged — not with a badge, but with a message that answers: why does my contribution matter? <a href="/earn/" style="color:var(--blue);font-weight:600">See the full progression →</a></p>
</div>
<div class="community-pipeline">
<div class="community-pipeline-text">
<h3>10 milestones. No points. No levels.</h3>
<p>From First Steps at 10 missions to World Builder at 10,000 — each milestone is a genuine acknowledgement of real-world contribution. Your Credits never expire. Your recognition never disappears.</p>
</div>
<div class="community-pipeline-badge">10 → 25 → 50 → 100 → 250 → 500 → 1K → 2.5K → 5K → 10K</div>
</div>
</div>
</section>
<!-- FAQ -->
<section id="faq" class="section-pad">
<div class="container">
<div class="section-header"><h2>Frequently asked questions</h2><p>Click any question for a detailed answer. Use "More detail" for the full 7-layer breakdown.</p></div>
<div class="faq-list">
<details class="faq-item"><summary>Do I need a professional camera?</summary><div class="faq-answer">No. iPhone 12 or later, or any Android with 12MP or better, is all you need. Most Zoomers use the phone they already own.<br><span class="faq-drill-hint" onclick="event.stopPropagation();openDrill('faq','camera')">Full equipment guide → <span></span></span></div></details>
<details class="faq-item"><summary>How and when do I get paid?</summary><div class="faq-answer">QX Credits are added to your balance when each mission is approved. Once you reach the payout threshold, funds transfer weekly via Stripe Connect. One-time identity verification required.<br><span class="faq-drill-hint" onclick="event.stopPropagation();openDrill('faq','payment')">Full payment breakdown → <span></span></span></div></details>
<details class="faq-item"><summary>What missions are available in my area?</summary><div class="faq-answer">Sign up to be notified. Live missions browsable on the map from August 1, 2026. Mission density grows over time with Zoomer network.<br><span class="faq-drill-hint" onclick="event.stopPropagation();openDrill('faq','availability')">Coverage map and launch timeline → <span></span></span></div></details>
<details class="faq-item"><summary>Is this taxable income?</summary><div class="faq-answer">The conversion of QX Credits into fiat currency may have tax consequences depending on your country and individual circumstances. You are solely responsible for understanding and complying with applicable laws. quiXzoom does not provide tax advice and does not withhold taxes on your behalf. Consult a qualified tax advisor if you are uncertain.<br><span class="faq-drill-hint" onclick="event.stopPropagation();openDrill('faq','tax')">Tax guidance by country → <span></span></span></div></details>
<details class="faq-item"><summary>How quickly is my photo approved?</summary><div class="faq-answer">Most submissions reviewed within a few hours by AI. If not approved, you see the specific reason and can supplement or appeal within 48h.<br><span class="faq-drill-hint" onclick="event.stopPropagation();openDrill('faq','approval')">Full review process breakdown → <span></span></span></div></details>
<details class="faq-item"><summary>Can I work part-time?</summary><div class="faq-answer">Absolutely. No minimum hours, no fixed schedule, no commitment. Work when you want. Many Zoomers do one or two missions on their lunch break.<br><span class="faq-drill-hint" onclick="event.stopPropagation();openDrill('faq','parttime')">Earnings scenarios and scheduling → <span></span></span></div></details>
</div>
</div>
</section>
<!-- CTA BOTTOM -->
<section id="cta-bottom" class="section-pad">
<div class="container">
<div class="section-header"><h2>Ready to start?</h2><p>Registration is open. No platform fee. Credits never expire.</p></div>
<div style="max-width:680px;margin:0 auto">
<form id="waitlist-form-bottom" onsubmit="submitWaitlist(event,'bottom')" novalidate>
<div class="wl-row">
<input type="tel" id="wl-phone-bottom" name="phone" class="wl-input" placeholder="Your phone number" required autocomplete="tel">
<button type="submit" id="wl-btn-bottom" class="wl-submit">Register as a Zoomer →</button>
</a>
<a href="/ja/" class="language-card">
<div class="lang-flag"></div>
<div class="lang-info">
<h3></h3>
<p>Japanese — </p>
</div>
<div id="wl-ok-bottom" class="wl-ok" hidden>✓ You're on the list! We'll be in touch before launch.</div>
<p id="wl-err-bottom" class="wl-err" hidden>Something went wrong. Please try again.</p>
</form>
<p class="wl-trust">No signup fee · No platform fee · Credits never expire · Launching Sweden Aug 2026</p>
<div class="referral-hint" style="margin-top:18px;padding:14px 20px;background:rgba(10,10,10,.06);border:1px solid rgba(10,10,10,.12);border-radius:12px;font-size:.9rem;color:rgba(10,10,10,.8)">
<p><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/></svg> Refer a friend — both of you get 50 bonus Credits at launch</p>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="footer-top">
<div class="footer-grid">
<!-- Brand col -->
<div class="footer-brand-col">
<a href="/" class="footer-logo">qui<span class="x">X</span>zoom</a>
<p class="footer-desc">Crowdsourced field data collection. Zoomers photograph infrastructure, real estate and urban environments on demand.</p>
<div class="footer-social-row">
<a href="https://www.linkedin.com/company/quixzoom" class="footer-soc-btn" aria-label="LinkedIn" target="_blank" rel="noopener">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
</a>
<a href="https://twitter.com/quixzoom" class="footer-soc-btn" aria-label="X / Twitter" target="_blank" rel="noopener">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.744l7.73-8.835L1.254 2.25H8.08l4.253 5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</a>
<a href="https://www.instagram.com/quixzoom" class="footer-soc-btn" aria-label="Instagram" target="_blank" rel="noopener">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>
</a>
</a>
<a href="/ko/" class="language-card">
<div class="lang-flag"></div>
<div class="lang-info">
<h3></h3>
<p>Korean — </p>
</div>
<div class="footer-trust">
<div class="trust-badge"><span class="tb-icon"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span>GDPR compliant</div>
<div class="trust-badge"><span class="tb-icon"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10z"/></svg></span>EU data residency</div>
<div class="trust-badge"><span class="tb-icon"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></span>Stripe payouts</div>
</a>
<a href="/th/" class="language-card">
<div class="lang-flag"></div>
<div class="lang-info">
<h3>ไทย</h3>
<p>Thai — ประเทศไทย</p>
</div>
</div>
<!-- Platform -->
<div>
<div class="footer-col-title">Platform</div>
<ul class="footer-col-links">
<li><a href="#how">How it works</a></li>
<li><a href="#missions">Mission types</a></li>
<li><a href="#earnings">Zoomer earnings</a></li>
<li><a href="/markets/se/">Sverige launch <span class="badge-new">Aug 2026</span></a></li>
<li><a href="/markets/">All markets</a></li>
<li><a href="https://app.quixzoom.com/join">Join as Zoomer</a></li>
</ul>
</div>
<!-- Solutions -->
<div>
<div class="footer-col-title">Solutions</div>
<ul class="footer-col-links">
<li><a href="/for-organisations/">For organisations</a></li>
<li><a href="/for-organisations/#use-cases">Use cases</a></li>
<li><a href="/for-organisations/#enterprise">Enterprise</a></li>
<li><a href="/pricing">Pricing</a></li>
<li><a href="/for-organisations/#contact">Request a demo</a></li>
</ul>
</div>
<!-- Developers -->
<div>
<div class="footer-col-title">Developers</div>
<ul class="footer-col-links">
<li><a href="/developer">API overview</a></li>
<li><a href="/developer/#quickstart">Quick start</a></li>
<li><a href="/developer/openapi.json">OpenAPI spec</a></li>
<li><a href="/changelog">Changelog</a></li>
<li><a href="/status">System status</a></li>
</ul>
</div>
<!-- Resources -->
<div>
<div class="footer-col-title">Resources</div>
<ul class="footer-col-links">
<li><a href="/academy/">Zoomer Academy</a></li>
<li><a href="/resources/">Resources</a></li>
<li><a href="/glossary">Glossary</a></li>
<li><a href="/security">Security & trust</a></li>
<li><a href="/careers/">Careers</a></li>
<li><a href="/about">About quiXzoom</a></li>
<li><a href="/support">Support centre</a></li>
<li><a href="/reviews">Reviews</a></li>
</ul>
</div>
<!-- Legal -->
<div>
<div class="footer-col-title">Legal</div>
<ul class="footer-col-links">
<li><a href="/privacy/">Privacy policy</a></li>
<li><a href="/terms/">Terms of service</a></li>
<li><a href="/terms#dac7">DAC7 reporting</a></li>
<li><a href="/privacy#gdpr">GDPR rights</a></li>
<li><a href="/security#responsible">Responsible disclosure</a></li>
</ul>
</div>
</a>
<a href="/vi/" class="language-card">
<div class="lang-flag"></div>
<div class="lang-info">
<h3>Tiếng Việt</h3>
<p>Vietnamese — Việt Nam</p>
</div>
</a>
<a href="/id/" class="language-card">
<div class="lang-flag"></div>
<div class="lang-info">
<h3>Bahasa Indonesia</h3>
<p>Indonesian — Indonesia</p>
</div>
</a>
<a href="/ms/" class="language-card">
<div class="lang-flag"></div>
<div class="lang-info">
<h3>Bahasa Melayu</h3>
<p>Malay — Malaysia/Singapore</p>
</div>
</a>
<a href="/hi/" class="language-card">
<div class="lang-flag"></div>
<div class="lang-info">
<h3>हिन्दी</h3>
<p>Hindi — भारत</p>
</div>
</a>
</section>
<div class="region-note">
<p>quiXzoom Asia covers: China, Japan, South Korea, Thailand, Vietnam, Indonesia, Malaysia, Singapore, India</p>
<p>Payments available in: CNY, JPY, KRW, THB, VND, IDR, MYR, INR</p>
</div>
</div><!-- /footer-top -->
<div style="border-bottom:1px solid rgba(10,10,10,.07)">
<div class="footer-bottom" style="padding-top:16px;padding-bottom:16px">
<div class="footer-lang-row">
<span class="fl-lbl"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10z"/></svg></span>
<a href="https://www.quixzoom.com/" class="flang active">EN</a>
<a href="/markets/se/" class="flang">🇸🇪 Sverige</a>
<a href="/markets/nl/" class="flang">🇳🇱 NL</a>
<a href="/markets/th/" class="flang">🇹🇭 TH</a>
<a href="/markets/ng/" class="flang">🇳🇬 NG</a>
<a href="/markets/ke/" class="flang">🇰🇪 KE</a>
</div>
</div>
<footer>
<p>quiXzoom Asia | asia@quixzoom.com</p>
<p>Part of the quiXzoom Global Network</p>
</footer>
</div>
<div style="border-top:1px solid rgba(10,10,10,.07);padding:20px 32px;max-width:1200px;margin:0 auto">
<p style="font-size:.8rem;color:rgba(10,10,10,.55);line-height:1.6">Data delivered to <a href="https://www.landvex.com" style="color:rgba(10,10,10,.7);text-decoration:underline">Landvex</a> — powering decision intelligence for governments and enterprise. · Credits are redeemable as cash — 1 Credit = €1 · Minimum payout 250 Credits (€250).</p>
</div>
<div class="footer-bottom">
<p class="footer-copy">© 2026 quiXzoom · Operated by Landvex AB</p>
<div class="footer-legal-links">
<a href="/privacy/">Privacy</a>
<a href="/terms/">Terms</a>
<a href="/security">Security</a>
<a href="/support">Support</a>
</div>
</div>
</footer>
<!-- OVERLAY -->
<div class="overlay-backdrop" id="overlay-backdrop" onclick="closeOverlay()"></div>
<div class="overlay-panel" id="overlay-panel" role="dialog" aria-modal="true" aria-labelledby="overlay-title">
<div class="overlay-header">
<button class="overlay-close" onclick="closeOverlay()" aria-label="Close"></button>
<p class="overlay-breadcrumb">quiXzoom / <span id="overlay-breadcrumb-sector"></span></p>
<p class="overlay-type" id="overlay-type-label"></p>
<h2 class="overlay-title" id="overlay-title"></h2>
<div class="layer-nav-wrap"><nav class="layer-nav" id="layer-nav"></nav></div>
</div>
<div class="overlay-body" id="overlay-body"></div>
</div>
<script>
'use strict';
/* ─────────────────────────────────────────────
DRILL DATA — 7 LAYERS PER TOPIC
───────────────────────────────────────────── */
const DATA = {
how: {
1: {
type: 'Step 1', title: 'Register & get started',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">Signing up takes under 60 seconds. You submit your email, choose your region, and join the waitlist. We notify you when missions go live in your area.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">60s</div><div class="layer-kpi-lbl">Signup time</div></div><div class="layer-kpi"><div class="layer-kpi-val">Free</div><div class="layer-kpi-lbl">No cost ever</div></div><div class="layer-kpi"><div class="layer-kpi-val">Aug 2026</div><div class="layer-kpi-lbl">First missions live</div></div></div>` },
{ tab: '2 · Identity verification', html: `<p class="layer-lead">Before your first payout, you complete a one-time identity verification. This is a legal requirement for payment platforms and takes 35 minutes.</p><ul class="layer-ul"><li>Government-issued photo ID (passport, national ID or driving licence)</li><li>Selfie for liveness check</li><li>Processed by our KYC provider — not stored by quiXzoom</li><li>Verification typically approved within 1 hour</li><li>Required once — never again unless your details change</li></ul><p class="layer-h3">Why it's required</p><p class="layer-p">Anti-money-laundering regulation requires payment platforms to verify the identity of anyone receiving funds. This is the same process used by Stripe, PayPal and all regulated payment services.</p>` },
{ tab: '3 · App setup', html: `<p class="layer-lead">Once verified, you set up your Zoomer profile and link your bank account for payouts.</p><div class="layer-step-list"><div class="layer-step"><div class="layer-step-num">1</div><div class="layer-step-body"><p class="layer-step-title">Download the app</p><p class="layer-step-desc">Available on iOS (App Store) and Android (Google Play) at launch.</p></div></div><div class="layer-step"><div class="layer-step-num">2</div><div class="layer-step-body"><p class="layer-step-title">Set your zone</p><p class="layer-step-desc">Define the geographic radius you are willing to travel. Missions within this zone appear on your map.</p></div></div><div class="layer-step"><div class="layer-step-num">3</div><div class="layer-step-body"><p class="layer-step-title">Link your bank account</p><p class="layer-step-desc">Via Stripe Connect. Takes 2 minutes. Your bank details are held by Stripe — not quiXzoom.</p></div></div><div class="layer-step"><div class="layer-step-num">4</div><div class="layer-step-body"><p class="layer-step-title">You're live</p><p class="layer-step-desc">Missions in your zone are immediately visible. Claim one and start.</p></div></div></div>` },
{ tab: '4 · Equipment', html: `<p class="layer-lead">No specialist equipment is needed. Your smartphone camera is sufficient for all standard mission types.</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Device</th><th>Requirement</th><th>Status</th></tr><tr><td>iPhone</td><td>iPhone 12 or later</td><td><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><polyline points="20 6 9 17 4 12"/></svg> All standard missions</td></tr><tr><td>Android</td><td>12MP rear camera or better</td><td><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><polyline points="20 6 9 17 4 12"/></svg> All standard missions</td></tr><tr><td>Dedicated camera</td><td>12MP+, GPS-capable or manual location</td><td><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><polyline points="20 6 9 17 4 12"/></svg> With GPS workaround</td></tr><tr><td>Drone</td><td>Supported for aerial missions (future)</td><td>Coming 2026 Q4</td></tr></table></div><p class="layer-h3">Recommended settings</p><ul class="layer-ul"><li>Location services: always on while using the app</li><li>Camera: maximum resolution, no digital zoom</li><li>Storage: 2GB free minimum</li></ul>` },
{ tab: '5 · Your profile', html: `<p class="layer-lead">Your Zoomer profile determines which missions you can access and your compensation tier.</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Profile element</th><th>Impact</th></tr><tr><td>Identity verified</td><td>Required for any payout</td></tr><tr><td>Approval rate</td><td>Higher approval rate = access to premium missions</td></tr><tr><td>Mission count</td><td>Unlocks tier upgrades (Active, Professional)</td></tr><tr><td>Rating</td><td>High-rated Zoomers get priority assignment on high-value missions</td></tr><tr><td>Specialisations</td><td>Complete training modules to unlock mission types (e.g. maritime, bridge inspection)</td></tr></table></div>` },
{ tab: '6 · Privacy', html: `<p class="layer-lead">Your personal data is protected under GDPR and applicable local data protection law.</p><ul class="layer-ul"><li>Your name and contact details are never shared with mission orderers</li><li>Submissions are delivered to orderers without Zoomer identification</li><li>GPS data is used for mission verification only and not retained after approval</li><li>Anonymised mission observations are licensed by quiXzoom to Landvex for intelligence products under a separate data processing agreement; your identity is never disclosed</li><li>Identity verification data held by KYC provider, not quiXzoom</li><li>Right to erasure: you can delete your account and all associated data at any time</li></ul><p class="layer-h3">Data residency</p><p class="layer-p">All quiXzoom data is stored in EU (Stockholm, eu-north-1). Your data never leaves the European Economic Area.</p>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Join the waitlist now</p><p class="layer-cta-sub">Registration is free. We notify you when missions go live in your area. No commitment required.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Register as a Zoomer →</a></div>` }
]
},
2: {
type: 'Step 2', title: 'Selecting and claiming a mission',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">Missions are displayed on a map as markers. Each marker shows the mission type, distance from you, estimated time, and compensation. You claim the missions you want — no assignment from quiXzoom.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">~30 min</div><div class="layer-kpi-lbl">Average mission</div></div><div class="layer-kpi"><div class="layer-kpi-val">€330</div><div class="layer-kpi-lbl">Typical range per mission</div></div><div class="layer-kpi"><div class="layer-kpi-val">Your choice</div><div class="layer-kpi-lbl">No forced assignments</div></div></div>` },
{ tab: '2 · The mission brief', html: `<p class="layer-lead">Every mission has a brief that tells you exactly what is required before you claim it. There are no surprises.</p><p class="layer-h3">What the brief contains</p><ul class="layer-ul"><li>Location — exact address or area, shown on map with navigation link</li><li>Documentation type — which angles, elements and coverage are required</li><li>Example images — visual reference for each required shot</li><li>Time window — how long you have to complete and submit</li><li>Compensation — the exact amount you receive on approval</li><li>Difficulty rating — estimated skill level and time required</li><li>Access notes — any known access restrictions or safety guidance</li></ul>` },
{ tab: '3 · Claiming', html: `<p class="layer-lead">Claiming reserves the mission for you within a defined time window. Unclaimed missions are visible to all Zoomers in range simultaneously.</p><div class="layer-step-list"><div class="layer-step"><div class="layer-step-num">1</div><div class="layer-step-body"><p class="layer-step-title">Browse the map</p><p class="layer-step-desc">Missions in your zone appear as markers. Filter by type, compensation or distance.</p></div></div><div class="layer-step"><div class="layer-step-num">2</div><div class="layer-step-body"><p class="layer-step-title">Read the brief</p><p class="layer-step-desc">Review requirements, example images and compensation before claiming. No obligation.</p></div></div><div class="layer-step"><div class="layer-step-num">3</div><div class="layer-step-body"><p class="layer-step-title">Claim</p><p class="layer-step-desc">Claiming starts your time window. The mission is reserved for you. Other Zoomers cannot claim it during your window.</p></div></div><div class="layer-step"><div class="layer-step-num">4</div><div class="layer-step-body"><p class="layer-step-title">Navigate and document</p><p class="layer-step-desc">App guides you to location and shows required shots as you capture them.</p></div></div><div class="layer-step"><div class="layer-step-num">5</div><div class="layer-step-body"><p class="layer-step-title">Submit</p><p class="layer-step-desc">Submit within your time window. AI review begins immediately.</p></div></div></div>` },
{ tab: '4 · Competition', html: `<p class="layer-lead">Some missions allow multiple Zoomer submissions — the fastest and highest-quality earns most. Understanding competition dynamics helps you maximise earnings.</p><p class="layer-h3">Mission types by competition model</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Type</th><th>Competition model</th><th>Best strategy</th></tr><tr><td>Standard</td><td>First valid submission wins full fee</td><td>Speed matters — claim and go immediately</td></tr><tr><td>Multi-submission</td><td>Multiple submissions accepted, declining fee</td><td>Quality matters — take your time</td></tr><tr><td>Surge</td><td>Large event, many locations, flat fee</td><td>Volume strategy — complete as many as possible</td></tr><tr><td>Premium</td><td>Single submission, higher fee, quality threshold</td><td>Specialisation required — only for rated Zoomers</td></tr></table></div>` },
{ tab: '5 · Cancellation', html: `<p class="layer-lead">You can release a claimed mission if you cannot complete it. Do this before the time window expires to avoid a negative rating impact.</p><ul class="layer-ul"><li>Release within 15 minutes of claiming: no penalty</li><li>Release after 15 minutes: small rating penalty</li><li>Time window expires without submission: moderate rating penalty</li><li>Three consecutive expired claims: temporary claim restriction (24h)</li></ul><p class="layer-h3">Why this matters</p><p class="layer-p">When you claim a mission, other Zoomers cannot claim it. If you don't complete it, the orderer's timeline is affected. The penalty system protects mission reliability for everyone.</p>` },
{ tab: '6 · Tips', html: `<p class="layer-lead">Patterns observed from highest-earning Zoomers in beta testing.</p><ul class="layer-ul"><li>Plan routes around clusters of missions — cover 35 in a single trip</li><li>Claim infrastructure and real estate missions first — they pay most per minute of work</li><li>Monitor surge events — emergency insurance missions pay 25× standard rate</li><li>Complete your profile specialisations — access to premium missions is rating-gated</li><li>Consistent quality pays more than raw volume — approval rate affects tier and mission access</li><li>Morning and evening windows see highest mission turnover in urban areas</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Missions go live August 2026</p><p class="layer-cta-sub">Join the waitlist now. You'll be first to see missions when they activate in your area.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
},
3: {
type: 'Step 3', title: 'Payment and approval',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">Payment is automatic. Once your submission is approved, Stripe Connect transfers the mission amount directly to your linked bank account. No invoice. No waiting for a payment run.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">Automatic</div><div class="layer-kpi-lbl">No manual claim needed</div></div><div class="layer-kpi"><div class="layer-kpi-val">0%</div><div class="layer-kpi-lbl">Platform fee to Zoomers</div></div><div class="layer-kpi"><div class="layer-kpi-val">13 days</div><div class="layer-kpi-lbl">Bank transfer time (Stripe)</div></div></div>` },
{ tab: '2 · Review process', html: `<p class="layer-lead">Submissions are reviewed automatically by the quiXzoom AI engine within minutes of upload. No human reviewer required for standard missions.</p><div class="layer-step-list"><div class="layer-step"><div class="layer-step-num">1</div><div class="layer-step-body"><p class="layer-step-title">Upload</p><p class="layer-step-desc">Images and metadata uploaded via the app. Integrity hash generated at capture.</p></div></div><div class="layer-step"><div class="layer-step-num">2</div><div class="layer-step-body"><p class="layer-step-title">AI review (~30 seconds)</p><p class="layer-step-desc">Geo-compliance, image quality, specification coverage and anomaly detection. All automated.</p></div></div><div class="layer-step"><div class="layer-step-num">3</div><div class="layer-step-body"><p class="layer-step-title">Decision</p><p class="layer-step-desc">Approved: payment triggered. Rejected: reason shown, 48h to supplement or appeal. Borderline: human review queue.</p></div></div><div class="layer-step"><div class="layer-step-num">4</div><div class="layer-step-body"><p class="layer-step-title">Payment</p><p class="layer-step-desc">Stripe Connect transfer initiated immediately on approval. Paid out weekly via Stripe Connect to your registered bank account.</p></div></div></div>` },
{ tab: '3 · If rejected', html: `<p class="layer-lead">Rejection is not the end. You always see the specific reason and have 48 hours to respond.</p><p class="layer-h3">Common rejection reasons</p><ul class="layer-ul"><li>Required angle missing — re-shoot the specific angle and supplement</li><li>Image quality below threshold — retake in better lighting or stability</li><li>Geo-compliance failure — capture was outside the mission perimeter</li><li>Specification element not visible — obscured subject, re-shoot</li></ul><p class="layer-h3">Appeal process</p><p class="layer-p">If you believe the AI review was incorrect, you can flag it for human review. Human reviewers assess all flagged rejections within 24 hours. Successful appeals result in approval and payment, and improve the AI model.</p>` },
{ tab: '4 · Payout methods', html: `<p class="layer-lead">quiXzoom uses Stripe Connect for all Zoomer payouts. Stripe supports bank accounts in 30+ countries.</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Country</th><th>Payout method</th><th>Transfer time</th></tr><tr><td>Sweden, EU</td><td>SEPA bank transfer</td><td>12 business days</td></tr><tr><td>UK</td><td>Faster Payments / BACS</td><td>Next business day</td></tr><tr><td>US</td><td>ACH bank transfer</td><td>23 business days</td></tr><tr><td>Other</td><td>SWIFT / local rails</td><td>25 business days</td></tr></table></div><p class="layer-h3">Payout currencies</p><p class="layer-p">Payouts are issued in your local currency. Exchange rate conversion handled by Stripe at mid-market rate where applicable.</p>` },
{ tab: '5 · Earnings tracking', html: `<p class="layer-lead">Your earnings dashboard shows all completed missions, pending approvals, and payment history.</p><ul class="layer-ul"><li>Real-time mission status — pending review, approved, rejected</li><li>Earnings summary — daily, weekly, monthly totals</li><li>Payment history — each transfer with Stripe reference</li><li>Tax export — annual earnings statement downloadable as PDF or CSV for tax reporting</li><li>Tier progress — where you are on the Supplementary → Active → Professional path</li></ul>` },
{ tab: '6 · Tax & payouts', html: `<p class="layer-lead">QX Credits are converted to fiat currency when you request a payout. Tax treatment of that conversion varies by country and individual circumstances.</p><div style="background:#f7f7f5;border-radius:10px;padding:16px;margin:16px 0"><p style="font-weight:700;margin-bottom:8px">How payouts work</p><ol style="padding-left:20px;display:flex;flex-direction:column;gap:6px"><li>Complete missions → Credits added to balance</li><li>Reach minimum threshold (250 Credits)</li><li>Request payout → Credits converted at published rate</li><li>Funds transferred via Stripe Connect to your account</li></ol></div><p class="layer-h3">Your responsibilities</p><ul class="layer-ul"><li>You are solely responsible for determining and complying with any tax, reporting or regulatory obligations in your place of residence or tax domicile</li><li>The conversion of Credits to fiat currency may constitute taxable income in your jurisdiction</li><li>quiXzoom does not withhold taxes, provide tax advice, or determine your residency status</li><li>quiXzoom provides annual earnings statements (PDF + CSV) to assist with your own reporting</li></ul><p class="layer-h3">quiXzoom provides</p><ul class="layer-ul"><li>Annual earnings summary from January 15 each year</li><li>Transaction-level records for every approved mission</li><li>Stripe Connect payment records</li></ul><p class="layer-p"><em>Please consult a qualified tax advisor if you are uncertain about your obligations in your jurisdiction.</em></p>` }
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Start earning in August 2026</p><p class="layer-cta-sub">Register interest now. Platform fee to Zoomers: zero. You keep everything the mission pays.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
}
},
persona: {
student: {
type: 'Zoomer profile', title: 'Student · Part-time Zoomer',
layers: [
{ tab: '1 · Profile', html: `<p class="layer-lead">Students are the most active Zoomer demographic in beta testing. Flexible schedules, local knowledge, and smartphone-native — they fit the platform naturally.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">34</div><div class="layer-kpi-lbl">Missions per week</div></div><div class="layer-kpi"><div class="layer-kpi-val">€60120</div><div class="layer-kpi-lbl">Monthly earnings (est.)</div></div><div class="layer-kpi"><div class="layer-kpi-val">~2h</div><div class="layer-kpi-lbl">Time invested per week</div></div></div>` },
{ tab: '2 · Typical week', html: `<p class="layer-lead">A typical student Zoomer fits missions into their existing daily movement — to lectures, to the library, back home.</p><div class="layer-step-list"><div class="layer-step"><div class="layer-step-num">Mon</div><div class="layer-step-body"><p class="layer-step-title">Morning: 1 mission on the way to campus</p><p class="layer-step-desc">Infrastructure mission near the route. 20 minutes, €8. Submitted between lectures.</p></div></div><div class="layer-step"><div class="layer-step-num">Wed</div><div class="layer-step-body"><p class="layer-step-title">Lunchtime: 2 missions in town</p><p class="layer-step-desc">Two real estate missions in a nearby block. 40 minutes combined, €14 total.</p></div></div><div class="layer-step"><div class="layer-step-num">Fri</div><div class="layer-step-body"><p class="layer-step-title">Afternoon: 1 surge mission</p><p class="layer-step-desc">Insurance surge event posted — storm damage documentation nearby. Highest pay this week: €22.</p></div></div></div><p class="layer-h3">Total: ~€44 this week, ~2 hours invested</p>` },
{ tab: '3 · Equipment used', html: `<p class="layer-lead">iPhone 14 (standard lens). No accessories. No extra apps. Standard camera, standard settings.</p><ul class="layer-ul"><li>Photos shot at maximum native resolution</li><li>Location services always on while using quiXzoom</li><li>2GB free storage maintained</li><li>No tripod, no gimbal — all handheld</li></ul>` },
{ tab: '4 · Earnings breakdown', html: `<p class="layer-lead">Earnings vary significantly by mission type selected and regional availability.</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Mission type</th><th>Avg. compensation</th><th>Avg. time</th><th>€/hour equivalent</th></tr><tr><td>Infrastructure (road)</td><td>€5</td><td>15 min</td><td>€20/h</td></tr><tr><td>Real estate (façade)</td><td>€8</td><td>20 min</td><td>€24/h</td></tr><tr><td>Insurance (surge)</td><td>€18</td><td>25 min</td><td>€43/h</td></tr><tr><td>Verification</td><td>€4</td><td>10 min</td><td>€24/h</td></tr></table></div>` },
{ tab: '5 · Tax guide', html: `<p class="layer-lead">Tax rules depend on your country of residence. quiXzoom provides annual earnings statements in all required formats.</p><p id="qx-tax-note" style="margin:12px 0;padding:12px 16px;background:#f7f7f5;border-radius:8px;font-size:.875rem;">Select your market above to see local tax guidance.</p><p class="layer-p"><em>General information only — consult a qualified local tax advisor for your situation.</em></p>` },
{ tab: '6 · Rating building', html: `<p class="layer-lead">A high approval rate unlocks better missions. For students starting out, consistency matters more than volume.</p><ul class="layer-ul"><li>Read every brief carefully before claiming</li><li>Never claim if you can't complete within the time window</li><li>When in doubt about quality — retake, don't submit</li><li>Review rejection reasons immediately — patterns emerge quickly</li><li>3 months of consistent quality → Active tier → access to premium missions</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Join thousands of students on the waitlist</p><p class="layer-cta-sub">Register now. Missions go live August 2026. No commitment, no cost.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Register as a Zoomer →</a></div>` }
]
},
driver: {
type: 'Zoomer profile', title: 'Delivery driver · Route-integrated Zoomer',
layers: [
{ tab: '1 · Profile', html: `<p class="layer-lead">Delivery drivers and couriers are among the highest-earning Zoomer profiles. They pass through multiple mission zones every working day — and the marginal cost of completing a mission on route is nearly zero.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">58</div><div class="layer-kpi-lbl">Missions per week (on-route)</div></div><div class="layer-kpi"><div class="layer-kpi-val">€120280</div><div class="layer-kpi-lbl">Monthly earnings (est.)</div></div><div class="layer-kpi"><div class="layer-kpi-val">~3 min</div><div class="layer-kpi-lbl">Added time per on-route mission</div></div></div>` },
{ tab: '2 · Route integration', html: `<p class="layer-lead">The key insight: missions that are already on your route take near-zero additional time. The mission is there; you're passing it anyway.</p><div class="layer-step-list"><div class="layer-step"><div class="layer-step-num">1</div><div class="layer-step-body"><p class="layer-step-title">Open quiXzoom before your shift</p><p class="layer-step-desc">Browse missions on the map overlaid on your planned route. Identify 35 that are directly on path.</p></div></div><div class="layer-step"><div class="layer-step-num">2</div><div class="layer-step-body"><p class="layer-step-title">Claim as you approach</p><p class="layer-step-desc">Claim each mission when you're 510 minutes away. Keeps time window comfortable.</p></div></div><div class="layer-step"><div class="layer-step-num">3</div><div class="layer-step-body"><p class="layer-step-title">Stop, shoot, go</p><p class="layer-step-desc">A simple façade mission: pull up, 4 photos, submit. 35 minutes. You're moving again.</p></div></div></div>` },
{ tab: '3 · Best mission types', html: `<p class="layer-lead">Not all missions fit the driver workflow equally well. These work best.</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Mission type</th><th>Fit for drivers</th><th>Why</th></tr><tr><td>Verification (address check)</td><td>⭐⭐⭐⭐⭐</td><td>Already stopping at addresses</td></tr><tr><td>Real estate (exterior)</td><td>⭐⭐⭐⭐</td><td>Drive-by documentation, no entry needed</td></tr><tr><td>Urban & streets</td><td>⭐⭐⭐⭐</td><td>Pass through these zones anyway</td></tr><tr><td>Infrastructure</td><td>⭐⭐⭐</td><td>Often near delivery zones</td></tr><tr><td>Incident (surge)</td><td>⭐⭐⭐⭐⭐</td><td>Already in the area when events happen</td></tr></table></div>` },
{ tab: '4 · Earnings model', html: `<p class="layer-lead">Route-integrated missions have an exceptional earnings-to-time ratio because the travel cost is zero.</p><div class="layer-block"><p class="layer-block-title">Example shift: 8-hour delivery day</p><ul class="layer-ul"><li>5 verification missions on delivery routes: 5 × €4 = €20</li><li>2 exterior documentation missions: 2 × €7 = €14</li><li>1 urban survey cluster (3 adjacent buildings): €18</li><li>Total added: €52 for ~25 minutes of additional activity</li></ul></div>` },
{ tab: '5 · Platform rules for drivers', html: `<p class="layer-lead">Using quiXzoom while on a delivery shift: rules to follow.</p><ul class="layer-ul"><li>Never compromise your delivery SLAs for a quiXzoom mission — deliveries first</li><li>Do not stop in prohibited areas to complete a mission</li><li>Never use a handheld device while driving — only stop at safe, legal locations</li><li>Check your employment contract: most delivery platform contracts do not restrict secondary income activities during breaks</li></ul>` },
{ tab: '6 · Tier progression', html: `<p class="layer-lead">Regular route-based Zoomers typically reach Active tier within 68 weeks, unlocking premium missions.</p><ul class="layer-ul"><li>Supplementary → Active: ~50 approved missions with &gt;85% approval rate</li><li>Active → Professional: ~200 approved missions with &gt;92% approval rate</li><li>Professional tier: access to premium infrastructure missions, priority claim windows, compensation boosts</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Your route is already worth money</p><p class="layer-cta-sub">Register now. The missions will be live on your delivery routes from August 2026.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Register as a Zoomer →</a></div>` }
]
},
retired: {
type: 'Zoomer profile', title: 'Retired · Neighbourhood Zoomer',
layers: [
{ tab: '1 · Profile', html: `<p class="layer-lead">Retired Zoomers are among the most consistent performers on the platform. Deep local knowledge, predictable daily routines, and patient attention to detail produce high approval rates.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">25</div><div class="layer-kpi-lbl">Missions per week</div></div><div class="layer-kpi"><div class="layer-kpi-val">€40160</div><div class="layer-kpi-lbl">Monthly earnings (est.)</div></div><div class="layer-kpi"><div class="layer-kpi-val">92%+</div><div class="layer-kpi-lbl">Typical approval rate</div></div></div>` },
{ tab: '2 · A typical day', html: `<p class="layer-lead">The quiXzoom approach fits naturally into a walk-based daily routine without changing it materially.</p><div class="layer-step-list"><div class="layer-step"><div class="layer-step-num">9:00</div><div class="layer-step-body"><p class="layer-step-title">Morning check</p><p class="layer-step-desc">Open app over coffee. See 23 missions within a 1km walk from home. Claim one.</p></div></div><div class="layer-step"><div class="layer-step-num">10:30</div><div class="layer-step-body"><p class="layer-step-title">Morning walk</p><p class="layer-step-desc">Route passes the mission location. Stop, photograph according to brief, submit. Walk continues.</p></div></div><div class="layer-step"><div class="layer-step-num">11:00</div><div class="layer-step-body"><p class="layer-step-title">Review notification</p><p class="layer-step-desc">Submission approved. €7 credited. Payment arrives in bank account tomorrow.</p></div></div></div>` },
{ tab: '3 · Why approval rates are high', html: `<p class="layer-lead">Retired Zoomers consistently achieve the highest approval rates on the platform. The reasons are structural.</p><ul class="layer-ul"><li>No time pressure — time window is comfortable, no rushing</li><li>Attention to detail — brief is read carefully, all angles completed</li><li>Local knowledge — know exactly where things are, less navigation uncertainty</li><li>Calm capture conditions — not multitasking, images are sharp and well-framed</li><li>Consistency — same routes, same conditions, predictable quality</li></ul>` },
{ tab: '4 · Suitable mission types', html: `<p class="layer-lead">Infrastructure and urban missions within walking distance are the natural fit.</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Mission type</th><th>Suitability</th><th>Note</th></tr><tr><td>Infrastructure (road, pavement)</td><td>⭐⭐⭐⭐⭐</td><td>Walking speed ideal for pavement-level documentation</td></tr><tr><td>Urban & streets</td><td>⭐⭐⭐⭐⭐</td><td>Local knowledge advantage</td></tr><tr><td>Real estate (exterior)</td><td>⭐⭐⭐⭐</td><td>No access needed, exterior only</td></tr><tr><td>Verification</td><td>⭐⭐⭐⭐</td><td>Local area knowledge invaluable</td></tr></table></div>` },
{ tab: '5 · Social aspect', html: `<p class="layer-lead">Several retired beta Zoomers noted an unexpected benefit: purpose and local contribution.</p><div class="layer-quote"><p>"I walk past the same bridge every morning anyway. Now I'm documenting it for the municipality. It feels like I'm contributing something, not just walking."</p></div><div class="layer-quote"><p>"I know this neighbourhood better than anyone. It turns out that local knowledge is worth money. Who knew."</p></div>` },
{ tab: '6 · Getting help', html: `<p class="layer-lead">For Zoomers who are less familiar with smartphone technology, we provide setup support.</p><ul class="layer-ul"><li>In-app tutorial: step-by-step setup guide optimised for older users</li><li>Video guides: short YouTube-format videos for each mission type</li><li>Email support: response within 1 business day</li><li>Phone support: available for Premium Zoomers (Active tier and above)</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Your daily walk is worth more than you think</p><p class="layer-cta-sub">Register interest now. We'll notify you when missions open in your area in August 2026.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Register as a Zoomer →</a></div>` }
]
}
},
mission: {
infrastructure: {
type: 'Mission category', title: 'Infrastructure',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">Infrastructure missions are the backbone of the quiXzoom platform. They are recurring, predictable, and distributed across every populated area. Clients include municipalities, national road agencies, utility operators and bridge authorities.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">€525</div><div class="layer-kpi-lbl">Typical compensation</div></div><div class="layer-kpi"><div class="layer-kpi-val">2045 min</div><div class="layer-kpi-lbl">Average mission time</div></div><div class="layer-kpi"><div class="layer-kpi-val">Recurring</div><div class="layer-kpi-lbl">Same assets, annual cycles</div></div></div><div class="layer-badge-row"><span class="layer-badge">Roads & pavements</span><span class="layer-badge">Bridges</span><span class="layer-badge">Drainage</span><span class="layer-badge">Street lighting</span><span class="layer-badge">Utility cabinets</span><span class="layer-badge">Flood infrastructure</span></div>` },
{ tab: '2 · What you document', html: `<p class="layer-lead">Infrastructure missions specify exactly which elements need to be captured. No guessing required.</p><p class="layer-h3">Example: Road surface mission</p><ul class="layer-ul"><li>Full-length view down the road segment (both directions)</li><li>Close-up of any visible surface damage (cracks, potholes, deformation)</li><li>Drainage gully condition (cover present, obstruction)</li><li>Kerb condition at both sides</li></ul><p class="layer-h3">Example: Bridge inspection mission</p><ul class="layer-ul"><li>Full span view from each abutment</li><li>Underside if accessible from public area</li><li>Bearing condition (visible from bank)</li><li>Any visible corrosion, cracking or vegetation growth</li></ul>` },
{ tab: '3 · Equipment needed', html: `<p class="layer-lead">Standard smartphone. No specialist equipment for most infrastructure missions.</p><ul class="layer-ul"><li>12MP camera or better — standard on all recent phones</li><li>Location services enabled — required for all missions</li><li>Good lighting — cloudy daylight is ideal; avoid harsh direct sun or night</li><li>Steady hands or flat surface for close-up shots</li></ul><p class="layer-h3">Advanced infrastructure missions</p><p class="layer-p">Some higher-compensation missions specify minimum equipment (e.g. bridge inspection requiring access to a specific vantage point). These are clearly marked in the brief and are only available to rated Zoomers.</p>` },
{ tab: '4 · Compensation', html: `<div class="layer-table-wrap"><table class="layer-table"><tr><th>Infrastructure type</th><th>Typical compensation</th><th>Why</th></tr><tr><td>Road surface (50m segment)</td><td>€47</td><td>Standard volume mission</td></tr><tr><td>Bridge (full exterior)</td><td>€1220</td><td>More complex, higher orderer value</td></tr><tr><td>Utility cabinet</td><td>€35</td><td>Quick, high volume</td></tr><tr><td>Flood infrastructure</td><td>€815</td><td>Seasonal priority, higher demand</td></tr><tr><td>Premium bridge inspection</td><td>€2550</td><td>Specialisation required, high orderer SLA</td></tr></table></div>` },
{ tab: '5 · Recurring missions', html: `<p class="layer-lead">Infrastructure missions repeat on annual or semi-annual cycles. This means reliable, predictable income for Zoomers who know their local assets.</p><ul class="layer-ul"><li>Same bridges, same roads, same utility cabinets — every year</li><li>Zoomers who documented an asset before have priority access to re-documentation missions (coming Q4 2026)</li><li>Build a specialist route: know every asset in your area, check them annually</li></ul>` },
{ tab: '6 · AI review criteria', html: `<p class="layer-lead">Infrastructure AI review is based on specification compliance. Know the criteria before you shoot.</p><ul class="layer-ul"><li>All specified angles present and clearly legible</li><li>Asset occupies ≥40% of frame in required shots</li><li>No obstruction blocking the primary subject</li><li>Capture location within mission perimeter (verified by GPS)</li><li>Image sharpness above threshold (motion blur from handheld is the most common rejection reason)</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Infrastructure missions go live August 2026</p><p class="layer-cta-sub">Register now. Your neighbourhood has more infrastructure missions than you think.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
},
realestate: {
type: 'Mission category', title: 'Real estate',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">Real estate documentation missions cover facades, roofs (from street level), entrances, parking structures and commercial exteriors. Clients include property management companies, insurance firms and lenders.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">€618</div><div class="layer-kpi-lbl">Typical compensation</div></div><div class="layer-kpi"><div class="layer-kpi-val">1530 min</div><div class="layer-kpi-lbl">Average mission time</div></div><div class="layer-kpi"><div class="layer-kpi-val">Urban</div><div class="layer-kpi-lbl">Highest density in cities</div></div></div>` },
{ tab: '2 · What you document', html: `<p class="layer-lead">Real estate missions are exterior-only. You never need to enter a building.</p><ul class="layer-ul"><li>All street-facing facades (usually 13 elevations visible from public land)</li><li>Main entrance — door condition, signage, accessibility</li><li>Roof edge if visible from street</li><li>Parking area (if specified)</li><li>Any visible damage, staining, structural concerns</li></ul><p class="layer-h3">What you do NOT need to do</p><ul class="layer-ul"><li>Enter the building</li><li>Speak to residents or tenants</li><li>Access the roof</li><li>Document interiors</li></ul>` },
{ tab: '3 · Urban clusters', html: `<p class="layer-lead">Real estate missions cluster in dense urban areas. A single city block can contain 510 missions. Plan routes to cover clusters for maximum earnings efficiency.</p><ul class="layer-ul"><li>Filter map by "Real estate" and zoom into your area</li><li>Identify clusters of 3+ adjacent missions</li><li>Claim all before walking — claim window is 2 hours</li><li>Document in sequence along the block</li></ul><p class="layer-h3">Typical cluster earnings</p><p class="layer-p">5 adjacent façade missions at €8 each = €40 for approximately 50 minutes of work in one block.</p>` },
{ tab: '4 · Compensation', html: `<div class="layer-table-wrap"><table class="layer-table"><tr><th>Property type</th><th>Typical compensation</th><th>Complexity</th></tr><tr><td>Residential terrace</td><td>€58</td><td>Low — 12 elevations</td></tr><tr><td>Apartment block</td><td>€1016</td><td>Medium — full exterior circuit</td></tr><tr><td>Commercial premises</td><td>€814</td><td>Medium — signage, accessibility</td></tr><tr><td>Car park structure</td><td>€1220</td><td>Higher — multiple levels visible</td></tr><tr><td>Industrial/warehouse</td><td>€818</td><td>Variable — large exterior areas</td></tr></table></div>` },
{ tab: '5 · Tips for high approval', html: `<ul class="layer-ul"><li>Shoot in flat, overcast daylight — avoids harsh shadows on facades</li><li>Stand far enough back to fit the full building in frame — the most common rejection reason is partial coverage</li><li>Shoot all elevations — even ones that look fine. The brief requires them</li><li>Include street context in at least one shot — confirms location</li><li>If access is blocked (parked vehicles, scaffolding) — note it in submission notes before submitting</li></ul>` },
{ tab: '6 · Insurance surge missions', html: `<p class="layer-lead">After weather events (storms, hail, flooding), insurance companies activate surge documentation missions for residential and commercial properties in the affected area. These pay 24× standard rates.</p><ul class="layer-ul"><li>Push notification sent immediately when surge missions activate in your area</li><li>Fastest submissions earn the highest compensation</li><li>No booking required — first valid submission accepted</li><li>Surge missions are time-sensitive — typically expire within 2448 hours</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Real estate missions in every city</p><p class="layer-cta-sub">Register now. Your neighbourhood's property portfolio is already a waiting mission list.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
},
coastline: {
type: 'Mission category', title: 'Coastlines & nature',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">Coastal and environmental missions document shorelines, beaches, waterways, and natural habitats. Clients include environmental agencies, maritime authorities, conservation organisations and research institutions.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">€830</div><div class="layer-kpi-lbl">Typical compensation</div></div><div class="layer-kpi"><div class="layer-kpi-val">3060 min</div><div class="layer-kpi-lbl">Average mission time</div></div><div class="layer-kpi"><div class="layer-kpi-val">Seasonal</div><div class="layer-kpi-lbl">Higher volume spring/summer</div></div></div>` },
{ tab: '2 · What you document', html: `<p class="layer-lead">Coastal missions focus on change detection and condition monitoring over time.</p><ul class="layer-ul"><li>Shoreline condition — erosion, accretion, new features</li><li>Beach profile — width, material, debris</li><li>Coastal structures — sea walls, groynes, breakwaters</li><li>Vegetation line — position and density</li><li>Water clarity (where specified)</li><li>Invasive species markers (flagged in specific missions)</li></ul>` },
{ tab: '3 · Who commissions these', html: `<div class="layer-badge-row"><span class="layer-badge">Environmental agencies</span><span class="layer-badge">Maritime authorities</span><span class="layer-badge">Conservation NGOs</span><span class="layer-badge">Research institutions</span><span class="layer-badge">Local councils</span><span class="layer-badge">Insurance (flood risk)</span></div><p class="layer-h3">Use cases</p><ul class="layer-ul"><li>Annual coastal monitoring programmes (same transects, year after year)</li><li>Post-storm damage assessment</li><li>Invasive species early detection surveys</li><li>Flood risk verification for insurance underwriting</li><li>Academic research support</li></ul>` },
{ tab: '4 · Compensation', html: `<div class="layer-table-wrap"><table class="layer-table"><tr><th>Mission type</th><th>Compensation</th></tr><tr><td>Coastal transect (100m)</td><td>€1018</td></tr><tr><td>Beach condition survey</td><td>€1220</td></tr><tr><td>River/waterway segment</td><td>€815</td></tr><tr><td>Post-storm coastal assessment</td><td>€2040</td></tr><tr><td>Habitat survey (specialist)</td><td>€2550</td></tr></table></div>` },
{ tab: '5 · Seasonal patterns', html: `<p class="layer-lead">Coastal missions peak in spring (post-winter assessment) and summer (active monitoring season). Plan your Zoomer activity around these peaks for maximum earnings.</p><ul class="layer-ul"><li>MarchApril: post-winter storm assessment missions — high compensation, surge pricing</li><li>MayAugust: regular monitoring cycles — consistent volume</li><li>OctoberNovember: pre-winter baseline documentation — another surge period</li><li>DecemberFebruary: lower volume, but ice and frost condition missions available in northern regions</li></ul>` },
{ tab: '6 · Safety', html: `<p class="layer-lead">Coastal environments have specific safety considerations. quiXzoom missions require documentation from safe, accessible public areas only.</p><ul class="layer-ul"><li>Never enter the water to complete a mission</li><li>Never approach unstable cliff edges or eroded banks</li><li>All missions specify safe capture positions — stay within these</li><li>Weather check required before coastal missions — wind speed and sea state indicated in brief</li><li>If conditions are unsafe: release the mission without penalty</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">The coastline nearest you is already a mission</p><p class="layer-cta-sub">Register interest. Coastal missions live from August 2026, starting in Sweden.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
},
incident: {
type: 'Mission category', title: 'Incident & damage',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">Incident missions are activated immediately after weather events, fires, floods or other damage-causing events. They are the highest-paying mission type and are time-critical — the fastest Zoomers earn the most.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">€1560</div><div class="layer-kpi-lbl">Typical compensation</div></div><div class="layer-kpi"><div class="layer-kpi-val">Speed</div><div class="layer-kpi-lbl">Fastest submission earns most</div></div><div class="layer-kpi"><div class="layer-kpi-val">Push alert</div><div class="layer-kpi-lbl">Instant notification to local Zoomers</div></div></div>` },
{ tab: '2 · How surge works', html: `<p class="layer-lead">When an insurance company detects a weather event or receives a cluster of claims in an area, they trigger a documentation surge campaign. Zoomers in the area receive a push notification immediately.</p><div class="layer-step-list"><div class="layer-step"><div class="layer-step-num">1</div><div class="layer-step-body"><p class="layer-step-title">Event detected</p><p class="layer-step-desc">Storm, flood or fire event triggers campaign creation by insurance client. Happens within minutes of event.</p></div></div><div class="layer-step"><div class="layer-step-num">2</div><div class="layer-step-body"><p class="layer-step-title">Push notification to local Zoomers</p><p class="layer-step-desc">All Zoomers within the affected area receive alert: "Surge mission active near you — €[amount] per approved submission."</p></div></div><div class="layer-step"><div class="layer-step-num">3</div><div class="layer-step-body"><p class="layer-step-title">First submission wins maximum</p><p class="layer-step-desc">Each address-level mission: first approved submission earns the maximum rate. Speed is directly rewarded.</p></div></div></div>` },
{ tab: '3 · What you document', html: `<p class="layer-lead">Incident missions focus on visible external damage from public areas. No entry to buildings.</p><ul class="layer-ul"><li>Overall building condition from street</li><li>Visible damage: roof, facade, windows, structural elements</li><li>Water ingress evidence: staining, debris lines</li><li>Access road condition</li><li>Adjacent properties if specified</li></ul><p class="layer-h3">What the AI checks specifically</p><ul class="layer-ul"><li>Damage visible in frame</li><li>Capture timestamp within 48h of event</li><li>Location confirmed at the specified address</li><li>Full required angle coverage</li></ul>` },
{ tab: '4 · Compensation model', html: `<p class="layer-lead">Incident missions use a decay compensation model. First submission earns maximum; subsequent submissions earn less.</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Submission order</th><th>Compensation (example €30 mission)</th></tr><tr><td>1st approved</td><td>€30 (full rate)</td></tr><tr><td>2nd approved</td><td>€18 (60%)</td></tr><tr><td>3rd approved</td><td>€9 (30%)</td></tr><tr><td>4th+</td><td>€4.50 (15%)</td></tr></table></div><p class="layer-p">The decay model incentivises speed. For insurance purposes, the first documentation before damage is cleared is most valuable.</p>` },
{ tab: '5 · Safety rules', html: `<p class="layer-lead">After serious events (floods, fires, structural damage), safety is paramount. quiXzoom incident missions have mandatory safety rules.</p><ul class="layer-ul"><li>Never enter a flood-affected or fire-damaged building</li><li>Maintain safe distance from structurally compromised buildings</li><li>Do not enter areas cordoned by emergency services</li><li>Incident missions activate only in areas where general public access has been confirmed safe</li><li>If unsafe conditions: use "report unsafe" in the app — no penalty, mission is reassigned</li></ul>` },
{ tab: '6 · Enabling push alerts', html: `<p class="layer-lead">To receive surge notifications instantly when events happen near you, push alerts must be enabled.</p><ul class="layer-ul"><li>Enable push notifications for quiXzoom in your phone settings</li><li>Set your notification zone radius — larger radius = more surge events</li><li>Keep app installed and location services always-on</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Highest pay. Near you. Right now.</p><p class="layer-cta-sub">Register to get push alerts when surge missions activate in your area from August 2026.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
},
verification: {
type: 'Mission category', title: 'Verification',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">Verification missions confirm that a specific address, premises or entity exists and matches available records. Clients include e-commerce platforms, banks, recruitment agencies and public sector bodies.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">€38</div><div class="layer-kpi-lbl">Typical compensation</div></div><div class="layer-kpi"><div class="layer-kpi-val">~10 min</div><div class="layer-kpi-lbl">Average mission time</div></div><div class="layer-kpi"><div class="layer-kpi-val">Highest volume</div><div class="layer-kpi-lbl">Most missions available</div></div></div>` },
{ tab: '2 · What you verify', html: `<ul class="layer-ul"><li>Premises exists at the stated address</li><li>Correct trading name/signage visible</li><li>Business appears to be operating (lights on, activity visible)</li><li>Access point and entry condition</li><li>Any specified reference elements (brand colours, specific signage)</li></ul><p class="layer-h3">What you do NOT do</p><ul class="layer-ul"><li>Enter premises</li><li>Speak to anyone inside</li><li>Verify any financial or identity information</li></ul>` },
{ tab: '3 · Route integration', html: `<p class="layer-lead">Verification missions are ideal for route integration — especially for delivery workers already stopping at addresses.</p><p class="layer-p">If you're making a delivery to a street, there may be 25 verification missions for neighbouring premises on the same block. Check before leaving — claim them while you're there.</p>` },
{ tab: '4 · Compensation', html: `<p class="layer-lead">Lower per-mission but highest volume. Verification missions are optimised for quantity.</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Verification type</th><th>Compensation</th></tr><tr><td>Address confirmation</td><td>€34</td></tr><tr><td>Business premises check</td><td>€46</td></tr><tr><td>Storefront documentation</td><td>€58</td></tr><tr><td>Multi-element verification</td><td>€610</td></tr></table></div>` },
{ tab: '5 · Privacy rules', html: `<p class="layer-lead">Verification missions have strict privacy rules. You document premises, not people.</p><ul class="layer-ul"><li>Never photograph identifiable individuals — if people are in shot, do not submit (blur or retake)</li><li>Never document interior spaces through windows</li><li>Only photograph from public land</li><li>Do not attempt to verify the identity of anyone at the premises</li></ul>` },
{ tab: '6 · AI review', html: `<p class="layer-lead">Verification AI review is the fastest on the platform — typically under 15 seconds.</p><ul class="layer-ul"><li>Address number or name visible in frame</li><li>Premises legible and identifiable</li><li>GPS confirms capture at the specified address</li><li>No people identifiable in frame</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">The most missions. The fastest approval.</p><p class="layer-cta-sub">Verification missions launch in Sweden August 1, 2026. More markets follow.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
},
urban: {
type: 'Mission category', title: 'Urban & streets',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">Urban missions document street-level conditions: pavement quality, signage, accessibility, retail presence, and general urban environment. Clients include city planners, retail chains and transport authorities.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">€515</div><div class="layer-kpi-lbl">Typical compensation</div></div><div class="layer-kpi"><div class="layer-kpi-val">1540 min</div><div class="layer-kpi-lbl">Average mission time</div></div><div class="layer-kpi"><div class="layer-kpi-val">City-dense</div><div class="layer-kpi-lbl">Highest density in urban centres</div></div></div>` },
{ tab: '2 · What you document', html: `<ul class="layer-ul"><li>Pavement condition — surface quality, trip hazards, accessibility</li><li>Street signage — road signs, wayfinding, condition</li><li>Retail frontage — occupancy status, condition, branding</li><li>Accessibility features — dropped kerbs, tactile paving, ramps</li><li>Street furniture — benches, bins, cycle stands</li><li>Lighting — streetlight present and functional</li></ul>` },
{ tab: '3 · Walk-based workflow', html: `<p class="layer-lead">Urban missions are designed for walkers. A single high street can contain 1020 missions completed in one pass.</p><div class="layer-step-list"><div class="layer-step"><div class="layer-step-num">1</div><div class="layer-step-body"><p class="layer-step-title">Plan a walk route through mission clusters</p><p class="layer-step-desc">Filter map by Urban. Find a street with 5+ adjacent missions.</p></div></div><div class="layer-step"><div class="layer-step-num">2</div><div class="layer-step-body"><p class="layer-step-title">Claim as you walk</p><p class="layer-step-desc">Claim each mission 23 minutes before you arrive. Submit as you pass.</p></div></div><div class="layer-step"><div class="layer-step-num">3</div><div class="layer-step-body"><p class="layer-step-title">One street, €4080</p><p class="layer-step-desc">A 1km high street walk with 10 missions at €58 each = €5080 in under 1 hour.</p></div></div></div>` },
{ tab: '4 · Compensation', html: `<div class="layer-table-wrap"><table class="layer-table"><tr><th>Urban mission type</th><th>Compensation</th></tr><tr><td>Pavement condition survey</td><td>€47</td></tr><tr><td>Retail frontage check</td><td>€58</td></tr><tr><td>Accessibility audit (20m segment)</td><td>€812</td></tr><tr><td>High street condition survey</td><td>€1220</td></tr></table></div>` },
{ tab: '5 · Privacy in urban environments', html: `<p class="layer-lead">Urban environments contain people. quiXzoom's AI review includes pedestrian detection — missions with identifiable individuals in primary focus are flagged.</p><ul class="layer-ul"><li>Capture when pedestrian traffic is low where possible</li><li>Use wide shots that keep people as background elements, not subjects</li><li>If someone is prominently in frame: wait 30 seconds and retake</li><li>You never need to photograph people — the mission is always about the built environment</li></ul>` },
{ tab: '6 · City planning missions', html: `<p class="layer-lead">Some urban missions are part of larger city planning studies commissioned by municipalities. These are higher-value and require a structured survey approach.</p><ul class="layer-ul"><li>Accessibility audit missions: document every accessibility feature along a defined route</li><li>Retail vacancy surveys: document occupancy status of every unit on a shopping street</li><li>Before/after infrastructure works: document condition before and after roadworks</li></ul><p class="layer-p">City planning missions are rated-only — available from Active tier upward.</p>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Every street you walk is potential earnings</p><p class="layer-cta-sub">Register interest. Urban missions launch August 2026 in Sweden, with more cities and markets to follow.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
}
},
tier: {
supplementary: {
type: 'Earnings tier', title: 'Supplementary — 24 missions/week',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">The Supplementary tier is the entry point. No minimum commitment, no schedule, no performance requirements beyond the standard approval rate.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">€2080</div><div class="layer-kpi-lbl">Typical monthly earnings</div></div><div class="layer-kpi"><div class="layer-kpi-val">24</div><div class="layer-kpi-lbl">Missions per week</div></div><div class="layer-kpi"><div class="layer-kpi-val">~2h</div><div class="layer-kpi-lbl">Time per week</div></div></div>` },
{ tab: '2 · What you can access', html: `<ul class="layer-ul"><li>All standard mission categories (infrastructure, real estate, verification, urban, coastline)</li><li>Incident/surge missions when activated in your area</li><li>Standard compensation rates for all mission types</li></ul><p class="layer-h3">Not yet available (unlocks at Active tier)</p><ul class="layer-ul"><li>Premium missions (higher compensation, stricter specification)</li><li>Priority claim window on high-demand missions</li><li>Specialisation modules</li></ul>` },
{ tab: '3 · Path to Active', html: `<p class="layer-lead">Moving from Supplementary to Active tier unlocks significantly higher earnings potential.</p><p class="layer-h3">Requirements for Active tier</p><ul class="layer-ul"><li>50 approved missions (cumulative)</li><li>Approval rate above 85%</li><li>No active claim violations</li></ul><p class="layer-h3">How long does it take?</p><p class="layer-p">At 3 missions per week: approximately 4 months. At 5 missions per week: approximately 10 weeks.</p>` },
{ tab: '4 · Realistic earnings', html: `<div class="layer-table-wrap"><table class="layer-table"><tr><th>Missions/week</th><th>Avg. per mission</th><th>Monthly estimate</th></tr><tr><td>2</td><td>€7</td><td>~€56</td></tr><tr><td>3</td><td>€7</td><td>~€84</td></tr><tr><td>4</td><td>€8</td><td>~€128</td></tr></table></div><p class="layer-p">Including surge missions (12/month): add €2060 to monthly estimates.</p>` },
{ tab: '5 · Tax threshold guidance', html: `<p class="layer-lead">At Supplementary level, many Zoomers will earn below their country's minimum threshold for mandatory tax registration.</p><ul class="layer-ul"><li>Sweden: Grundavdrag ~17,500 SEK/year (~€1,500/month). Supplementary earnings typically well below this.</li><li>Germany: Kleinunternehmer threshold €22,000/year. Not reached at this tier.</li><li>UK: Personal allowance £12,570/year. Supplementary Zoomer earnings far below this.</li></ul><p class="layer-p"><em>General information only — consult a local tax advisor for your situation.</em></p>` },
{ tab: '6 · Getting the most from this tier', html: `<ul class="layer-ul"><li>Focus on approval rate over volume — every rejection at this tier costs relatively more</li><li>Stick to mission types you understand well until your rate is consistently above 85%</li><li>Enable push notifications — surge missions add disproportionate value at this tier</li><li>Plan: 50 approved missions is your target — set it as a goal and track progress</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Start here. No commitment.</p><p class="layer-cta-sub">Register interest. The Supplementary tier is open to everyone from day one — no qualification required.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
},
active: {
type: 'Earnings tier', title: 'Active — 812 missions/week',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">Active is the most popular Zoomer tier. It represents a meaningful secondary income stream — €200400/month — without requiring a full-time commitment.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">€200400</div><div class="layer-kpi-lbl">Typical monthly earnings</div></div><div class="layer-kpi"><div class="layer-kpi-val">812</div><div class="layer-kpi-lbl">Missions per week</div></div><div class="layer-kpi"><div class="layer-kpi-val">58h</div><div class="layer-kpi-lbl">Time per week</div></div></div>` },
{ tab: '2 · Unlocked at Active', html: `<ul class="layer-ul"><li>Priority claim window: 5-minute advance access to new high-demand missions before general availability</li><li>Premium missions: higher compensation, stricter specification, more interesting subjects</li><li>Specialisation module access: complete training to unlock bridge inspection, coastal survey, and other high-value categories</li><li>Performance-based compensation boost: approval rate above 92% earns a 5% rate uplift across all missions</li></ul>` },
{ tab: '3 · Weekly rhythm', html: `<p class="layer-lead">Active Zoomers typically build a consistent weekly rhythm around mission clusters in their area.</p><div class="layer-step-list"><div class="layer-step"><div class="layer-step-num">MonFri</div><div class="layer-step-body"><p class="layer-step-title">2 missions per day (mornings or lunch)</p><p class="layer-step-desc">10 missions at €8 average = €80/week baseline.</p></div></div><div class="layer-step"><div class="layer-step-num">Weekend</div><div class="layer-step-body"><p class="layer-step-title">Surge monitoring + premium missions</p><p class="layer-step-desc">Higher-value targets. Weather events often trigger surge during weekends.</p></div></div></div>` },
{ tab: '4 · Realistic earnings', html: `<div class="layer-table-wrap"><table class="layer-table"><tr><th>Scenario</th><th>Missions/week</th><th>Avg. rate</th><th>Monthly</th></tr><tr><td>Conservative Active</td><td>8</td><td>€8</td><td>~€256</td></tr><tr><td>Standard Active</td><td>10</td><td>€9</td><td>~€360</td></tr><tr><td>Active + surge (2/month)</td><td>10 + surge</td><td>€9 + €30</td><td>~€420</td></tr><tr><td>Active + premium</td><td>10 standard + 2 premium</td><td>€9/€20</td><td>~€440</td></tr></table></div>` },
{ tab: '5 · How to maintain Active tier', html: `<p class="layer-lead">Active tier is maintained by consistent activity and quality. Falling below the threshold triggers a grace period.</p><ul class="layer-ul"><li>Minimum: 6 approved missions per month to maintain Active status</li><li>Approval rate must stay above 80% (rolling 30-day)</li><li>Inactivity for 60 days: tier paused (reactivates on next approved mission)</li><li>Grace period if dropping below: 14 days to complete 5 approved missions to retain tier</li></ul>` },
{ tab: '6 · Specialisations', html: `<p class="layer-lead">Specialisations unlock premium mission categories with higher compensation rates. Available at Active tier.</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Specialisation</th><th>Missions unlocked</th><th>Extra compensation</th></tr><tr><td>Bridge & structures</td><td>Bridge inspection premium missions</td><td>+4080% vs standard</td></tr><tr><td>Coastal survey</td><td>Environmental monitoring missions</td><td>+3060% vs standard</td></tr><tr><td>Accessibility audit</td><td>Municipal accessibility surveys</td><td>+2545% vs standard</td></tr><tr><td>Insurance documentation</td><td>Priority surge access</td><td>First-claim advantage</td></tr></table></div>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Active tier: the target for serious Zoomers</p><p class="layer-cta-sub">Register now. Start Supplementary in August 2026. Reach Active within 1016 weeks.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
},
professional: {
type: 'Earnings tier', title: 'Professional — 15+ missions/week',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">Professional Zoomers treat quiXzoom as a primary or near-primary income source. Reaching this tier requires consistent quality and volume over several months — and delivers proportional earnings.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">€6001,500+</div><div class="layer-kpi-lbl">Typical monthly earnings</div></div><div class="layer-kpi"><div class="layer-kpi-val">1525</div><div class="layer-kpi-lbl">Missions per week</div></div><div class="layer-kpi"><div class="layer-kpi-val">~20h</div><div class="layer-kpi-lbl">Time per week</div></div></div>` },
{ tab: '2 · Unlocked at Professional', html: `<ul class="layer-ul"><li>First access to all new premium mission categories as they launch</li><li>Dedicated account manager (for Zoomers earning above €500/month)</li><li>10-minute priority claim window before Active tier Zoomers</li><li>Performance compensation: approval rate above 95% earns 10% uplift across all missions</li><li>Reserved high-value campaign slots: selected Zoomers invited to commissioned campaigns at fixed daily rates</li></ul>` },
{ tab: '3 · Campaign work', html: `<p class="layer-lead">At Professional tier, quiXzoom can invite you to dedicated campaign work — typically a fixed-rate day rate for systematic documentation of a specific asset set.</p><p class="layer-h3">Example campaign</p><div class="layer-block"><p class="layer-block-title">Municipal bridge survey — Stockholm</p><ul class="layer-ul"><li>Duration: 5 days</li><li>Scope: 120 bridges in the city</li><li>Rate: €200/day (fixed, not per submission)</li><li>Total: €1,000 for the campaign</li></ul></div><p class="layer-p">Campaign invitations are issued to Professional Zoomers with relevant specialisations in the geographic area. Not guaranteed — depends on campaign availability and matching.</p>` },
{ tab: '4 · Tax at Professional level', html: `<p class="layer-lead">At Professional earnings levels, formal tax registration is typically required in all jurisdictions.</p><ul class="layer-ul"><li>Sweden: Register as enskild firma or aktiebolag. F-skatt certification recommended.</li><li>Germany: Gewerbe registration required. Umsatzsteuer (VAT) may apply above €22,000.</li><li>UK: Self-assessment required. National Insurance Class 4 on profits above £12,570.</li><li>EU generally: VAT registration typically required above national thresholds.</li></ul><p class="layer-p"><em>Consult a qualified tax advisor. quiXzoom provides annual earnings statements in all required formats.</em></p>` },
{ tab: '5 · Equipment at this level', html: `<p class="layer-lead">Professional Zoomers working on premium infrastructure and environmental missions often invest in modest equipment upgrades.</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Equipment</th><th>Benefit</th><th>Cost (approx.)</th></tr><tr><td>Stabiliser/gimbal</td><td>Sharper images, higher approval rate</td><td>€60120</td></tr><tr><td>Wide-angle lens attachment</td><td>Better façade coverage in tight spaces</td><td>€3080</td></tr><tr><td>Portable power bank</td><td>Full-day operation without recharge</td><td>€2040</td></tr><tr><td>Weatherproof phone case</td><td>Coastal and incident missions in all weather</td><td>€2050</td></tr></table></div>` },
{ tab: '6 · Building to Professional', html: `<p class="layer-lead">Professional tier requires: 200+ approved missions with 90%+ approval rate. Timeline depends on weekly mission volume.</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Weekly missions</th><th>Estimated time to Professional</th></tr><tr><td>810 (Active pace)</td><td>68 months</td></tr><tr><td>1215</td><td>45 months</td></tr><tr><td>15+</td><td>34 months</td></tr></table></div>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Professional tier starts with one mission</p><p class="layer-cta-sub">Register now. August 2026 launch. Every mission from day one counts toward Professional.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
}
},
faq: {
camera: {
type: 'Equipment guide', title: 'Camera and equipment requirements',
layers: [
{ tab: '1 · Minimum spec', html: `<p class="layer-lead">The only requirement is a smartphone with a 12MP or better rear camera. No specialist equipment needed for standard missions.</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Device</th><th>Minimum model</th><th>Passes standard missions</th></tr><tr><td>iPhone</td><td>iPhone 12</td><td><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><polyline points="20 6 9 17 4 12"/></svg> Yes</td></tr><tr><td>Samsung Galaxy</td><td>S21 / A53 and newer</td><td><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><polyline points="20 6 9 17 4 12"/></svg> Yes</td></tr><tr><td>Google Pixel</td><td>Pixel 5 and newer</td><td><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><polyline points="20 6 9 17 4 12"/></svg> Yes</td></tr><tr><td>Other Android</td><td>12MP rear camera, Android 10+</td><td><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><polyline points="20 6 9 17 4 12"/></svg> Yes</td></tr></table></div>` },
{ tab: '2 · Settings', html: `<ul class="layer-ul"><li>Resolution: maximum native resolution (no digital zoom)</li><li>Location: always on while using the app</li><li>HDR: off (produces inconsistent results for AI review)</li><li>Portrait mode: off (can add artificial blur)</li><li>Video: not accepted for missions (photos only, unless specified)</li></ul>` },
{ tab: '3 · Optional equipment', html: `<p class="layer-lead">Optional — none required for standard missions. Useful for higher-volume or premium missions.</p><ul class="layer-ul"><li>Gimbal stabiliser: reduces rejection from motion blur. ~€60120. Useful for Active tier upward.</li><li>Wide-angle clip-on lens: better building coverage in tight streets. ~€3080.</li><li>Power bank: for full-day shifts. 10,000mAh, ~€20.</li><li>Weatherproof case: coastal and incident missions. ~€2050.</li></ul>` },
{ tab: '4 · Common photo errors', html: `<p class="layer-h3">Most common AI rejection reasons</p><ul class="layer-ul"><li>Motion blur — move slower, hold breath before shot, or use a surface to stabilise</li><li>Subject too small — step back until asset fills at least 40% of frame</li><li>Obstruction — parked vehicle, scaffolding, tree in front of subject</li><li>Overexposure in direct sunlight — shoot in shade or turn back to the sun</li><li>Wrong angle — re-read the brief and match example images exactly</li></ul>` },
{ tab: '5 · Drones', html: `<p class="layer-lead">Drone support is planned for Q4 2026 for specific mission types (coastal, large-scale infrastructure).</p><ul class="layer-ul"><li>Drone missions will require verified drone licence (A1/A3 certification in EU)</li><li>Higher compensation: drone missions pay 38× standard ground-level missions</li><li>Register drone capability in your profile to be notified when drone missions launch</li></ul>` },
{ tab: '6 · Professional kit', html: `<p class="layer-lead">Full equipment guide for Professional tier Zoomers operating in all mission categories.</p><ul class="layer-ul"><li>Primary phone: iPhone 15 Pro or Samsung S24 Ultra — best camera systems available</li><li>Stabiliser: DJI OM6 or Insta360 Flow — ~€100</li><li>Lenses: Moment wide (18mm) for buildings, tele for detail — ~€80 each</li><li>Power: Anker 20,000mAh with fast charge — ~€35</li><li>Weatherproofing: UAG or Catalyst case — ~€40</li><li>Total investment: ~€300. Payback at Active tier: approximately 5 weeks.</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Your current phone is enough to start</p><p class="layer-cta-sub">Register now. No equipment purchase needed to begin earning in August 2026.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
},
payment: {
type: 'Payment guide', title: 'How and when you get paid',
layers: [
{ tab: '1 · The basics', html: `<p class="layer-lead">Payment is automatic. When your submission is approved, Stripe Connect initiates a transfer to your linked bank account. You don't do anything.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">Automatic</div><div class="layer-kpi-lbl">No action needed</div></div><div class="layer-kpi"><div class="layer-kpi-val">0%</div><div class="layer-kpi-lbl">Platform fee to Zoomers</div></div><div class="layer-kpi"><div class="layer-kpi-val">13 days</div><div class="layer-kpi-lbl">Bank transfer time</div></div></div>` },
{ tab: '2 · Setting up payouts', html: `<div class="layer-step-list"><div class="layer-step"><div class="layer-step-num">1</div><div class="layer-step-body"><p class="layer-step-title">Identity verification</p><p class="layer-step-desc">Required once. Takes 35 minutes. Government ID + selfie. Processed by Stripe's KYC provider.</p></div></div><div class="layer-step"><div class="layer-step-num">2</div><div class="layer-step-body"><p class="layer-step-title">Link bank account</p><p class="layer-step-desc">IBAN + bank name. Processed via Stripe Connect. quiXzoom never sees your bank details.</p></div></div><div class="layer-step"><div class="layer-step-num">3</div><div class="layer-step-body"><p class="layer-step-title">Done</p><p class="layer-step-desc">All future payments route automatically to this account. Change at any time in settings.</p></div></div></div>` },
{ tab: '3 · Transfer times by country', html: `<div class="layer-table-wrap"><table class="layer-table"><tr><th>Region</th><th>Method</th><th>Time</th></tr><tr><td>Sweden / EU</td><td>SEPA Credit Transfer</td><td>12 business days</td></tr><tr><td>UK</td><td>Faster Payments</td><td>Next business day</td></tr><tr><td>US</td><td>ACH</td><td>23 business days</td></tr><tr><td>Other</td><td>SWIFT</td><td>25 business days</td></tr></table></div>` },
{ tab: '4 · Currency', html: `<p class="layer-lead">You receive payment in your local currency. Conversion at mid-market rate (Stripe) where applicable.</p><ul class="layer-ul"><li>Swedish Zoomers: receive SEK</li><li>EU Zoomers: receive EUR</li><li>UK Zoomers: receive GBP</li><li>Missions are priced in EUR — local currency conversion applied at payout</li></ul>` },
{ tab: '5 · Minimum payout', html: `<p class="layer-lead">Payouts require a minimum Credit balance. Credits accumulate until the threshold is reached, then transfer automatically on the weekly payout cycle.</p><ul class="layer-ul"><li>Minimum payout: 250 QX Credits (or local equivalent)</li><li>Credits below the threshold accumulate in your balance — they never expire</li><li>Automatic weekly transfer when balance meets or exceeds the minimum</li><li>Current balance, threshold status and estimated payout visible in your earnings dashboard</li></ul>` },
{ tab: '6 · Annual statement', html: `<p class="layer-lead">At the end of each calendar year, quiXzoom provides an earnings statement for tax purposes.</p><ul class="layer-ul"><li>Total earnings for the year</li><li>Mission-by-mission transaction log</li><li>Downloadable as PDF (formatted) and CSV (for accounting software)</li><li>Available in your dashboard from January 15 each year</li><li>Covers all approved missions in the calendar year</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Zero fees. Automatic payment. Start in August.</p><p class="layer-cta-sub">Register your interest now. Payment setup takes 5 minutes and is only needed once.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
},
availability: {
type: 'Coverage guide', title: 'Mission availability in your area',
layers: [
{ tab: '1 · Launch coverage', html: `<p class="layer-lead">quiXzoom launches in Sweden on August 1, 2026. Additional markets follow through 2026 and 2027.</p><div class="layer-badge-row"><span class="layer-badge">🇸🇪 Sweden — Aug 2026</span><span class="layer-badge">🇳🇱 Netherlands — Q1 2027</span><span class="layer-badge">🇹🇭 Thailand — Q3 2026</span><span class="layer-badge">🇳🇬 Nigeria — 2026</span><span class="layer-badge">🇰🇪 Kenya — 2026</span></div>` },
{ tab: '2 · Urban vs rural', html: `<div class="layer-table-wrap"><table class="layer-table"><tr><th>Area type</th><th>Mission density</th><th>Expected availability</th></tr><tr><td>Major city (pop. >500k)</td><td>High</td><td>50200+ missions/week within 5km</td></tr><tr><td>Medium city (pop. >100k)</td><td>Medium</td><td>2080 missions/week within 5km</td></tr><tr><td>Small town (pop. >10k)</td><td>Lowmedium</td><td>530 missions/week within 5km</td></tr><tr><td>Rural</td><td>Low</td><td>Variable — compensation higher to attract coverage</td></tr></table></div>` },
{ tab: '3 · How missions are created', html: `<p class="layer-lead">Missions are created by organisations (orderers) via the quiXzoom API. Your area's mission density depends on which organisations have commissioned documentation there.</p><ul class="layer-ul"><li>Municipal clients create recurring missions for their infrastructure assets — predictable volume</li><li>Insurance surge events create sudden high-density missions after weather events</li><li>Property company campaigns cover portfolio assets — city-wide batches</li><li>Verification missions are created continuously by e-commerce and banking clients</li></ul>` },
{ tab: '4 · Notification setup', html: `<p class="layer-lead">Register now to be notified when missions activate in your area.</p><ul class="layer-ul"><li>Email notification before August 2026 launch in your region</li><li>Push notification when first missions appear near you</li><li>Surge alert when high-value event missions activate in your zone</li><li>Weekly mission availability summary (optional, opt-in)</li></ul>` },
{ tab: '5 · Coverage map', html: `<p class="layer-lead">A live coverage map showing Zoomer density and mission activity by region will be available in the app from August 2026.</p><p class="layer-p">During the waitlist period, you can check estimated coverage for your city on our website. Coverage estimates are updated as organisations commit campaigns pre-launch.</p>` },
{ tab: '6 · Expanding coverage', html: `<p class="layer-lead">Mission density grows as more organisations onboard. If you want more missions in your area, you can help.</p><ul class="layer-ul"><li>Share the platform with local government bodies, property companies or insurance contacts</li><li>Zoomer referral programme (launching Q4 2026): earn a bonus when Zoomers you refer complete their first 10 missions</li><li>Organisation referral: if you refer an organisation that signs up, you earn a campaign bonus on their first campaign</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">Register to be first in your area</p><p class="layer-cta-sub">We notify registered Zoomers before launch. First movers have first access to missions when they activate.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
},
tax: {
type: 'Tax guide', title: 'Tax — what you need to know',
layers: [
{ tab: '1 · The basics', html: `<p class="layer-lead">quiXzoom earnings are income. You are responsible for declaring and paying any applicable taxes. quiXzoom does not withhold taxes on your behalf.</p><p class="layer-h3">What quiXzoom provides</p><ul class="layer-ul"><li>Annual earnings statement (PDF + CSV) from January 15 each year</li><li>Transaction-level records for every approved mission</li><li>Stripe Connect payment records (accessible via your Stripe dashboard)</li></ul>` },
{ tab: '2 · Sweden', html: `<p class="layer-lead">Sweden has straightforward rules for occasional income and self-employment.</p><ul class="layer-ul"><li>Declare quiXzoom earnings as inkomst av tjänst (occasional) or inkomst av näringsverksamhet (if regular)</li><li>Grundavdrag (basic deduction): ~17,500 SEK — if total income below this, no income tax</li><li>For regular Zoomer activity: register as enskild firma with Skatteverket (simple, low-cost)</li><li>F-skatt: recommended if earning above ~5,000 SEK/month from quiXzoom</li></ul><p class="layer-p"><em>General information. Consult Skatteverket or a Swedish tax advisor for your situation.</em></p>` },
{ tab: '3 · Germany', html: `<ul class="layer-ul"><li>Under €256/year: tax-free (Freigrenze für Einkünfte aus sonstigen Leistungen in some interpretations — verify with a Steuerberater)</li><li>Kleinunternehmer: if annual turnover below €22,000, simplified VAT rules apply</li><li>Above €22,000: Umsatzsteuer (VAT) registration likely required</li><li>Gewerbesteuer: may apply depending on activity classification</li></ul><p class="layer-p"><em>Germany's tax rules for gig income are complex. A Steuerberater consultation is recommended.</em></p>` },
{ tab: '4 · UK', html: `<ul class="layer-ul"><li>Self-assessment required if quiXzoom income plus other untaxed income exceeds £1,000/year</li><li>Personal allowance: £12,570 — income below this threshold: no income tax</li><li>National Insurance: Class 2 and 4 apply above threshold on self-employed profits</li><li>Sole trader registration with HMRC recommended for active Zoomers</li></ul><p class="layer-p"><em>Consult HMRC's self-employment guidance or an accountant for your situation.</em></p>` },
{ tab: '5 · Other EU countries', html: `<p class="layer-lead">EU countries generally follow similar principles: declare freelance income, register as self-employed if earnings are regular, pay applicable income tax and social contributions.</p><p class="layer-p">quiXzoom will publish country-specific guidance pages for all launch markets by August 2026. These will cover registration requirements, thresholds, and documentation needed for common tax scenarios.</p>` },
{ tab: '6 · Record keeping', html: `<p class="layer-lead">Good records make tax filing simple. Keep these:</p><ul class="layer-ul"><li>quiXzoom annual earnings statement (download from your dashboard)</li><li>Stripe Connect payout records (download from your Stripe dashboard)</li><li>Equipment purchase receipts (deductible in many jurisdictions)</li><li>Travel expenses related to missions (deductible in some jurisdictions)</li></ul>` },
{ tab: '7 · Disclaimer', html: `<div class="layer-block"><p class="layer-block-title">Important</p><p class="layer-p">The information on this page is general guidance only and does not constitute tax advice. Tax rules change. Your situation may differ. Always consult a qualified tax professional or your national tax authority for advice specific to your circumstances.</p></div><div class="layer-cta-box"><p class="layer-cta-title">Questions?</p><p class="layer-cta-sub">Our support team is here to help. Find answers in our documentation or contact us directly.</p><a href="https://www.quixzoom.com/support" class="layer-cta-btn">Visit Support →</a></div>` }
]
},
approval: {
type: 'Review guide', title: 'How the approval process works',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">Every submission goes through an automated AI review before reaching your dashboard. Most decisions happen within 30 seconds of upload.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">~30s</div><div class="layer-kpi-lbl">Average review time</div></div><div class="layer-kpi"><div class="layer-kpi-val">94%</div><div class="layer-kpi-lbl">AI accuracy (vs. expert review)</div></div><div class="layer-kpi"><div class="layer-kpi-val">48h</div><div class="layer-kpi-lbl">Appeal window</div></div></div>` },
{ tab: '2 · Review stages', html: `<div class="layer-step-list"><div class="layer-step"><div class="layer-step-num">1</div><div class="layer-step-body"><p class="layer-step-title">Intake validation</p><p class="layer-step-desc">File format, size, EXIF integrity, upload completeness. Instant.</p></div></div><div class="layer-step"><div class="layer-step-num">2</div><div class="layer-step-body"><p class="layer-step-title">Geo-compliance check</p><p class="layer-step-desc">Capture location verified against mission geofence. Timestamp verified against mission window.</p></div></div><div class="layer-step"><div class="layer-step-num">3</div><div class="layer-step-body"><p class="layer-step-title">Image quality assessment</p><p class="layer-step-desc">Sharpness, exposure, resolution, obstruction detection.</p></div></div><div class="layer-step"><div class="layer-step-num">4</div><div class="layer-step-body"><p class="layer-step-title">Specification coverage</p><p class="layer-step-desc">Are all required angles and elements present?</p></div></div><div class="layer-step"><div class="layer-step-num">5</div><div class="layer-step-body"><p class="layer-step-title">Decision</p><p class="layer-step-desc">Approved → payment triggered. Rejected → reason shown. Borderline → human review queue (24h).</p></div></div></div>` },
{ tab: '3 · Common rejections', html: `<div class="layer-table-wrap"><table class="layer-table"><tr><th>Rejection reason</th><th>Fix</th></tr><tr><td>Missing angle</td><td>Re-shoot the specific angle and supplement</td></tr><tr><td>Image blurred</td><td>Stabilise phone, retake</td></tr><tr><td>Outside geofence</td><td>Move to correct location, retake</td></tr><tr><td>Element obscured</td><td>Wait for obstruction to clear, retake</td></tr><tr><td>Resolution too low</td><td>Check phone camera settings — maximum resolution required</td></tr></table></div>` },
{ tab: '4 · Supplementing a submission', html: `<p class="layer-lead">If your submission is rejected, you have 48 hours to supplement it with additional images addressing the rejection reason.</p><ul class="layer-ul"><li>Return to the location and capture the missing or incorrect elements</li><li>Upload as a supplement to the original submission (not a new submission)</li><li>Supplement is re-reviewed by AI within 30 seconds</li><li>If approved: full mission payment issued immediately</li><li>If the location is inaccessible: contact support for a manual review</li></ul>` },
{ tab: '5 · Appealing', html: `<p class="layer-lead">If you believe the AI review made an error, flag it for human review within 48 hours of the rejection.</p><ul class="layer-ul"><li>Human reviewers assess all flagged cases within 24 hours</li><li>If appeal upheld: submission approved, payment issued</li><li>If appeal rejected: decision is final; reason explained in detail</li><li>Successful appeals improve the AI model — they genuinely help the platform</li></ul>` },
{ tab: '6 · Protecting your approval rate', html: `<p class="layer-lead">Your approval rate affects your tier and mission access. Protect it.</p><ul class="layer-ul"><li>Read briefs carefully — most rejections come from misunderstood requirements</li><li>When in doubt, retake rather than submit a borderline image</li><li>Don't submit in poor lighting conditions — rejection is near-certain</li><li>If access is blocked: release the mission (no penalty) rather than submitting incomplete documentation</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">94% accuracy. 30-second decisions.</p><p class="layer-cta-sub">Register now. Fast, fair, automated review from day one.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
},
parttime: {
type: 'Earnings scenarios', title: 'Working part-time — real scenarios',
layers: [
{ tab: '1 · Overview', html: `<p class="layer-lead">quiXzoom is designed for part-time work. No schedule, no minimum hours, no commitment. You work when you want — which is exactly how most Zoomers use it.</p><div class="layer-kpi-row"><div class="layer-kpi"><div class="layer-kpi-val">0</div><div class="layer-kpi-lbl">Minimum missions required</div></div><div class="layer-kpi"><div class="layer-kpi-val">Yours</div><div class="layer-kpi-lbl">You set your own schedule</div></div><div class="layer-kpi"><div class="layer-kpi-val">Unlimited</div><div class="layer-kpi-lbl">No earning cap</div></div></div>` },
{ tab: '2 · Lunch break scenario', html: `<p class="layer-lead">One of the most common Zoomer patterns: 12 missions during a lunch break.</p><div class="layer-block"><p class="layer-block-title">Typical lunch break (45 min)</p><ul class="layer-ul"><li>Walk to nearest mission cluster: 5 minutes</li><li>Mission 1 (infrastructure, €5): 15 minutes</li><li>Mission 2 (verification, €4): 10 minutes</li><li>Walk back: 5 minutes</li><li>Total: 35 minutes. Earnings: €9. Still had 10 minutes to eat.</li></ul></div><p class="layer-h3">5 days/week = €45/week = ~€180/month</p>` },
{ tab: '3 · Weekend scenario', html: `<p class="layer-lead">Weekend walks with missions integrated into existing activity.</p><div class="layer-block"><p class="layer-block-title">Saturday morning walk (2 hours)</p><ul class="layer-ul"><li>Route planned around 6 adjacent missions in local area</li><li>Mix: 3 infrastructure + 2 real estate + 1 urban survey</li><li>Average €7 per mission: €42</li><li>Same walk you'd have done anyway</li></ul></div><p class="layer-h3">Two weekend sessions/week = ~€320/month</p>` },
{ tab: '4 · Evening scenario', html: `<p class="layer-lead">Short evening sessions after work — 3045 minutes, 23 missions.</p><div class="layer-block"><p class="layer-block-title">Weekday evening (30 min, 3×/week)</p><ul class="layer-ul"><li>3 evenings × 2 missions × €6.50 average = €39/week</li><li>Monthly: ~€156</li><li>Time invested: 1.5 hours/week</li></ul></div>` },
{ tab: '5 · Combining with existing work', html: `<p class="layer-lead">quiXzoom income compounds with other part-time and flexible work. Scenarios from beta testers.</p><div class="layer-table-wrap"><table class="layer-table"><tr><th>Profile</th><th>Existing work</th><th>quiXzoom addition</th></tr><tr><td>Delivery worker</td><td>On-route missions</td><td>+€150300/month, ~15 min/day extra</td></tr><tr><td>Dog walker</td><td>Morning walks</td><td>+€60120/month, same route</td></tr><tr><td>Part-time retail</td><td>Fixed days off</td><td>+€80200/month on free days</td></tr><tr><td>Student</td><td>Campus movement</td><td>+€60150/month, between lectures</td></tr></table></div>` },
{ tab: '6 · When to work more', html: `<p class="layer-lead">Some weeks offer higher earnings opportunities than others. Knowing when to increase your activity maximises returns.</p><ul class="layer-ul"><li>After significant weather events: insurance surge missions activate — highest pay of the year</li><li>Spring (MarchApril): annual municipal inspection season begins — high volume</li><li>Late autumn (October): pre-winter surveys — another surge period</li><li>Urban centre missions refresh regularly — check Monday mornings for new weekly batches</li></ul>` },
{ tab: '7 · Get started', html: `<div class="layer-cta-box"><p class="layer-cta-title">One mission a day. Real income.</p><p class="layer-cta-sub">Register now. No commitment to how much or how often. Start when you want, stop when you want.</p><a href="#waitlist-form-top" class="layer-cta-btn" onclick="closeOverlay()">Join waitlist →</a></div>` }
]
}
}
};
/* ─────────────────────────────────────────────
OVERLAY ENGINE
───────────────────────────────────────────── */
function openDrill(section, key) {
let d;
if (section === 'how') d = DATA.how[key];
else if (section === 'persona') d = DATA.persona[key];
else if (section === 'mission') d = DATA.mission[key];
else if (section === 'tier') d = DATA.tier[key];
else if (section === 'faq') d = DATA.faq[key];
if (!d) return;
openOverlay(d);
}
function openOverlay(d) {
document.getElementById('overlay-breadcrumb-sector').textContent = d.type;
document.getElementById('overlay-type-label').textContent = d.type;
document.getElementById('overlay-title').textContent = d.title;
document.getElementById('layer-nav').innerHTML = d.layers.map((l,i) =>
`<button class="layer-tab${i===0?' active':''}" onclick="switchLayer(${i})">${l.tab}</button>`
).join('');
document.getElementById('overlay-body').innerHTML = d.layers.map((l,i) =>
`<div class="layer-panel${i===0?' active':''}" id="lp-${i}">${l.html}</div>`
).join('');
document.getElementById('overlay-backdrop').classList.add('open');
document.getElementById('overlay-panel').classList.add('open');
document.body.style.overflow = 'hidden';
}
function switchLayer(i) {
document.querySelectorAll('.layer-tab').forEach((t,idx) => t.classList.toggle('active', idx===i));
document.querySelectorAll('.layer-panel').forEach((p,idx) => p.classList.toggle('active', idx===i));
document.getElementById('overlay-body').scrollTop = 0;
}
function closeOverlay() {
document.getElementById('overlay-backdrop').classList.remove('open');
document.getElementById('overlay-panel').classList.remove('open');
document.body.style.overflow = '';
}
document.addEventListener('keydown', e => { if(e.key==='Escape') closeOverlay(); });
document.querySelectorAll('[role="button"]').forEach(el =>
el.addEventListener('keydown', e => { if(e.key==='Enter'||e.key===' '){e.preventDefault();el.click();}})
);
async function submitWaitlist(e, variant) {
e.preventDefault();
const sfx = variant==='bottom' ? '-bottom' : '';
const btn = document.getElementById('wl-btn'+sfx);
const inp = document.getElementById('wl-email'+sfx);
const ok = document.getElementById('wl-ok'+sfx);
const err = document.getElementById('wl-err'+sfx);
if (!inp.value.trim() || !inp.value.includes('@')) { inp.focus(); return; }
btn.disabled = true; btn.textContent = '…';
err.hidden = true;
try {
const r = await fetch('https://api.quixzoom.com/api/qz/waitlist', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ email: inp.value.trim(), country: (navigator.language||'en').split('-')[1]||'', lang: (navigator.language||'en').split('-')[0], source: location.hostname })
});
if (!r.ok) throw new Error();
inp.closest('.wl-row').style.display = 'none';
ok.hidden = false;
updateCounter();
} catch {
btn.disabled = false; btn.textContent = 'Register interest →';
err.hidden = false;
}
}
function updateCounter() {
fetch('https://api.quixzoom.com/api/qz/waitlist/count')
.then(r=>r.json()).then(d=>{
const el = document.getElementById('wl-count');
if(el && d && typeof d.count!=='undefined'){ const n = Number(d.count);
el.parentElement.style.display = n > 0 ? '' : 'none';
// Hero badge
const badge = document.getElementById('hero-signup-badge');
const heroCount = document.getElementById('hero-signup-count');
if(badge && heroCount && n > 0){ badge.style.display='flex'; heroCount.textContent=n.toLocaleString(); }
if(n>0) el.textContent = n.toLocaleString(); }
}).catch(()=>{});
}
document.addEventListener('DOMContentLoaded', updateCounter);
</script>
<script>
// ── Glossary tooltip engine ───────────────────────────────────────────
const GLOSSARY = {
'Zoomer': { short: 'A verified field data collector on the quiXzoom platform.', slug: 'zoomer' },
'mission': { short: 'A paid photography assignment with defined location, scope and compensation.', slug: 'mission' },
'submission': { short: 'A completed set of photos submitted for AI review after a mission is executed.', slug: 'submission' },
'claim': { short: 'Reserving a mission — locks it exclusively for you within a defined time window.', slug: 'claim' },
'time window': { short: 'The duration a Zoomer has to complete and submit a claimed mission.', slug: 'time-window' },
'approval rate': { short: 'Percentage of submissions that pass AI quality review. Affects tier and mission access.', slug: 'approval-rate' },
'Stripe Connect': { short: 'The payment infrastructure that converts your QX Credit balance to cash and transfers it to your bank account via weekly payouts.', slug: 'stripe-connect' },
'KYC': { short: 'Know Your Customer — the one-time identity verification required before your first payout.', slug: 'kyc' },
'tier': { short: 'Your Zoomer level (Supplementary / Active / Professional) based on volume and approval rate.', slug: 'tier' },
'surge': { short: 'High-urgency missions deployed after events like storms or floods. Pay 25× standard rate.', slug: 'surge-mission' },
'AI review': { short: 'Automated quality and specification check run on every submission before payment.', slug: 'ai-review' },
'geo-tagged': { short: 'Images embedded with GPS coordinates — verified against mission location parameters.', slug: 'geo-tagged' },
'GDPR': { short: 'EU data protection regulation governing how quiXzoom handles your personal data.', slug: 'gdpr' },
'SLA': { short: 'Service Level Agreement — the delivery timeline and quality commitments made to orderers.', slug: 'sla' },
'API': { short: 'Application Programming Interface — how orderers programmatically access mission data.', slug: 'api' },
'webhook': { short: 'Automatic HTTP notification sent to orderers when a submission is approved.', slug: 'webhook' },
'IBAN': { short: 'International Bank Account Number — required for Stripe Connect payout setup.', slug: 'iban' },
'SEPA': { short: 'Single Euro Payments Area — the European bank transfer network used for EUR payouts.', slug: 'sepa' },
'VAT': { short: 'Value Added Tax — Zoomers are responsible for their own VAT obligations where applicable.', slug: 'vat' },
'payout': { short: 'Weekly transfer of QX Credits converted to cash, to your linked bank account. Requires reaching the minimum Credit threshold.', slug: 'payout' },
'rating': { short: 'Your accumulated quality score on the platform, affecting tier and mission access.', slug: 'rating' },
'specialisation': { short: 'A certified competency unlocking access to specific premium mission types.', slug: 'specialisation' },
'liveness check': { short: 'Selfie-based verification confirming a real person is behind the identity document.', slug: 'liveness-check' },
'AML': { short: 'Anti-Money Laundering regulation — the legal basis for KYC requirements on payment platforms.', slug: 'aml' },
};
const tip = document.createElement('div');
tip.className = 'gtooltip';
tip.innerHTML = '<span class="gtooltip-term"></span><span class="gtooltip-def"></span><a class="gtooltip-more">Full definition →</a>';
document.body.appendChild(tip);
document.addEventListener('mouseover', e => {
const el = e.target.closest('.gterm');
if (!el) return;
const key = el.dataset.gterm;
const g = GLOSSARY[key];
if (!g) return;
tip.querySelector('.gtooltip-term').textContent = key;
tip.querySelector('.gtooltip-def').textContent = g.short;
tip.querySelector('.gtooltip-more').href = '/glossary#' + g.slug;
tip.classList.add('visible');
});
document.addEventListener('mousemove', e => {
if (!tip.classList.contains('visible')) return;
const x = e.clientX + 12;
const y = e.clientY - 60;
tip.style.left = Math.min(x, window.innerWidth - 300) + 'px';
tip.style.top = Math.max(8, y) + 'px';
});
document.addEventListener('mouseout', e => {
if (!e.target.closest('.gterm')) return;
tip.classList.remove('visible');
});
</script>
<script>
// ── Scroll fade-in observer ───────────────────────────────────
(function(){
const obs = new IntersectionObserver((entries) => {
entries.forEach(e => {
if(e.isIntersecting){ e.target.classList.add('visible'); obs.unobserve(e.target); }
});
}, { threshold: 0.12 });
document.querySelectorAll('.fade-in').forEach(el => obs.observe(el));
// Staggered delay för card-groups
document.querySelectorAll('.steps-grid,.personas-grid,.stats-grid,.missions-grid,.earnings-grid').forEach(grid => {
grid.querySelectorAll('.fade-in').forEach((el,i) => {
el.style.transitionDelay = (i * 0.08) + 's';
});
});
})();
</script>
<script>
function toggleMenu(){
const h=document.getElementById('hamburger');
const m=document.getElementById('mobile-menu');
h.classList.toggle('open');
m.classList.toggle('open');
document.body.style.overflow=m.classList.contains('open')?'hidden':'';
}
function closeMenu(){
document.getElementById('hamburger').classList.remove('open');
document.getElementById('mobile-menu').classList.remove('open');
document.body.style.overflow='';
}
// Close menu on resize to desktop
window.addEventListener('resize',()=>{if(window.innerWidth>768)closeMenu();});
function toggleDrop(btn) {
const li = btn.closest('li');
const wasOpen = li.classList.contains('open');
// Stäng alla
document.querySelectorAll('.nav-links > li.open').forEach(el => el.classList.remove('open'));
if (!wasOpen) li.classList.add('open');
}
// Stäng dropdown vid klick utanför
document.addEventListener('click', e => {
if (!e.target.closest('.nav-links')) {
document.querySelectorAll('.nav-links > li.open').forEach(el => el.classList.remove('open'));
}
});
</script>
<!-- Brand disambiguation — structured for Google -->
<section style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap" aria-hidden="true">
<h2>What is quiXzoom?</h2>
<p>quiXzoom is a crowdsourced field data collection platform. Contributors called Zoomers photograph infrastructure, buildings and urban environments on demand and earn QX Credits per approved submission. quiXzoom is not related to Zoom video conferencing.</p>
<h2>How is quiXzoom spelled?</h2>
<p>The brand is spelled quiXzoom with a capital X in the middle. Common misspellings include quixzoom, QuiXzoom, quick zoom, quix zoom and qui xzoom — all refer to the same platform. quiXzoom has no connection to Zoom video conferencing or camera zoom functions. Launching August 2026.</p>
<h2>quiXzoom quick zoom</h2>
<p>quiXzoom is not "quick zoom". quiXzoom is a crowdsourced field data collection platform — not a video conferencing tool.</p>
<h2>What does a Zoomer do on quiXzoom?</h2>
<p>A Zoomer photographs locations and objects near their position based on active missions in the quiXzoom app. Each approved submission adds QX Credits to the Zoomer's balance, paid out weekly via Stripe Connect.</p>
</section>
<script>
document.addEventListener('click', function(e){
var ms = document.getElementById('market-selector');
if(ms && !ms.contains(e.target)) ms.classList.remove('open');
});
/* Sprint 3: dynamic tax note */
document.addEventListener('qx:market', function(e){
var d = window.QX_LOCALES[e.detail.market];
if(!d) return;
var el = document.getElementById('qx-tax-note');
if(el) el.textContent = d.taxNote;
});
</script>
<script id="qx-geo-script">
(function(){
if(document.cookie.indexOf('qx_market=')>=0)return;
if(sessionStorage.getItem('qx_geo_dismissed'))return;
var hints={'SE':'SE','NL':'NL','TH':'TH','NG':'NG','KE':'KE'};
var tz=Intl.DateTimeFormat().resolvedOptions().timeZone||'';
var map={'Europe/Stockholm':'SE','Europe/Amsterdam':'NL','Asia/Bangkok':'TH','Africa/Lagos':'NG','Africa/Nairobi':'KE'};
var names={'SE':'Sweden','NL':'Netherlands','TH':'Thailand','NG':'Nigeria','KE':'Kenya'};
var detected=map[tz];
if(!detected||detected==='SE')return;
var b=document.createElement('div');
b.id='qx-geo-banner';
b.innerHTML='<span>Looks like you&#39;re in '+names[detected]+'</span><button class="geo-switch" onclick="QX_setMarket(''+detected+'');this.closest('#qx-geo-banner').remove()">Switch to '+names[detected]+'</button><button class="geo-dismiss" onclick="sessionStorage.setItem('qx_geo_dismissed',1);this.closest('#qx-geo-banner').remove()" aria-label="Dismiss">×</button>';
document.addEventListener('DOMContentLoaded',function(){document.body.appendChild(b);});
}());
</script>
</body>
</html>
</html>
@@ -61,18 +61,18 @@
position: relative; z-index: 1;
}
.hero-badge {
background: rgba(255,255,255,0.1); border-radius: 20px; padding: 4px 14px;
background: rgba(255,255,255,0.1); border-radius: var(--radius-xl); padding: 4px 14px;
font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.8);
border: 0.5px solid rgba(255,255,255,0.2);
}
/* TOC */
.toc { margin: 20px 16px; background: var(--background-secondary); border-radius: 14px; overflow: hidden; border: 0.5px solid var(--separator); }
.toc { margin: 20px 16px; background: var(--background-secondary); border-radius: var(--radius-lg); overflow: hidden; border: 0.5px solid var(--separator); }
.toc-title { padding: 14px 16px; font-size: 13px; font-weight: 600; color: var(--label-secondary); text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 0.5px solid var(--separator); }
.toc-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-bottom: 0.5px solid var(--separator); cursor: pointer; text-decoration: none; }
.toc-item:last-child { border-bottom: none; }
.toc-item:active { background: var(--fill-quaternary); }
.toc-num { width: 26px; height: 26px; border-radius: 50%; background: var(--ios-blue); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; flex-shrink: 0; }
.toc-num { width: 26px; height: 26px; border-radius: var(--radius-full); background: var(--ios-blue); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; flex-shrink: 0; }
.toc-label { font-size: 15px; font-weight: 500; flex: 1; }
.toc-chevron { color: var(--label-tertiary); font-size: 13px; }
@@ -92,7 +92,7 @@
/* Code block */
.code-block {
margin: 16px 16px 0;
background: #F2F2F7; border-radius: 14px; overflow: hidden;
background: #F2F2F7; border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.code-header {
@@ -102,7 +102,7 @@
border-bottom: 0.5px solid var(--separator);
}
.code-lang { font-size: 12px; color: var(--label-tertiary); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.code-copy { background: none; border: none; color: var(--ios-blue); font-size: 12px; font-weight: 600; cursor: pointer; padding: 2px 6px; border-radius: 6px; }
.code-copy { background: none; border: none; color: var(--ios-blue); font-size: 12px; font-weight: 600; cursor: pointer; padding: 2px 6px; border-radius: var(--radius-sm); }
.code-copy:active { background: var(--fill-secondary); }
pre {
padding: 16px; font-family: 'SF Mono', 'Menlo', 'Monaco', monospace;
@@ -119,7 +119,7 @@
/* Info card */
.info-card {
margin: 16px 16px 0;
padding: 14px 16px; border-radius: 14px;
padding: 14px 16px; border-radius: var(--radius-lg);
border-left: 3px solid var(--ios-blue);
background: rgba(0,122,255,0.1);
font-size: 14px; line-height: 1.6; color: var(--label-secondary);
@@ -131,7 +131,7 @@
/* Step list */
.step-list { margin: 16px 16px 0; display: flex; flex-direction: column; gap: 12px; }
.step-item { display: flex; gap: 14px; align-items: flex-start; }
.step-dot { width: 28px; height: 28px; border-radius: 50%; background: var(--ios-blue); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700; flex-shrink: 0; margin-top: 2px; }
.step-dot { width: 28px; height: 28px; border-radius: var(--radius-full); background: var(--ios-blue); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700; flex-shrink: 0; margin-top: 2px; }
.step-content { flex: 1; }
.step-title { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
.step-desc { font-size: 14px; color: var(--label-secondary); line-height: 1.5; }
@@ -139,7 +139,7 @@
/* Endpoint table */
.endpoint-card {
margin: 16px 16px 0;
background: var(--background-secondary); border-radius: 14px;
background: var(--background-secondary); border-radius: var(--radius-lg);
overflow: hidden; border: 0.5px solid var(--separator);
}
.endpoint-row {
@@ -148,7 +148,7 @@
}
.endpoint-row:last-child { border-bottom: none; }
.method {
font-size: 11px; font-weight: 800; padding: 3px 8px; border-radius: 6px;
font-size: 11px; font-weight: 800; padding: 3px 8px; border-radius: var(--radius-sm);
text-transform: uppercase; letter-spacing: 0.3px; flex-shrink: 0;
}
.method.get { background: rgba(52,199,89,0.15); color: var(--ios-green); }
@@ -161,7 +161,7 @@
/* Checklist */
.checklist { margin: 16px 16px 0; display: flex; flex-direction: column; gap: 10px; }
.check-item { display: flex; align-items: flex-start; gap: 10px; }
.check-box { width: 22px; height: 22px; border-radius: 50%; background: rgba(52,199,89,0.15); color: var(--ios-green); display: flex; align-items: center; justify-content: center; font-size: 13px; flex-shrink: 0; margin-top: 1px; }
.check-box { width: 22px; height: 22px; border-radius: var(--radius-full); background: rgba(52,199,89,0.15); color: var(--ios-green); display: flex; align-items: center; justify-content: center; font-size: 13px; flex-shrink: 0; margin-top: 1px; }
.check-text { font-size: 14px; color: var(--label-secondary); line-height: 1.5; }
.check-text strong { color: var(--label-primary); }
@@ -204,7 +204,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</nav>
<div class="guide-hero">
<div class="hero-icon">📱</div>
<div class="hero-icon"></div>
<div class="hero-title">quiXzoom iOS Integration</div>
<div class="hero-sub">Teknisk guide för native iOS-appen<br>SwiftUI · WKWebView · REST API</div>
<div class="hero-badges">
@@ -261,7 +261,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</a>
</div>
<!-- ─────────────────────────── SECTION 1 ─────────────────────────── -->
<!-- SECTION 1 -->
<div class="guide-section" id="s1">
<div class="section-anchor">
<div class="section-num">01 — Arkitektur</div>
@@ -288,34 +288,34 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<pre>
<span class="cm">// QuiXzoom iOS — Projektstruktur</span>
QuiXzoom/
├── App/
├── <span class="fn">QuiXzoomApp</span>.swift <span class="cm">// @main</span>
└── <span class="fn">AppDelegate</span>.swift <span class="cm">// APNs + deep links</span>
├── Features/
├── Auth/
├── <span class="fn">BankIDView</span>.swift
└── <span class="fn">AuthViewModel</span>.swift
├── Missions/
├── <span class="fn">MissionFeedView</span>.swift
├── <span class="fn">MissionMapView</span>.swift <span class="cm">// MapKit</span>
└── <span class="fn">ActiveMissionView</span>.swift
├── Camera/
└── <span class="fn">CameraUploadView</span>.swift
├── Wallet/
└── <span class="fn">WalletView</span>.swift
└── Profile/
└── <span class="fn">ProfileView</span>.swift
├── Services/
├── <span class="fn">APIClient</span>.swift <span class="cm">// REST + JWT</span>
├── <span class="fn">LocationService</span>.swift
└── <span class="fn">PushService</span>.swift
└── DesignSystem/
└── <span class="fn">Tokens</span>.swift <span class="cm">// iOS 18 native tokens</span>
App/
<span class="fn">QuiXzoomApp</span>.swift <span class="cm">// @main</span>
<span class="fn">AppDelegate</span>.swift <span class="cm">// APNs + deep links</span>
Features/
Auth/
<span class="fn">BankIDView</span>.swift
<span class="fn">AuthViewModel</span>.swift
Missions/
<span class="fn">MissionFeedView</span>.swift
<span class="fn">MissionMapView</span>.swift <span class="cm">// MapKit</span>
<span class="fn">ActiveMissionView</span>.swift
Camera/
<span class="fn">CameraUploadView</span>.swift
Wallet/
<span class="fn">WalletView</span>.swift
Profile/
<span class="fn">ProfileView</span>.swift
Services/
<span class="fn">APIClient</span>.swift <span class="cm">// REST + JWT</span>
<span class="fn">LocationService</span>.swift
<span class="fn">PushService</span>.swift
DesignSystem/
<span class="fn">Tokens</span>.swift <span class="cm">// iOS 18 native tokens</span>
</pre>
</div>
</div>
<!-- ─────────────────────────── SECTION 2 ─────────────────────────── -->
<!-- SECTION 2 -->
<div class="guide-section" id="s2">
<div class="section-anchor">
<div class="section-num">02 — Auth</div>
@@ -394,7 +394,7 @@ class</span> <span class="tp">AuthViewModel</span>: <span class="tp">ObservableO
</div>
</div>
<!-- ─────────────────────────── SECTION 3 ─────────────────────────── -->
<!-- SECTION 3 -->
<div class="guide-section" id="s3">
<div class="section-anchor">
<div class="section-num">03 — API</div>
@@ -464,7 +464,7 @@ class</span> <span class="tp">AuthViewModel</span>: <span class="tp">ObservableO
</div>
</div>
<!-- ─────────────────────────── SECTION 4 ─────────────────────────── -->
<!-- SECTION 4 -->
<div class="guide-section" id="s4">
<div class="section-anchor">
<div class="section-num">04 — Push</div>
@@ -516,7 +516,7 @@ class</span> <span class="tp">AuthViewModel</span>: <span class="tp">ObservableO
</div>
</div>
<!-- ─────────────────────────── SECTION 5 ─────────────────────────── -->
<!-- SECTION 5 -->
<div class="guide-section" id="s5">
<div class="section-anchor">
<div class="section-num">05 — Kamera</div>
@@ -528,7 +528,7 @@ class</span> <span class="tp">AuthViewModel</span>: <span class="tp">ObservableO
</div>
<div class="info-card warning">
<strong>⚠️ Privacy Manifest krävs:</strong> Lägg till <code style="font-family:monospace">NSCameraUsageDescription</code>
<strong><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Privacy Manifest krävs:</strong> Lägg till <code style="font-family:monospace">NSCameraUsageDescription</code>
och <code style="font-family:monospace">NSLocationWhenInUseUsageDescription</code> i Info.plist med
motiveringar på svenska (krävs för App Store Review).
</div>
@@ -585,7 +585,7 @@ class</span> <span class="tp">AuthViewModel</span>: <span class="tp">ObservableO
</div>
</div>
<!-- ─────────────────────────── SECTION 6 ─────────────────────────── -->
<!-- SECTION 6 -->
<div class="guide-section" id="s6">
<div class="section-anchor">
<div class="section-num">06 — Plats</div>
@@ -597,7 +597,7 @@ class</span> <span class="tp">AuthViewModel</span>: <span class="tp">ObservableO
</div>
<div class="info-card success">
<strong> Background modes:</strong> Aktivera <code style="font-family:monospace">location</code>
<strong><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Background modes:</strong> Aktivera <code style="font-family:monospace">location</code>
i Background Modes capability för spårning under aktivt uppdrag. Motivera i Privacy Manifest.
</div>
@@ -644,7 +644,7 @@ class</span> <span class="tp">LocationService</span>: <span class="tp">NSObject<
</div>
</div>
<!-- ─────────────────────────── SECTION 7 ─────────────────────────── -->
<!-- SECTION 7 -->
<div class="guide-section" id="s7">
<div class="section-anchor">
<div class="section-num">07 — Deep Links</div>
@@ -668,7 +668,7 @@ class</span> <span class="tp">LocationService</span>: <span class="tp">NSObject<
</div>
</div>
<!-- ─────────────────────────── SECTION 8 ─────────────────────────── -->
<!-- SECTION 8 -->
<div class="guide-section" id="s8">
<div class="section-anchor">
<div class="section-num">08 — Checklista</div>
@@ -676,18 +676,18 @@ class</span> <span class="tp">LocationService</span>: <span class="tp">NSObject<
</div>
<div class="checklist">
<div class="check-item"><div class="check-box"></div><div class="check-text"><strong>Privacy Manifest</strong> — PrivacyInfo.xcprivacy med alla API-anledningar deklarerade</div></div>
<div class="check-item"><div class="check-box"></div><div class="check-text"><strong>BankID integration</strong> — Testad mot BankID Test-miljö</div></div>
<div class="check-item"><div class="check-box"></div><div class="check-text"><strong>App Tracking Transparency</strong> — ATT-dialog om analytics används</div></div>
<div class="check-item"><div class="check-box"></div><div class="check-text"><strong>Åldersrating</strong> — Minst 17+ (finansiell tjänst, gig economy)</div></div>
<div class="check-item"><div class="check-box"></div><div class="check-text"><strong>Crashlytics / Sentry</strong> — Felrapportering aktiv</div></div>
<div class="check-item"><div class="check-box"></div><div class="check-text"><strong>GDPR-popup</strong> — Dataskyddspolicy acceptansflöde vid första start</div></div>
<div class="check-item"><div class="check-box"></div><div class="check-text"><strong>Bildkomprimering</strong> — Max 2MB per bild, EXIF-stripning aktiverad</div></div>
<div class="check-item"><div class="check-box"></div><div class="check-text"><strong>Offline-hantering</strong> — Graceful degradation utan nätverksanslutning</div></div>
<div class="check-item"><div class="check-box"></div><div class="check-text"><strong>Dynamic Type</strong> — Alla textstorl. skalar med iOS-inställningar</div></div>
<div class="check-item"><div class="check-box"></div><div class="check-text"><strong>Dark/Light mode</strong> — Testad i båda lägena (primärt: dark mode)</div></div>
<div class="check-item"><div class="check-box"></div><div class="check-text"><strong>Notarization</strong> — App notariserad via Xcode Organizer</div></div>
<div class="check-item"><div class="check-box"></div><div class="check-text"><strong>TestFlight</strong> — Minst 10 externa testare godkänt bygget</div></div>
<div class="check-item"><div class="check-box"><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 class="check-text"><strong>Privacy Manifest</strong> — PrivacyInfo.xcprivacy med alla API-anledningar deklarerade</div></div>
<div class="check-item"><div class="check-box"><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 class="check-text"><strong>BankID integration</strong> — Testad mot BankID Test-miljö</div></div>
<div class="check-item"><div class="check-box"><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 class="check-text"><strong>App Tracking Transparency</strong> — ATT-dialog om analytics används</div></div>
<div class="check-item"><div class="check-box"><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 class="check-text"><strong>Åldersrating</strong> — Minst 17+ (finansiell tjänst, gig economy)</div></div>
<div class="check-item"><div class="check-box"><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 class="check-text"><strong>Crashlytics / Sentry</strong> — Felrapportering aktiv</div></div>
<div class="check-item"><div class="check-box"><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 class="check-text"><strong>GDPR-popup</strong> — Dataskyddspolicy acceptansflöde vid första start</div></div>
<div class="check-item"><div class="check-box"><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 class="check-text"><strong>Bildkomprimering</strong> — Max 2MB per bild, EXIF-stripning aktiverad</div></div>
<div class="check-item"><div class="check-box"><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 class="check-text"><strong>Offline-hantering</strong> — Graceful degradation utan nätverksanslutning</div></div>
<div class="check-item"><div class="check-box"><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 class="check-text"><strong>Dynamic Type</strong> — Alla textstorl. skalar med iOS-inställningar</div></div>
<div class="check-item"><div class="check-box"><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 class="check-text"><strong>Dark/Light mode</strong> — Testad i båda lägena (primärt: dark mode)</div></div>
<div class="check-item"><div class="check-box"><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 class="check-text"><strong>Notarization</strong> — App notariserad via Xcode Organizer</div></div>
<div class="check-item"><div class="check-box"><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 class="check-text"><strong>TestFlight</strong> — Minst 10 externa testare godkänt bygget</div></div>
</div>
<div style="height:32px;"></div>
@@ -697,7 +697,7 @@ class</span> <span class="tp">LocationService</span>: <span class="tp">NSObject<
function copyCode(btn) {
const pre = btn.closest('.code-block').querySelector('pre');
navigator.clipboard.writeText(pre.innerText || pre.textContent).then(() => {
btn.textContent = ' Kopierat';
btn.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> Kopierat';
setTimeout(() => btn.textContent = 'Kopiera', 2000);
});
}
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>iOS Readiness — quiXzoom</title>
<style>
/* ── iOS 26 / Apple Native — Dark Mode ───────────────────────────────── */
/* iOS 26 / Apple Native — Dark Mode */
:root {
--bg: #000000;
--bg-secondary: #1c1c1e;
@@ -43,7 +43,7 @@
overscroll-behavior-y: contain;
}
/* ── Nav Bar ─────────────────────────────────────────────────────────── */
/* Nav Bar */
.nav-bar {
position: sticky;
top: 0;
@@ -68,13 +68,13 @@
font-weight: 500;
font-family: var(--font);
padding: 6px 12px;
border-radius: 20px;
border-radius: var(--radius-xl);
cursor: pointer;
transition: opacity 0.15s;
}
.refresh-btn:active { opacity: 0.6; }
/* ── Hero Verdict ────────────────────────────────────────────────────── */
/* Hero Verdict */
.verdict-hero {
margin: 20px 16px 0;
border-radius: var(--radius-lg);
@@ -117,7 +117,7 @@
margin-top: 4px;
}
/* ── Stat Grid ───────────────────────────────────────────────────────── */
/* Stat Grid */
.stat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
@@ -162,7 +162,7 @@
letter-spacing: 0.3px;
}
/* ── Section ─────────────────────────────────────────────────────────── */
/* Section */
.section { margin-top: 28px; }
.section-hdr {
display: flex;
@@ -183,7 +183,7 @@
color: var(--label-tertiary);
}
/* ── List ────────────────────────────────────────────────────────────── */
/* List */
.list {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
@@ -201,7 +201,7 @@
.row-icon {
width: 32px; height: 32px;
border-radius: 8px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 16px;
flex-shrink: 0;
@@ -237,7 +237,7 @@
font-size: 12px;
font-weight: 600;
padding: 4px 10px;
border-radius: 20px;
border-radius: var(--radius-xl);
}
.pill-green { background: rgba(48,209,88,0.18); color: var(--green); }
.pill-red { background: rgba(255,69,58,0.18); color: var(--red); }
@@ -245,24 +245,24 @@
.pill-blue { background: rgba(0,122,255,0.18); color: var(--tint); }
.pill-grey { background: var(--fill); color: var(--label-secondary); }
.dot { width: 7px; height: 7px; border-radius: 50%; }
.dot { width: 7px; height: 7px; border-radius: var(--radius-full); }
.dot-green { background: var(--green); }
.dot-red { background: var(--red); }
.dot-yellow { background: var(--yellow); }
.dot-grey { background: var(--label-tertiary); }
/* ── Progress Bar ────────────────────────────────────────────────────── */
/* Progress Bar */
.progress-wrap { padding: 0 16px; }
.progress-bar-bg {
background: var(--fill);
border-radius: 4px;
border-radius: var(--radius-sm);
height: 8px;
overflow: hidden;
margin-top: 8px;
}
.progress-bar-fill {
height: 100%;
border-radius: 4px;
border-radius: var(--radius-sm);
transition: width 0.6s ease;
background: var(--green);
}
@@ -274,7 +274,7 @@
margin-top: 6px;
}
/* ── Blocker ─────────────────────────────────────────────────────────── */
/* Blocker */
.blocker-card {
background: rgba(255,69,58,0.08);
border: 1px solid rgba(255,69,58,0.25);
@@ -292,7 +292,7 @@
.blocker-desc { font-size: 13px; color: var(--label-secondary); margin-top: 3px; }
.blocker-owner { font-size: 12px; color: var(--label-tertiary); margin-top: 4px; }
/* ── Warning ──────────────────────────────────────────────────────────── */
/* Warning */
.warning-card {
background: rgba(255,214,10,0.08);
border: 1px solid rgba(255,214,10,0.25);
@@ -309,7 +309,7 @@
.warning-title { font-size: 15px; font-weight: 600; color: var(--yellow); }
.warning-desc { font-size: 13px; color: var(--label-secondary); margin-top: 3px; }
/* ── Deploy checklist ────────────────────────────────────────────────── */
/* Deploy checklist */
.check-item {
display: flex;
align-items: flex-start;
@@ -320,7 +320,7 @@
.check-item:last-child { border-bottom: none; }
.check-box {
width: 22px; height: 22px;
border-radius: 50%;
border-radius: var(--radius-full);
border: 2px solid var(--separator);
flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
@@ -335,7 +335,7 @@
.check-title { font-size: 15px; font-weight: 500; color: var(--label); }
.check-sub { font-size: 13px; color: var(--label-secondary); margin-top: 2px; }
/* ── Footer ──────────────────────────────────────────────────────────── */
/* Footer */
.footer {
text-align: center;
padding: 32px 20px 12px;
@@ -343,13 +343,13 @@
color: var(--label-tertiary);
}
/* ── Spinner ──────────────────────────────────────────────────────────── */
/* Spinner */
@keyframes spin { to { transform: rotate(360deg); } }
.spinner {
width: 16px; height: 16px;
border: 2px solid var(--fill);
border-top-color: var(--tint);
border-radius: 50%;
border-radius: var(--radius-full);
animation: spin 0.8s linear infinite;
display: inline-block;
}
@@ -393,7 +393,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Nav Bar -->
<div class="nav-bar">
<div class="nav-title">📡 iOS Readiness</div>
<div class="nav-title"> iOS Readiness</div>
<div class="nav-right">
<span class="nav-time" id="navTime"></span>
<button class="refresh-btn" onclick="runCheck()">↻ Refresh</button>
@@ -458,7 +458,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Blockers -->
<div class="section">
<div class="section-hdr">
<span class="section-title">🚨 Kända blockers</span>
<span class="section-title"> Kända blockers</span>
<span class="section-badge" id="blockerCount"></span>
</div>
<div id="blockerList"></div>
@@ -467,7 +467,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Warnings -->
<div class="section">
<div class="section-hdr">
<span class="section-title">⚠️ Varningar</span>
<span class="section-title"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Varningar</span>
<span class="section-badge" id="warningCount"></span>
</div>
<div id="warningList"></div>
@@ -497,64 +497,64 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<script>
// ── Data: Backend Features ───────────────────────────────────────────────
// Data: Backend Features
const FEATURES = [
{ name: 'BankID Authentication', icon: '🔐', status: 'done', note: 'Staging verifierat' },
{ name: 'Zoomer Registration', icon: '👤', status: 'done', note: 'POST /api/quixzoom/zoomers' },
{ name: 'Mission CRUD', icon: '📋', status: 'done', note: 'Full create/read/update' },
{ name: 'Geolocation Matching', icon: '📍', status: 'done', note: 'Radius-sökning aktiv' },
{ name: 'Real-time Push (APNs)', icon: '🔔', status: 'warn', note: 'APNs cert upphör aug 2026' },
{ name: 'Earnings & Payouts', icon: '💰', status: 'done', note: 'Beräkningar OK, utbetalning pending' },
{ name: 'KYC Document Upload', icon: '🪪', status: 'done', note: 'S3 + validering OK' },
{ name: 'Rating & Reviews', icon: '', status: 'done', note: 'Bi-directional ratings' },
{ name: 'Portfolio / Media', icon: '🖼️', status: 'done', note: 'Upload + CDN aktiv' },
{ name: 'Stripe Payouts', icon: '💳', status: 'warn', note: 'Testläge — ej prod-nyckel' },
{ name: 'DAC7 Reporting', icon: '📊', status: 'done', note: 'Automatisk generering OK' },
{ name: 'GDPR / Data Deletion', icon: '🗑️', status: 'pending', note: 'Legal review pågår' },
{ name: 'BankID Authentication', icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7" width="10" height="7" rx="1" stroke="#666" stroke-width="1.5"/><path d="M5 7V5C5 3.3 6.3 2 8 2C9.7 2 11 3.3 11 5V7" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="10.5" r="1" fill="#666"/></svg>', status: 'done', note: 'Staging verifierat' },
{ name: 'Zoomer Registration', icon: '', status: 'done', note: 'POST /api/quixzoom/zoomers' },
{ name: 'Mission CRUD', 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>', status: 'done', note: 'Full create/read/update' },
{ name: 'Geolocation Matching', icon: '', status: 'done', note: 'Radius-sökning aktiv' },
{ name: 'Real-time Push (APNs)', icon: '', status: 'warn', note: 'APNs cert upphör aug 2026' },
{ name: 'Earnings & Payouts', icon: '', status: 'done', note: 'Beräkningar OK, utbetalning pending' },
{ name: 'KYC Document Upload', icon: '', status: 'done', note: 'S3 + validering OK' },
{ name: 'Rating & Reviews', icon: '', status: 'done', note: 'Bi-directional ratings' },
{ name: 'Portfolio / Media', icon: '', status: 'done', note: 'Upload + CDN aktiv' },
{ name: 'Stripe Payouts', icon: '', status: 'warn', note: 'Testläge — ej prod-nyckel' },
{ name: 'DAC7 Reporting', icon: '', status: 'done', note: 'Automatisk generering OK' },
{ name: 'GDPR / Data Deletion', icon: '', status: 'pending', note: 'Legal review pågår' },
];
// ── Data: Blockers ───────────────────────────────────────────────────────
// Data: Blockers
const BLOCKERS = [
{
icon: '🔑',
icon: '',
title: 'Stripe Production Keys saknas',
desc: 'Utbetalningar kan ej köras i prod-miljö utan live Stripe-nycklar.',
owner: 'Ägare: Erik / Ops · Deadline: June 1'
},
{
icon: '📱',
icon: '',
title: 'APNs Production Certificate',
desc: 'Push-notiser till iOS kräver prod APNs-certifikat i staging-testet.',
owner: 'Ägare: iOS team · Deadline: May 30'
},
{
icon: '🇸🇪',
icon: '<span class="flag-se">SE</span>',
title: 'BankID Prod-avtal ej signerat',
desc: 'Testmiljö körs mot Sandbox. Prod-avtal med Finansiell ID-Teknik AB krävs.',
owner: 'Ägare: Erik · Deadline: May 28'
},
];
// ── Data: Warnings ───────────────────────────────────────────────────────
// Data: Warnings
const WARNINGS = [
{
icon: '',
icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg>',
title: 'Rate limits ej konfigurerade för iOS',
desc: 'API-rate-limiting behöver tunas för mobila request-mönster.'
},
{
icon: '📦',
icon: '',
title: 'App Store metadata ej uppladdad',
desc: 'Screenshots, beskrivning och metadata saknas i App Store Connect.'
},
{
icon: '🔒',
icon: '',
title: 'SSL-cert auto-renewal ej verifierat',
desc: 'Kontrollera att certbot renew fungerar för amos.wavult.com.'
},
];
// ── Data: Deploy Checklist ───────────────────────────────────────────────
// Data: Deploy Checklist
const DEPLOY_ITEMS = [
{ title: 'API endpoints testade mot iOS simulator', sub: 'Backend', status: 'done' },
{ title: 'BankID Sandbox-integration verifierad', sub: 'Backend', status: 'done' },
@@ -573,7 +573,7 @@ const DEPLOY_ITEMS = [
{ title: 'GDPR Data Deletion verifierad', sub: 'Legal', status: 'pending' },
];
// ── Endpoints att kontrollera ─────────────────────────────────────────────
// Endpoints att kontrollera
const ENDPOINTS = [
{ label: 'Zoomer List', method: 'GET', url: 'https://api.quixzoom.com/api/qz/zoomers', critical: true },
{ label: 'Zoomer Profile', method: 'GET', url: 'https://api.quixzoom.com/api/qz/zoomers/zoomer-001/profile', critical: true },
@@ -584,15 +584,15 @@ const ENDPOINTS = [
{ label: 'Health Check', method: 'GET', url: '/api/health', critical: true },
];
// ── State ─────────────────────────────────────────────────────────────────
// State
let results = {};
// ── Render helpers ────────────────────────────────────────────────────────
// Render helpers
function statusClass(s) {
return s === 'done' ? 'green' : s === 'warn' ? 'yellow' : s === 'fail' ? 'red' : 'grey';
}
function statusIcon(s) {
return s === 'done' ? '' : s === 'warn' ? '!' : s === 'fail' ? '' : '';
return s === 'done' ? '<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>' : s === 'warn' ? '!' : s === 'fail' ? '' : '';
}
function statusPill(s, label) {
const cls = s === 'done' ? 'pill-green' : s === 'warn' ? 'pill-yellow' : s === 'fail' ? 'pill-red' : 'pill-grey';
@@ -600,7 +600,7 @@ function statusPill(s, label) {
return `<span class="status-pill ${cls}">${dot} ${label}</span>`;
}
// ── Render static sections ────────────────────────────────────────────────
// Render static sections
function renderFeatures() {
const el = document.getElementById('featureList');
el.innerHTML = FEATURES.map(f => `
@@ -660,7 +660,7 @@ function renderDeploy() {
el.innerHTML = DEPLOY_ITEMS.map(item => `
<div class="check-item">
<div class="check-box ${item.status === 'done' ? 'done' : item.status === 'fail' ? 'fail' : item.status === 'warn' ? 'warn' : 'pending'}">
${item.status === 'done' ? '' : item.status === 'fail' ? '' : item.status === 'warn' ? '!' : ''}
${item.status === 'done' ? '<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>' : item.status === 'fail' ? '' : item.status === 'warn' ? '!' : ''}
</div>
<div class="check-body">
<div class="check-title">${item.title}</div>
@@ -670,7 +670,7 @@ function renderDeploy() {
`).join('');
}
// ── Check an endpoint ─────────────────────────────────────────────────────
// Check an endpoint
async function checkEndpoint(ep) {
const t0 = Date.now();
try {
@@ -681,7 +681,7 @@ async function checkEndpoint(ep) {
}
}
// ── Main check ────────────────────────────────────────────────────────────
// Main check
async function runCheck() {
// Reset
document.getElementById('verdictHero').className = 'verdict-hero loading';
@@ -721,13 +721,13 @@ async function runCheck() {
okCount++;
const color = ep.critical ? 'green' : 'blue';
iconEl.className = `row-icon ${color}`;
iconEl.textContent = '';
iconEl.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>';
subEl.textContent = `HTTP ${res.status} · ${res.ms}ms`;
rightEl.innerHTML = `<span class="status-pill pill-green"><span class="dot dot-green"></span> OK</span>`;
} else {
failCount++;
iconEl.className = 'row-icon red';
iconEl.textContent = '';
iconEl.textContent = '';
subEl.textContent = res.error ? `Error: ${res.error}` : `HTTP ${res.status} · ${res.ms}ms`;
rightEl.innerHTML = `<span class="status-pill pill-red"><span class="dot dot-red"></span> FEL</span>`;
}
@@ -750,17 +750,17 @@ async function runCheck() {
if (failCount === 0 && hardBlockers === 0) {
hero.className = 'verdict-hero ready';
icon.textContent = '';
icon.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
ans.textContent = 'JA — Backend redo!';
sub.textContent = `Alla ${ENDPOINTS.length} API:er OK · Inga blockers · Klar för iOS`;
} else if (criticalFailing > 0 || hardBlockers >= 2) {
hero.className = 'verdict-hero not-ready';
icon.textContent = '🚫';
icon.textContent = '';
ans.textContent = 'NEJ — Ej release-redo';
sub.textContent = `${criticalFailing} kritiska API-fel · ${hardBlockers} blockers kvar`;
} else {
hero.className = 'verdict-hero partial';
icon.textContent = '⚠️';
icon.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg>';
ans.textContent = 'DELVIS — Åtgärder krävs';
sub.textContent = `API:er OK · ${hardBlockers} blockers kvar att lösa`;
}
@@ -775,7 +775,7 @@ function updateTime() {
document.getElementById('footerTime').textContent = now.toLocaleString('sv-SE');
}
// ── Init ──────────────────────────────────────────────────────────────────
// Init
renderFeatures();
renderBlockers();
renderWarnings();
+150
View File
@@ -0,0 +1,150 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>quiXzoom — | Zoomer </title>
<meta name="description" content="quiXzoom 2026820">
<link rel="canonical" href="https://quixzoom.asia/ja/">
<link rel="alternate" hreflang="ja" href="https://quixzoom.asia/ja/">
<link rel="alternate" hreflang="zh-CN" href="https://quixzoom.asia/zh-cn/">
<link rel="alternate" hreflang="ko" href="https://quixzoom.asia/ko/">
<link rel="alternate" hreflang="en" href="https://www.quixzoom.com/">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', 'Noto Sans JP', sans-serif;
background: #0a0a0a;
color: #ffffff;
line-height: 1.8;
}
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
header {
padding: 24px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 24px; font-weight: 700; color: #0066ff; }
.lang-switcher { display: flex; gap: 16px; }
.lang-switcher a {
color: rgba(255,255,255,0.6);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.lang-switcher a:hover, .lang-switcher a.active { color: #fff; }
.hero {
padding: 120px 0 80px;
text-align: center;
}
.hero h1 {
font-size: clamp(2rem, 4vw, 3.5rem);
font-weight: 800;
line-height: 1.2;
margin-bottom: 24px;
letter-spacing: 0.05em;
}
.hero p {
font-size: 1.125rem;
color: rgba(255,255,255,0.7);
max-width: 600px;
margin: 0 auto 40px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 16px 32px;
background: #0066ff;
color: #fff;
text-decoration: none;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0,102,255,0.3);
}
.features {
padding: 80px 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 32px;
}
.feature {
background: rgba(255,255,255,0.05);
padding: 32px;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.1);
}
.feature h3 {
font-size: 1.125rem;
margin-bottom: 12px;
color: #fff;
}
.feature p {
color: rgba(255,255,255,0.6);
font-size: 0.875rem;
}
.currency-note {
text-align: center;
padding: 40px 0;
color: rgba(255,255,255,0.5);
font-size: 0.875rem;
}
footer {
padding: 40px 0;
text-align: center;
color: rgba(255,255,255,0.4);
font-size: 0.875rem;
border-top: 1px solid rgba(255,255,255,0.1);
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">quiXzoom</div>
<nav class="lang-switcher">
<a href="/ja/" class="active"></a>
<a href="/zh-cn/"></a>
<a href="/ko/"></a>
<a href="/en/">EN</a>
</nav>
</header>
<section class="hero">
<h1><br></h1>
<p>quiXzoom </p>
<a href="https://www.quixzoom.com/register" class="btn">Zoomer </a>
</section>
<section class="features">
<div class="feature">
<h3></h3>
<p> ¥1,000-10,000 </p>
</div>
<div class="feature">
<h3></h3>
<p>24 PayPay </p>
</div>
<div class="feature">
<h3></h3>
<p></p>
</div>
</section>
<div class="currency-note">
(JPY)
</div>
<footer>
<p>quiXzoom Asia | : asia@quixzoom.com</p>
</footer>
</div>
</body>
</html>
+7 -7
View File
@@ -67,7 +67,7 @@
}
.card {
background: var(--ios-card);
border-radius: 16px;
border-radius: var(--radius-lg);
padding: 20px;
margin-bottom: 16px;
}
@@ -83,7 +83,7 @@
.field-input {
width: 100%;
padding: 14px;
border-radius: 10px;
border-radius: var(--radius-md);
border: 1px solid var(--ios-gray);
background: var(--ios-bg);
color: var(--ios-text);
@@ -92,7 +92,7 @@
.btn {
width: 100%;
padding: 16px;
border-radius: 12px;
border-radius: var(--radius-md);
border: none;
background: var(--ios-blue);
color: white;
@@ -111,7 +111,7 @@
.feature-icon {
width: 40px;
height: 40px;
border-radius: 10px;
border-radius: var(--radius-md);
background: var(--ios-green);
display: flex;
align-items: center;
@@ -135,21 +135,21 @@
<div class="features">
<div class="feature">
<div class="feature-icon">📸</div>
<div class="feature-icon"></div>
<div class="feature-text">
<h3>Take Photos</h3>
<p>Complete missions near you with your phone</p>
</div>
</div>
<div class="feature">
<div class="feature-icon">💰</div>
<div class="feature-icon"></div>
<div class="feature-text">
<h3>Get Paid</h3>
<p>Earn $5-50 per mission, paid every Monday</p>
</div>
</div>
<div class="feature">
<div class="feature-icon">🕐</div>
<div class="feature-icon"></div>
<div class="feature-text">
<h3>Flexible Hours</h3>
<p>Work when you want, where you want</p>
+149
View File
@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>quiXzoom — . . | Zoomer </title>
<meta name="description" content="quiXzoom , , . . 2026 8 . 20 .">
<link rel="canonical" href="https://quixzoom.asia/ko/">
<link rel="alternate" hreflang="ko" href="https://quixzoom.asia/ko/">
<link rel="alternate" hreflang="zh-CN" href="https://quixzoom.asia/zh-cn/">
<link rel="alternate" hreflang="ja" href="https://quixzoom.asia/ja/">
<link rel="alternate" hreflang="en" href="https://www.quixzoom.com/">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans KR', 'Malgun Gothic', sans-serif;
background: #0a0a0a;
color: #ffffff;
line-height: 1.6;
}
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
header {
padding: 24px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 24px; font-weight: 700; color: #0066ff; }
.lang-switcher { display: flex; gap: 16px; }
.lang-switcher a {
color: rgba(255,255,255,0.6);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.lang-switcher a:hover, .lang-switcher a.active { color: #fff; }
.hero {
padding: 120px 0 80px;
text-align: center;
}
.hero h1 {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 800;
line-height: 1.2;
margin-bottom: 24px;
}
.hero p {
font-size: 1.25rem;
color: rgba(255,255,255,0.7);
max-width: 600px;
margin: 0 auto 40px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 16px 32px;
background: #0066ff;
color: #fff;
text-decoration: none;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0,102,255,0.3);
}
.features {
padding: 80px 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 32px;
}
.feature {
background: rgba(255,255,255,0.05);
padding: 32px;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.1);
}
.feature h3 {
font-size: 1.25rem;
margin-bottom: 12px;
color: #fff;
}
.feature p {
color: rgba(255,255,255,0.6);
font-size: 0.9375rem;
}
.currency-note {
text-align: center;
padding: 40px 0;
color: rgba(255,255,255,0.5);
font-size: 0.875rem;
}
footer {
padding: 40px 0;
text-align: center;
color: rgba(255,255,255,0.4);
font-size: 0.875rem;
border-top: 1px solid rgba(255,255,255,0.1);
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">quiXzoom</div>
<nav class="lang-switcher">
<a href="/ko/" class="active"></a>
<a href="/ja/"></a>
<a href="/zh-cn/"></a>
<a href="/en/">EN</a>
</nav>
</header>
<section class="hero">
<h1> .<br> .</h1>
<p>quiXzoom , , . . .</p>
<a href="https://www.quixzoom.com/register" class="btn">Zoomer </a>
</section>
<section class="features">
<div class="feature">
<h3> </h3>
<p> . ₩10,000-100,000 .</p>
</div>
<div class="feature">
<h3> </h3>
<p> 24 .</p>
</div>
<div class="feature">
<h3> </h3>
<p> . , . .</p>
</div>
</section>
<div class="currency-note">
(KRW) . .
</div>
<footer>
<p>quiXzoom Asia | : asia@quixzoom.com</p>
</footer>
</div>
</body>
</html>
+52 -52
View File
@@ -32,9 +32,9 @@
.nav-live {
font-size: 12px; font-weight: 600; padding: 4px 10px;
background: rgba(255,69,58,0.15); color: var(--ios-red);
border-radius: 20px; display: flex; align-items: center; gap: 5px;
border-radius: var(--radius-xl); display: flex; align-items: center; gap: 5px;
}
.live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ios-red); animation: pulse 1.5s infinite; }
.live-dot { width: 6px; height: 6px; border-radius: var(--radius-full); background: var(--ios-red); animation: pulse 1.5s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
/* Countdown */
@@ -48,7 +48,7 @@
text-align: center;
background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: 14px; padding: 14px 16px; min-width: 70px;
border-radius: var(--radius-lg); padding: 14px 16px; min-width: 70px;
}
.countdown-num {
font-family: var(--font-display);
@@ -68,7 +68,7 @@
.target-card {
flex: 1; background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: 14px; padding: 16px; text-align: center;
border-radius: var(--radius-lg); padding: 16px; text-align: center;
}
.target-icon { font-size: 24px; margin-bottom: 6px; }
.target-num { font-size: 26px; font-weight: 800; font-family: var(--font-display); letter-spacing: -0.5px; }
@@ -85,7 +85,7 @@
/* Checklist */
.checklist-card {
background: var(--background-secondary);
border-radius: 14px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.check-row {
@@ -97,7 +97,7 @@
.check-row:last-child { border-bottom: none; }
.check-row:active { opacity: 0.7; }
.check-box {
width: 24px; height: 24px; border-radius: 12px;
width: 24px; height: 24px; border-radius: var(--radius-md);
border: 2px solid var(--separator); flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 13px; transition: all 0.15s;
@@ -108,7 +108,7 @@
.check-title.done { text-decoration: line-through; color: var(--label-secondary); }
.check-sub { font-size: 12px; color: var(--label-secondary); margin-top: 2px; }
.check-tag {
font-size: 11px; font-weight: 600; padding: 3px 8px; border-radius: 6px;
font-size: 11px; font-weight: 600; padding: 3px 8px; border-radius: var(--radius-sm);
}
.tag-critical { background: rgba(255,69,58,0.15); color: var(--ios-red); }
.tag-content { background: rgba(10,132,255,0.12); color: var(--ios-blue); }
@@ -121,7 +121,7 @@
.channel-card {
background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: 14px; padding: 16px;
border-radius: var(--radius-lg); padding: 16px;
display: flex; gap: 14px; align-items: flex-start;
}
.channel-icon { font-size: 28px; flex-shrink: 0; }
@@ -130,7 +130,7 @@
.channel-desc { font-size: 13px; color: var(--label-secondary); line-height: 1.5; margin-bottom: 8px; }
.channel-action {
display: inline-flex; align-items: center; gap: 5px;
padding: 7px 12px; border-radius: 8px;
padding: 7px 12px; border-radius: var(--radius-md);
font-size: 13px; font-weight: 600;
background: var(--fill-secondary); color: var(--label-primary);
border: none; cursor: pointer; transition: opacity 0.12s;
@@ -141,7 +141,7 @@
/* Content Calendar */
.calendar-card {
background: var(--background-secondary);
border-radius: 14px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.calendar-week {
@@ -153,7 +153,7 @@
padding: 14px 16px 10px;
}
.week-badge {
font-size: 12px; font-weight: 700; padding: 3px 10px; border-radius: 8px;
font-size: 12px; font-weight: 700; padding: 3px 10px; border-radius: var(--radius-md);
}
.week-1 { background: rgba(10,132,255,0.15); color: var(--ios-blue); }
.week-2 { background: rgba(175,82,222,0.15); color: var(--ios-purple); }
@@ -164,7 +164,7 @@
.calendar-events { padding: 0 16px 14px; display: flex; flex-direction: column; gap: 8px; }
.cal-event {
display: flex; align-items: center; gap: 10px;
background: var(--fill-quaternary); border-radius: 10px; padding: 10px 12px;
background: var(--fill-quaternary); border-radius: var(--radius-md); padding: 10px 12px;
}
.cal-date { font-size: 13px; font-weight: 600; color: var(--label-secondary); min-width: 40px; }
.cal-text { font-size: 14px; flex: 1; }
@@ -175,7 +175,7 @@
margin: 0 16px 24px;
background: linear-gradient(135deg, rgba(255,69,58,0.12), rgba(255,159,10,0.08));
border: 0.5px solid rgba(255,69,58,0.25);
border-radius: 16px; padding: 20px;
border-radius: var(--radius-lg); padding: 20px;
text-align: center;
}
.launch-day-icon { font-size: 48px; margin-bottom: 10px; }
@@ -190,14 +190,14 @@
.press-card {
background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: 14px; padding: 16px;
border-radius: var(--radius-lg); padding: 16px;
margin: 0 16px 24px;
}
.press-title { font-size: 17px; font-weight: 600; margin-bottom: 8px; }
.press-body { font-size: 14px; color: var(--label-secondary); line-height: 1.6; margin-bottom: 12px; }
.press-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 10px 16px; border-radius: 10px;
padding: 10px 16px; border-radius: var(--radius-md);
background: var(--ios-blue); color: #fff;
font-size: 14px; font-weight: 600;
border: none; cursor: pointer;
@@ -254,7 +254,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Countdown -->
<div class="countdown-hero">
<div class="countdown-title">🚀 Countdown till lansering</div>
<div class="countdown-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> Countdown till lansering</div>
<div class="countdown-grid">
<div class="countdown-unit">
<div class="countdown-num" id="cd-days"></div>
@@ -279,17 +279,17 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Targets -->
<div class="target-row">
<div class="target-card">
<div class="target-icon">🎯</div>
<div class="target-icon"></div>
<div class="target-num" style="color:var(--ios-blue)">100</div>
<div class="target-label">Zoomers dag 1</div>
</div>
<div class="target-card">
<div class="target-icon">📦</div>
<div class="target-icon"></div>
<div class="target-num" style="color:var(--ios-green)">1 000</div>
<div class="target-label">Uppdrag månad 1</div>
</div>
<div class="target-card">
<div class="target-icon">💰</div>
<div class="target-icon"></div>
<div class="target-num" style="color:var(--ios-orange)">500k</div>
<div class="target-label">GMV månad 1 kr</div>
</div>
@@ -307,38 +307,38 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="channels-grid">
<div class="channel-card">
<div class="channel-icon">💼</div>
<div class="channel-icon"></div>
<div class="channel-info">
<div class="channel-name">LinkedIn</div>
<div class="channel-desc">Företagspost från Wavult Group + personlig post från Erik. Target: fastighetsägare, byggföretag, infrastrukturbolag.</div>
<a class="channel-action" href="https://linkedin.com/company/wavult" target="_blank">📝 Öppna LinkedIn</a>
<a class="channel-action" href="https://linkedin.com/company/wavult" target="_blank"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 13V10L10 3L13 6L6 13H3Z" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 4L12 7" stroke="#666" stroke-width="1.5"/></svg> Öppna LinkedIn</a>
</div>
</div>
<div class="channel-card">
<div class="channel-icon">𝕏</div>
<div class="channel-icon"></div>
<div class="channel-info">
<div class="channel-name">Twitter / X Thread</div>
<div class="channel-desc">Lanserings-thread: "Vi bygger Uber för drönare i Sverige 🇸🇪" — 8 tweets med bilder, stats och CTA till App Store.</div>
<button class="channel-action" onclick="copyTweet()">📋 Kopiera launch tweet</button>
<div class="channel-desc">Lanserings-thread: "Vi bygger Uber för drönare i Sverige <span class="flag-se">SE</span>" — 8 tweets med bilder, stats och CTA till App Store.</div>
<button class="channel-action" onclick="copyTweet()"><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> Kopiera launch tweet</button>
</div>
</div>
<div class="channel-card">
<div class="channel-icon">🚀</div>
<div class="channel-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg></div>
<div class="channel-info">
<div class="channel-name">Product Hunt</div>
<div class="channel-desc">Submit dag 1 (1 augusti). Mål: Top 5 Product of the Day. Mobilisera team för upvotes på morgonen.</div>
<a class="channel-action" href="https://producthunt.com/posts/new" target="_blank">🔗 Submit to PH</a>
<a class="channel-action" href="https://producthunt.com/posts/new" target="_blank"> Submit to PH</a>
</div>
</div>
<div class="channel-card">
<div class="channel-icon">📰</div>
<div class="channel-icon"></div>
<div class="channel-info">
<div class="channel-name">Press Release</div>
<div class="channel-desc">Skicka till Breakit, Ny Teknik, DI Digital. Embargo lift: 1 augusti 08:00. Distribution: TT Nyhetsbyrån + direkt kontakt.</div>
<button class="channel-action" onclick="showPR()">📄 Visa pressrelease</button>
<button class="channel-action" onclick="showPR()"> Visa pressrelease</button>
</div>
</div>
@@ -347,7 +347,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- LAUNCH DAY Card -->
<div class="launch-day-card">
<div class="launch-day-icon">🎯</div>
<div class="launch-day-icon"></div>
<div class="launch-day-title">LAUNCH DAY — 1 augusti</div>
<div class="launch-day-sub">08:00 — App Store live<br>08:30 — LinkedIn + Twitter posts<br>09:00 — Product Hunt live<br>10:00 — Press release distribution<br>12:00 — Status check: zoomers onboardade<br>18:00 — End of day report</div>
</div>
@@ -368,23 +368,23 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="calendar-events">
<div class="cal-event">
<span class="cal-date">Jun 1</span>
<span class="cal-text">LinkedIn: "Något är på väg i Sverige... 🇸🇪🚁"</span>
<span class="cal-icon">💼</span>
<span class="cal-text">LinkedIn: "Något är på väg i Sverige... <span class="flag-se">SE</span><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>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">Jun 3</span>
<span class="cal-text">Twitter: Bakom kulisserna — drönare i aktion</span>
<span class="cal-icon">𝕏</span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">Jun 5</span>
<span class="cal-text">Waitlist open: "Be first Zoomer in Sweden"</span>
<span class="cal-icon">🎯</span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">Jun 7</span>
<span class="cal-text">Instagram Reel: Drönarvideo från pilot-uppdrag</span>
<span class="cal-icon">📱</span>
<span class="cal-icon"></span>
</div>
</div>
</div>
@@ -400,18 +400,18 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="calendar-events">
<div class="cal-event">
<span class="cal-date">Jun 8</span>
<span class="cal-text">✈️ Inlämning App Store (deadline)</span>
<span class="cal-icon">📱</span>
<span class="cal-text"> Inlämning App Store (deadline)</span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">Jun 9</span>
<span class="cal-text">Beta zoomer-berättelse #1: "Mitt första uppdrag"</span>
<span class="cal-icon">👤</span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">Jun 11</span>
<span class="cal-text">LinkedIn: "Varför vi bygger quiXzoom" — långpost</span>
<span class="cal-icon">💼</span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">Jun 13</span>
@@ -421,7 +421,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="cal-event">
<span class="cal-date">Jun 14</span>
<span class="cal-text">Press kit skickat · Media briefing</span>
<span class="cal-icon">📰</span>
<span class="cal-icon"></span>
</div>
</div>
</div>
@@ -429,7 +429,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="calendar-week">
<div class="calendar-week-header">
<div>
<span class="week-badge week-launch">🚀 LAUNCH DAY</span>
<span class="week-badge week-launch"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> LAUNCH DAY</span>
<div class="week-title" style="margin-top:4px">1 augusti 2026</div>
<div class="week-sub">All hands on deck</div>
</div>
@@ -437,23 +437,23 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="calendar-events">
<div class="cal-event">
<span class="cal-date">08:00</span>
<span class="cal-text">App Store LIVE 🎉</span>
<span class="cal-icon">🍎</span>
<span class="cal-text">App Store LIVE </span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">08:30</span>
<span class="cal-text">LinkedIn + Twitter launch posts</span>
<span class="cal-icon">📣</span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">09:00</span>
<span class="cal-text">Product Hunt submission</span>
<span class="cal-icon">🚀</span>
<span class="cal-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg></span>
</div>
<div class="cal-event">
<span class="cal-date">10:00</span>
<span class="cal-text">Press release distribution (TT, Breakit)</span>
<span class="cal-icon">📰</span>
<span class="cal-icon"></span>
</div>
</div>
</div>
@@ -465,10 +465,10 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Press Release Modal -->
<div id="pr-modal" style="display:none;position:fixed;inset:0;z-index:9999;background: rgba(242,242,247,0.8);backdrop-filter:blur(8px);overflow-y:auto;padding:20px">
<div style="background:var(--background-secondary);border-radius:20px;max-width:600px;margin:0 auto;padding:24px;border:0.5px solid var(--separator)">
<div style="background:var(--background-secondary);border-radius: var(--radius-xl);max-width:600px;margin:0 auto;padding:24px;border:0.5px solid var(--separator)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<div style="font-size:18px;font-weight:700">📰 Pressrelease</div>
<button onclick="hidePR()" style="background:var(--fill-secondary);border:none;border-radius:50%;width:32px;height:32px;font-size:18px;cursor:pointer;color:var(--label-primary)">×</button>
<div style="font-size:18px;font-weight:700"> Pressrelease</div>
<button onclick="hidePR()" style="background:var(--fill-secondary);border:none;border-radius: var(--radius-full);width:32px;height:32px;font-size:18px;cursor:pointer;color:var(--label-primary)">×</button>
</div>
<div style="font-size:11px;color:var(--label-secondary);margin-bottom:16px;text-transform:uppercase;letter-spacing:0.5px">EMBARGO: 1 augusti 2026 · 08:00 CET</div>
<div style="font-size:18px;font-weight:700;margin-bottom:12px;line-height:1.4">quiXzoom lanserar i Sverige — den första drönargig-plattformen för den svenska marknaden</div>
@@ -478,7 +478,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
Plattformen finns tillgänglig från idag i hela Sverige via App Store. Zoomer-registrering är öppen för certifierade drönarpilöter.
</div>
<div style="font-size:12px;color:var(--label-tertiary);margin-bottom:16px">Kontakt: erik@hypbit.com · +46709123223</div>
<button onclick="copyPR()" style="background:var(--ios-blue);color:#fff;border:none;border-radius:10px;padding:12px 20px;font-size:14px;font-weight:600;cursor:pointer;width:100%">📋 Kopiera pressrelease</button>
<button onclick="copyPR()" style="background:var(--ios-blue);color:#fff;border:none;border-radius: var(--radius-md);padding:12px 20px;font-size:14px;font-weight:600;cursor:pointer;width:100%"><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> Kopiera pressrelease</button>
</div>
</div>
@@ -536,7 +536,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
checks.forEach((c, i) => { if (savedChecks[i]) c.done = true; });
function tagLabel(t) {
return { critical:'🔴 Kritisk', content:'🔵 Content', tech:'🟣 Tech', marketing:'🟠 Marketing', done:' Klar' }[t] || t;
return { critical:' Kritisk', content:' Content', tech:'🟣 Tech', marketing:'🟠 Marketing', done:'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Klar' }[t] || t;
}
function tagClass(t) {
return { critical:'tag-critical', content:'tag-content', tech:'tag-tech', marketing:'tag-marketing', done:'tag-done' }[t] || '';
@@ -549,7 +549,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
el.innerHTML = checks.map((c,i) => `
<div class="check-row" onclick="toggleCheck(${i})">
<div class="check-box ${c.done?'done':''}">${c.done?'':''}</div>
<div class="check-box ${c.done?'done':''}">${c.done?'<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 class="check-info">
<div class="check-title ${c.done?'done':''}">${c.text}</div>
<div class="check-sub">${c.sub}</div>
@@ -571,7 +571,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
// Tweet
function copyTweet() {
const tweet = `Vi lanserar quiXzoom idag 🚁🇸🇪\n\nSveriges första drönargig-plattform.\n\nZoomers = certifierade drönarpilöter som tar uppdrag när det passar dem.\n\nFasad-scanning, takinspektion, infrastruktur — på 24h.\n\nLadda ner nu 👇\nhttps://quixzoom.com/app`;
const tweet = `Vi lanserar quiXzoom idag <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 class="flag-se">SE</span>\n\nSveriges första drönargig-plattform.\n\nZoomers = certifierade drönarpilöter som tar uppdrag när det passar dem.\n\nFasad-scanning, takinspektion, infrastruktur — på 24h.\n\nLadda ner nu \nhttps://quixzoom.com/app`;
navigator.clipboard?.writeText(tweet).then(() => alert('Tweet kopierad!')).catch(() => alert(tweet));
}
+46 -46
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Launch Readiness — quiXzoom 1 augusti 2026</title>
<style>
/* ── iOS 26 / Apple Native — Light Mode ──────────────────────────────── */
/* iOS 26 / Apple Native — Light Mode */
:root {
--bg: #f2f2f7;
--bg-card: #ffffff;
@@ -41,7 +41,7 @@
padding-bottom: calc(var(--safe-bottom) + 60px);
}
/* ── Nav Bar ─────────────────────────────────────────────────────────── */
/* Nav Bar */
.nav-bar {
position: sticky;
top: 0;
@@ -62,18 +62,18 @@
font-size: 13px;
font-weight: 600;
padding: 4px 12px;
border-radius: 20px;
border-radius: var(--radius-xl);
transition: background 0.3s;
}
.nav-badge.ready { background: var(--green); }
.nav-badge.warn { background: var(--orange); }
/* ── Page Header ─────────────────────────────────────────────────────── */
/* Page Header */
.page-header { padding: 24px 20px 8px; }
.page-title { font-size: 34px; font-weight: 700; letter-spacing: -0.5px; line-height: 1.1; }
.page-sub { font-size: 15px; color: var(--label-secondary); margin-top: 4px; }
/* ── Countdown Hero ──────────────────────────────────────────────────── */
/* Countdown Hero */
.countdown-hero {
margin: 16px 16px 0;
background: linear-gradient(135deg, #001E50 0%, #0040A8 100%);
@@ -98,7 +98,7 @@
.cd-block {
flex: 1;
background: rgba(255,255,255,0.12);
border-radius: 10px;
border-radius: var(--radius-md);
padding: 12px 6px 10px;
text-align: center;
}
@@ -131,7 +131,7 @@
}
.countdown-launch strong { color: #fff; font-weight: 700; }
/* ── Progress Hero ───────────────────────────────────────────────────── */
/* Progress Hero */
.progress-hero {
margin: 12px 16px 0;
background: var(--bg-card);
@@ -160,18 +160,18 @@
.days-left.green { color: var(--green); }
.progress-bar-bg {
background: var(--fill);
border-radius: 4px;
border-radius: var(--radius-sm);
height: 10px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: 4px;
border-radius: var(--radius-sm);
background: linear-gradient(90deg, var(--tint), #5AC8FA);
transition: width 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* ── Stat Row ────────────────────────────────────────────────────────── */
/* Stat Row */
.stat-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
@@ -204,7 +204,7 @@
margin-top: 2px;
}
/* ── Final Verification Banner ───────────────────────────────────────── */
/* Final Verification Banner */
.verify-banner {
margin: 22px 16px 0;
background: rgba(52,199,89,0.10);
@@ -233,7 +233,7 @@
.verify-row:last-child { border-bottom: none; padding-bottom: 0; }
.verify-dot {
width: 20px; height: 20px;
border-radius: 50%;
border-radius: var(--radius-full);
display: flex; align-items: center; justify-content: center;
font-size: 11px;
font-weight: 700;
@@ -249,10 +249,10 @@
color: var(--label-secondary);
background: var(--fill);
padding: 2px 6px;
border-radius: 4px;
border-radius: var(--radius-sm);
}
/* ── Category Section ────────────────────────────────────────────────── */
/* Category Section */
.cat-section { margin-top: 28px; }
.cat-header {
display: flex;
@@ -262,7 +262,7 @@
}
.cat-icon {
width: 28px; height: 28px;
border-radius: 7px;
border-radius: var(--radius-sm);
display: flex; align-items: center; justify-content: center;
font-size: 14px;
flex-shrink: 0;
@@ -282,18 +282,18 @@
}
.cat-progress-bar {
height: 3px;
border-radius: 2px;
border-radius: var(--radius-sm);
background: var(--fill);
margin: 0 20px 8px;
overflow: hidden;
}
.cat-progress-fill {
height: 100%;
border-radius: 2px;
border-radius: var(--radius-sm);
transition: width 0.6s ease;
}
/* ── Checklist ───────────────────────────────────────────────────────── */
/* Checklist */
.checklist {
background: var(--bg-card);
border-radius: var(--radius-lg);
@@ -316,7 +316,7 @@
.check-circle {
width: 24px; height: 24px;
border-radius: 50%;
border-radius: var(--radius-full);
flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 13px;
@@ -354,7 +354,7 @@
font-size: 12px;
font-weight: 500;
padding: 2px 8px;
border-radius: 6px;
border-radius: var(--radius-sm);
background: var(--fill);
color: var(--label-secondary);
}
@@ -377,7 +377,7 @@
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 6px;
border-radius: var(--radius-sm);
background: var(--fill);
color: var(--label-secondary);
}
@@ -385,14 +385,14 @@
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 6px;
border-radius: var(--radius-sm);
}
.tag-done { background: rgba(52,199,89,0.15); color: var(--green); }
.tag-inprog { background: rgba(0,122,255,0.12); color: var(--tint); }
.tag-blocked { background: rgba(255,59,48,0.12); color: var(--red); }
.tag-pending { background: var(--fill); color: var(--label-tertiary); }
/* ── Footer ──────────────────────────────────────────────────────────── */
/* Footer */
.footer {
text-align: center;
padding: 32px 20px 12px;
@@ -434,7 +434,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Nav Bar -->
<div class="nav-bar">
<div class="nav-title">🚀 Launch Readiness</div>
<div class="nav-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> Launch Readiness</div>
<div class="nav-badge" id="navBadge">Laddar…</div>
</div>
@@ -514,38 +514,38 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Final Verification Section -->
<div style="margin:22px 16px 0">
<div style="font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:0.6px;color:var(--label-secondary);padding:0 4px 8px;display:flex;align-items:center;gap:6px;">
<span style="font-size:16px"></span> Final Verification — W11-QUIXZOOM-LAUNCH
<span style="font-size:16px"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span> Final Verification — W11-QUIXZOOM-LAUNCH
</div>
</div>
<div class="verify-banner">
<div class="verify-banner-title"><span>🔍</span> 5 kritiska kontrollpunkter</div>
<div class="verify-banner-title"><span><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></span> 5 kritiska kontrollpunkter</div>
<div class="verify-row">
<div class="verify-dot vdot-ok"></div>
<div class="verify-dot vdot-ok"><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 class="verify-text">Alla quiXzoom API-endpoints svarar</div>
<div class="verify-endpoint">14 moduler · /api/quixzoom/*</div>
</div>
<div class="verify-row">
<div class="verify-dot vdot-ok"></div>
<div class="verify-dot vdot-ok"><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 class="verify-text">Zoomer-registrering end-to-end</div>
<div class="verify-endpoint">POST /zoomers/register → KYC → godkänd</div>
</div>
<div class="verify-row">
<div class="verify-dot vdot-ok"></div>
<div class="verify-dot vdot-ok"><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 class="verify-text">Mission create / accept / complete-flöde</div>
<div class="verify-endpoint">draft → open → active → completed</div>
</div>
<div class="verify-row">
<div class="verify-dot vdot-ok"></div>
<div class="verify-dot vdot-ok"><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 class="verify-text">Payout-automation aktiv (Stripe Connect)</div>
<div class="verify-endpoint">POST /payouts/process · 85% Zoomer / 15% plattform</div>
</div>
<div class="verify-row">
<div class="verify-dot vdot-ok"></div>
<div class="verify-dot vdot-ok"><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 class="verify-text">DAC7 XML-export fungerar</div>
<div class="verify-endpoint">GET /compliance/dac7/report?format=xml · Skatteverket-schema</div>
</div>
@@ -562,7 +562,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<script>
const LAUNCH = new Date('2026-06-15T09:00:00+02:00');
// ── Countdown ─────────────────────────────────────────────────────────────
// Countdown
function pad(n) { return String(n).padStart(2, '0'); }
function tickCountdown() {
@@ -591,7 +591,7 @@ const CATEGORIES = [
{
id: 'verification',
name: 'API Verification',
icon: '⚙️',
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>',
color: '#007AFF',
items: [
{ title: 'missions.mjs — create/accept/complete lifecycle', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /missions + PATCH /:id/status · statusmaskin verifierad' },
@@ -609,7 +609,7 @@ const CATEGORIES = [
{
id: 'zoomer',
name: 'Zoomer-flöde',
icon: '🧑‍💼',
icon: '',
color: '#34C759',
items: [
{ title: 'Zoomer-registrering komplett (6-stegs KYC)', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'Personuppgifter → ID → Adress → Bankkonto → Skatteform → Villkor' },
@@ -623,7 +623,7 @@ const CATEGORIES = [
{
id: 'mission',
name: 'Mission-flöde',
icon: '🎯',
icon: '',
color: '#FF9500',
items: [
{ title: 'Customer bokar uppdrag (request-mission-flöde)', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /customers/:id/book → mission draft' },
@@ -638,7 +638,7 @@ const CATEGORIES = [
{
id: 'payout',
name: 'Utbetalningar & DAC7',
icon: '💳',
icon: '',
color: '#AF52DE',
items: [
{ title: 'Stripe Production Keys konfigurerade', owner: 'Ops', status: 'done', deadline: 'Juni 5' },
@@ -654,7 +654,7 @@ const CATEGORIES = [
{
id: 'ios',
name: 'iOS Build',
icon: '📱',
icon: '',
color: '#5AC8FA',
items: [
{ title: 'Mac SSH-access för iOS builds löst', owner: 'Johan', status: 'done', deadline: 'Juni 1', note: 'Eskalerat → löst efter Sven-intervention' },
@@ -668,7 +668,7 @@ const CATEGORIES = [
{
id: 'appstore',
name: 'App Store',
icon: '🏪',
icon: '',
color: '#007AFF',
items: [
{ title: 'App Store Connect-konto verifierat', owner: 'Ops', status: 'done', deadline: 'Maj 10' },
@@ -682,7 +682,7 @@ const CATEGORIES = [
{
id: 'legal',
name: 'Legal / Compliance',
icon: '⚖️',
icon: '',
color: '#FF3B30',
items: [
{ title: 'Privacy Policy + Terms of Service publicerade', owner: 'Legal', status: 'done', deadline: 'Maj 15' },
@@ -695,7 +695,7 @@ const CATEGORIES = [
{
id: 'operations',
name: 'Operations',
icon: '🔧',
icon: '',
color: '#FF9500',
items: [
{ title: 'Monitoring & alerting konfigurerat', owner: 'Ops', status: 'done', deadline: 'Juni 5' },
@@ -710,7 +710,7 @@ const CATEGORIES = [
{
id: 'marketing',
name: 'Marketing',
icon: '📣',
icon: '',
color: '#34C759',
items: [
{ title: 'Landing page quixzoom.se live', owner: 'Mktg', status: 'done', deadline: 'Maj 20' },
@@ -723,7 +723,7 @@ const CATEGORIES = [
},
];
// ── Helpers ───────────────────────────────────────────────────────────────
// Helpers
function computeDaysLeft() {
const diff = LAUNCH - new Date();
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
@@ -736,9 +736,9 @@ function statusCircleClass(s) {
return 'pending';
}
function statusCircleContent(s) {
if (s === 'done') return '';
if (s === 'done') return '<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>';
if (s === 'in-prog') return '→';
if (s === 'blocked') return '';
if (s === 'blocked') return '';
return '';
}
function statusTagClass(s) {
@@ -754,7 +754,7 @@ function statusTagLabel(s) {
return 'Pending';
}
// ── Render ────────────────────────────────────────────────────────────────
// Render
function render() {
let totalDone = 0, totalInprog = 0, totalBlocked = 0, totalPending = 0, totalItems = 0;
@@ -777,7 +777,7 @@ function render() {
document.getElementById('totalItems').textContent = totalItems;
const daysEl = document.getElementById('daysLeft');
daysEl.textContent = days > 0 ? `${days} dagar kvar` : 'LAUNCH DAY 🚀';
daysEl.textContent = days > 0 ? `${days} dagar kvar` : 'LAUNCH DAY <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg>';
if (days <= 3) daysEl.style.color = 'var(--red)';
document.getElementById('cntDone').textContent = totalDone;
+52 -52
View File
@@ -7,7 +7,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>quiXzoom — Launch Status</title>
<style>
/* ── iOS System Font Stack ───────────────────────────────────────────── */
/* iOS System Font Stack */
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
:root {
--ios-blue: #007AFF;
@@ -33,7 +33,7 @@
padding-bottom: env(safe-area-inset-bottom, 20px);
}
/* ── Status Bar ──────────────────────────────────────────────────────── */
/* Status Bar */
.status-bar {
display: flex;
align-items: center;
@@ -46,7 +46,7 @@
.status-bar .time { font-variant-numeric: tabular-nums; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; }
/* ── Navigation Bar ──────────────────────────────────────────────────── */
/* Navigation Bar */
.nav-bar {
padding: 6px 16px 12px;
display: flex;
@@ -66,18 +66,18 @@
font-size: 12px;
font-weight: 600;
padding: 5px 10px;
border-radius: 12px;
border-radius: var(--radius-md);
letter-spacing: 0.3px;
}
.launch-badge.warning { background: var(--ios-orange); }
/* ── Scroll Container ────────────────────────────────────────────────── */
/* Scroll Container */
.scroll { overflow-y: auto; padding: 0 16px 32px; }
/* ── Hero Card ───────────────────────────────────────────────────────── */
/* Hero Card */
.hero-card {
background: linear-gradient(135deg, #FF6D00 0%, #FF3D00 100%);
border-radius: 22px;
border-radius: var(--radius-xl);
padding: 24px;
color: white;
margin-bottom: 20px;
@@ -89,7 +89,7 @@
position: absolute;
right: -30px; top: -30px;
width: 140px; height: 140px;
border-radius: 50%;
border-radius: var(--radius-full);
background: rgba(255,255,255,0.12);
}
.hero-card::after {
@@ -97,7 +97,7 @@
position: absolute;
right: 30px; top: 40px;
width: 80px; height: 80px;
border-radius: 50%;
border-radius: var(--radius-full);
background: rgba(255,255,255,0.08);
}
.hero-label {
@@ -125,7 +125,7 @@
}
.countdown-unit {
background: rgba(255,255,255,0.22);
border-radius: 14px;
border-radius: var(--radius-lg);
padding: 12px 16px;
text-align: center;
backdrop-filter: blur(4px);
@@ -143,7 +143,7 @@
letter-spacing: 0.5px;
}
/* ── Section Header ──────────────────────────────────────────────────── */
/* Section Header */
.section-header {
font-size: 20px;
font-weight: 700;
@@ -151,7 +151,7 @@
margin: 24px 0 10px;
}
/* ── Stats Row ───────────────────────────────────────────────────────── */
/* Stats Row */
.stats-row {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -160,7 +160,7 @@
}
.stat-card {
background: var(--ios-card);
border-radius: 18px;
border-radius: var(--radius-lg);
padding: 18px 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
@@ -183,10 +183,10 @@
font-weight: 500;
}
/* ── Progress Bar ────────────────────────────────────────────────────── */
/* Progress Bar */
.readiness-card {
background: var(--ios-card);
border-radius: 18px;
border-radius: var(--radius-lg);
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
@@ -205,14 +205,14 @@
.readiness-label { font-size: 15px; font-weight: 500; color: var(--ios-gray); }
.progress-track {
background: var(--ios-gray6);
border-radius: 8px;
border-radius: var(--radius-md);
height: 10px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
border-radius: 8px;
border-radius: var(--radius-md);
background: linear-gradient(90deg, var(--ios-green) 0%, #30D158 100%);
transition: width 1.2s cubic-bezier(0.16,1,0.3,1);
}
@@ -221,7 +221,7 @@
color: var(--ios-gray);
}
/* ── Cities ──────────────────────────────────────────────────────────── */
/* Cities */
.cities-row {
display: flex;
flex-wrap: wrap;
@@ -230,7 +230,7 @@
}
.city-chip {
background: var(--ios-card);
border-radius: 20px;
border-radius: var(--radius-xl);
padding: 8px 14px;
font-size: 14px;
font-weight: 500;
@@ -241,14 +241,14 @@
}
.city-chip .dot {
width: 8px; height: 8px;
border-radius: 50%;
border-radius: var(--radius-full);
background: var(--ios-green);
}
/* ── Checklist ───────────────────────────────────────────────────────── */
/* Checklist */
.checklist-group {
background: var(--ios-card);
border-radius: 18px;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
margin-bottom: 20px;
@@ -263,7 +263,7 @@
.check-item:last-child { border-bottom: none; }
.check-icon {
width: 32px; height: 32px;
border-radius: 50%;
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
@@ -289,14 +289,14 @@
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 8px;
border-radius: var(--radius-md);
letter-spacing: 0.2px;
flex-shrink: 0;
}
.check-badge.done { background: rgba(52,199,89,0.15); color: #27a745; }
.check-badge.pending { background: rgba(255,149,0,0.15); color: #c87000; }
/* ── Footer ──────────────────────────────────────────────────────────── */
/* Footer */
.footer {
text-align: center;
font-size: 13px;
@@ -305,16 +305,16 @@
}
.footer strong { color: var(--ios-label); }
/* ── Skeleton / loading ──────────────────────────────────────────────── */
/* Skeleton / loading */
.skeleton {
background: linear-gradient(90deg, #e8e8e8 25%, #f5f5f5 50%, #e8e8e8 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8px;
border-radius: var(--radius-md);
}
@keyframes shimmer { to { background-position: -200% 0; } }
/* ── Dark Mode ───────────────────────────────────────────────────────── */
/* Dark Mode */
@media (prefers-color-scheme: dark) {
:root {
--ios-bg: #000000;
@@ -326,11 +326,11 @@
.skeleton { background: linear-gradient(90deg, #2a2a2a 25%, #383838 50%, #2a2a2a 75%); background-size: 200% 100%; }
}
/* ── Pulse dot ───────────────────────────────────────────────────────── */
/* Pulse dot */
.live-dot {
display: inline-block;
width: 8px; height: 8px;
border-radius: 50%;
border-radius: var(--radius-full);
background: var(--ios-green);
box-shadow: 0 0 0 0 rgba(52,199,89,0.5);
animation: pulse 2s infinite;
@@ -343,7 +343,7 @@
100%{ box-shadow: 0 0 0 0 rgba(52,199,89,0); }
}
/* ── Refresh button ──────────────────────────────────────────────────── */
/* Refresh button */
.refresh-btn {
display: flex;
align-items: center;
@@ -353,7 +353,7 @@
background: var(--ios-blue);
color: white;
border: none;
border-radius: 14px;
border-radius: var(--radius-lg);
padding: 16px;
font-size: 16px;
font-weight: 600;
@@ -438,22 +438,22 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Live-statistik</div>
<div class="stats-row">
<div class="stat-card">
<span class="icon">📍</span>
<span class="icon"></span>
<span class="val" id="stat-missions"></span>
<span class="lbl">Uppdrag aktiva</span>
</div>
<div class="stat-card">
<span class="icon"></span>
<span class="icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg></span>
<span class="val" id="stat-zoomers"></span>
<span class="lbl">Zoomers aktiva</span>
</div>
<div class="stat-card">
<span class="icon">🏙️</span>
<span class="icon"></span>
<span class="val" id="stat-cities"></span>
<span class="lbl">Städer täckta</span>
</div>
<div class="stat-card">
<span class="icon">📅</span>
<span class="icon"></span>
<span class="val" id="stat-days"></span>
<span class="lbl">Dagar kvar</span>
</div>
@@ -485,7 +485,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<!-- Checklist: Done -->
<div class="section-header">Klart </div>
<div class="section-header">Klart <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<div class="checklist-group" id="done-list">
<div class="check-item">
<div class="check-icon done skeleton" style="background:transparent"></div>
@@ -520,7 +520,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div><!-- /scroll -->
<script>
// ── Clock ──────────────────────────────────────────────────────────────
// Clock
function updateClock() {
const now = new Date();
document.getElementById('clock').textContent =
@@ -530,7 +530,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
updateClock();
setInterval(updateClock, 30000);
// ── Countdown ─────────────────────────────────────────────────────────
// Countdown
function updateCountdown() {
const launch = new Date('2026-06-15T07:00:00+02:00');
const now = new Date();
@@ -545,8 +545,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
updateCountdown();
setInterval(updateCountdown, 60000);
// ── Build checklist item ───────────────────────────────────────────────
const ICONS = { done: '', pending: '⏳' };
// Build checklist item
const ICONS = { done: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>', pending: '⏳' };
function buildItem(c) {
const div = document.createElement('div');
div.className = 'check-item';
@@ -561,7 +561,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
return div;
}
// ── Load data ──────────────────────────────────────────────────────────
// Load data
async function loadData() {
try {
// Use a dummy JWT for auth (dev mode)
@@ -580,7 +580,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
const data = await readRes.json();
const stats = statsRes.ok ? await statsRes.json() : null;
// ── Stats cards
// Stats cards
const activeMissions = stats ? (stats.missions.open + stats.missions.active) : data.summary.done;
const activeZoomers = stats ? (stats.zoomers.available + stats.zoomers.busy) : 3;
document.getElementById('stat-missions').textContent = activeMissions;
@@ -588,7 +588,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
document.getElementById('stat-cities').textContent = data.summary.cities;
document.getElementById('stat-days').textContent = data.days_to_launch;
// ── Readiness
// Readiness
const pct = data.readiness_pct;
document.getElementById('readiness-pct').textContent = pct + '%';
document.getElementById('done-count').textContent = data.summary.done;
@@ -602,17 +602,17 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
document.getElementById('progress-fill').style.width = pct + '%';
}, 100);
// ── Go badge
// Go badge
const badge = document.getElementById('go-badge');
if (data.launch_go) {
badge.textContent = '🚀 GO';
badge.textContent = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> GO';
badge.className = 'launch-badge';
} else {
badge.textContent = '⏳ ' + data.days_to_launch + 'd kvar';
badge.className = 'launch-badge warning';
}
// ── Cities
// Cities
const citiesRow = document.getElementById('cities-row');
citiesRow.innerHTML = '';
(data.summary.cities_list || []).forEach(city => {
@@ -622,7 +622,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
citiesRow.appendChild(chip);
});
// ── Checklist
// Checklist
const doneList = document.getElementById('done-list');
const pendingList = document.getElementById('pending-list');
doneList.innerHTML = '';
@@ -639,16 +639,16 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (doneList.children.length === 0)
doneList.innerHTML = '<div class="check-item" style="justify-content:center;color:var(--ios-gray);font-size:14px;padding:20px;">Inga klara ännu</div>';
if (pendingList.children.length === 0)
pendingList.innerHTML = '<div class="check-item" style="justify-content:center;color:var(--ios-green);font-size:14px;padding:20px;">Allt klart! 🎉</div>';
pendingList.innerHTML = '<div class="check-item" style="justify-content:center;color:var(--ios-green);font-size:14px;padding:20px;">Allt klart! </div>';
// ── Timestamp
// Timestamp
document.getElementById('last-updated').textContent =
'Uppdaterat ' + new Date().toLocaleTimeString('sv-SE', { hour:'2-digit', minute:'2-digit' });
} catch (err) {
console.error('Launch readiness load error:', err);
document.getElementById('readiness-sub').textContent = '⚠️ Kunde inte ladda data — kontrollera API';
document.getElementById('go-badge').textContent = '⚠️ Fel';
document.getElementById('readiness-sub').textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Kunde inte ladda data — kontrollera API';
document.getElementById('go-badge').textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Fel';
document.getElementById('go-badge').className = 'launch-badge warning';
}
}
+36 -36
View File
@@ -48,7 +48,7 @@
overflow-x: hidden;
}
/* ── Nav Bar ─────────────────────────────────────────────────────────── */
/* Nav Bar */
.nav-bar {
position: sticky;
top: 0;
@@ -81,7 +81,7 @@
font-size: 13px;
font-weight: 600;
padding: 4px 10px;
border-radius: 20px;
border-radius: var(--radius-xl);
background: var(--ios-bg-tertiary);
color: var(--ios-label-secondary);
}
@@ -89,7 +89,7 @@
.nav-pill.warning { background: rgba(255,159,10,0.2); color: var(--ios-orange); }
.nav-pill.ok { background: rgba(48,209,88,0.2); color: var(--ios-green); }
/* ── Hero Countdown ──────────────────────────────────────────────────── */
/* Hero Countdown */
.hero {
display: flex;
flex-direction: column;
@@ -160,7 +160,7 @@
}
.hero-date strong { color: var(--ios-label); font-weight: 600; }
/* ── Stat Row ────────────────────────────────────────────────────────── */
/* Stat Row */
.stat-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
@@ -187,7 +187,7 @@
margin-top: 2px;
}
/* ── Content ─────────────────────────────────────────────────────────── */
/* Content */
.content { padding: 0 0 20px; }
/* Section header */
@@ -237,7 +237,7 @@
.checkbox {
width: 24px;
height: 24px;
border-radius: 50%;
border-radius: var(--radius-full);
border: 2px solid var(--ios-bg-elevated);
display: flex;
align-items: center;
@@ -282,7 +282,7 @@
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 6px;
border-radius: var(--radius-sm);
background: rgba(255,69,58,0.15);
color: var(--ios-red);
flex-shrink: 0;
@@ -291,7 +291,7 @@
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 6px;
border-radius: var(--radius-sm);
background: rgba(255,159,10,0.15);
color: var(--ios-orange);
flex-shrink: 0;
@@ -300,7 +300,7 @@
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 6px;
border-radius: var(--radius-sm);
background: rgba(48,209,88,0.15);
color: var(--ios-green);
flex-shrink: 0;
@@ -318,7 +318,7 @@
.status-icon {
width: 36px;
height: 36px;
border-radius: 10px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
@@ -344,7 +344,7 @@
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border-radius: var(--radius-full);
flex-shrink: 0;
}
.status-dot.red { background: var(--ios-red); }
@@ -364,7 +364,7 @@
.action-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
@@ -408,12 +408,12 @@
.progress-bar-track {
height: 6px;
background: var(--ios-bg-elevated);
border-radius: 3px;
border-radius: var(--radius-sm);
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: 3px;
border-radius: var(--radius-sm);
background: var(--ios-green);
transition: width 0.8s var(--spring);
}
@@ -448,7 +448,7 @@
height: 14px;
border: 2px solid var(--ios-label-tertiary);
border-top-color: var(--ios-blue);
border-radius: 50%;
border-radius: var(--radius-full);
animation: spin 0.8s linear infinite;
vertical-align: middle;
}
@@ -501,19 +501,19 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</head>
<body>
<!-- ── Nav Bar ─────────────────────────────────────────────────────────────── -->
<!-- Nav Bar -->
<nav class="nav-bar">
<a class="nav-back" href="aamos-hub/index.html">
<svg width="10" height="17" viewBox="0 0 10 17" fill="none"><path d="M9 1L1 8.5L9 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
Hub
</a>
<span class="nav-title">quiXzoom Launch</span>
<span class="nav-pill critical" id="nav-pill">🚀 <span id="nav-days"></span>d</span>
<span class="nav-pill critical" id="nav-pill"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> <span id="nav-days"></span>d</span>
</nav>
<!-- ── Hero ───────────────────────────────────────────────────────────────── -->
<!-- Hero -->
<section class="hero">
<div class="hero-eyebrow">🇸🇪 Sverige-lansering</div>
<div class="hero-eyebrow"><span class="flag-se">SE</span> Sverige-lansering</div>
<h1 class="hero-title">quiXzoom Launch Tracker</h1>
<div class="countdown-ring">
@@ -533,7 +533,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<p class="hero-date">Lanseringsdatum: <strong>1 augusti 2026</strong></p>
</section>
<!-- ── Stats ──────────────────────────────────────────────────────────────── -->
<!-- Stats -->
<div class="stat-row">
<div class="stat-cell">
<div class="stat-num" id="stat-done" style="color:var(--ios-green)">0</div>
@@ -549,7 +549,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ── Content ────────────────────────────────────────────────────────────── -->
<!-- Content -->
<div class="content">
<!-- Section 1: Kritiska Milstolpar -->
@@ -665,12 +665,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Section 2: Revenue Readiness -->
<div class="section-header">
<h2 class="section-title">Revenue Readiness</h2>
<span class="section-badge crit" id="revenue-badge">⚠️ Ej redo</span>
<span class="section-badge crit" id="revenue-badge"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Ej redo</span>
</div>
<div class="card-list">
<div class="status-row">
<div class="status-icon red">💳</div>
<div class="status-icon red"></div>
<div class="status-text">
<div class="status-label">Stripe KYC</div>
<div class="status-desc">Blockar alla live-betalningar — aktivera omedelbart</div>
@@ -678,7 +678,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="status-dot red"></div>
</div>
<div class="status-row">
<div class="status-icon orange">🔄</div>
<div class="status-icon orange"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<div class="status-text">
<div class="status-label">Payment flow</div>
<div class="status-desc">Ej testbar utan Stripe KYC · Implementerat i kod</div>
@@ -686,7 +686,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="status-dot orange"></div>
</div>
<div class="status-row">
<div class="status-icon green">💰</div>
<div class="status-icon green"></div>
<div class="status-text">
<div class="status-label">Pricing konfigurerat</div>
<div class="status-desc">quiXzoom-priser satt i systemet</div>
@@ -694,7 +694,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="status-dot green"></div>
</div>
<div class="status-row">
<div class="status-icon red">🏦</div>
<div class="status-icon red"></div>
<div class="status-text">
<div class="status-label">Stripe Connect (zoomer-utbetalningar)</div>
<div class="status-desc">Väntar på KYC-godkännande</div>
@@ -712,7 +712,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="card-list">
<div class="status-row">
<div class="status-icon green">⚙️</div>
<div class="status-icon green"><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="status-text">
<div class="status-label">AAMOS backend</div>
<div class="status-desc">Live · amos.wavult.com operationell</div>
@@ -720,7 +720,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="status-dot green"></div>
</div>
<div class="status-row">
<div class="status-icon green">🌐</div>
<div class="status-icon green"></div>
<div class="status-text">
<div class="status-label">quixzoom.com</div>
<div class="status-desc">Landing page live</div>
@@ -728,7 +728,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="status-dot green"></div>
</div>
<div class="status-row">
<div class="status-icon orange">📱</div>
<div class="status-icon orange"></div>
<div class="status-text">
<div class="status-label">iOS / Android app</div>
<div class="status-desc">Under utveckling · App Store-submission ej genomförd</div>
@@ -736,7 +736,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="status-dot orange"></div>
</div>
<div class="status-row">
<div class="status-icon orange">🔑</div>
<div class="status-icon orange"></div>
<div class="status-text">
<div class="status-label">TED EU API</div>
<div class="status-desc">API-nyckel saknas · 5 min att fixa</div>
@@ -744,7 +744,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="status-dot orange"></div>
</div>
<div class="status-row">
<div class="status-icon red">🧪</div>
<div class="status-icon red"></div>
<div class="status-text">
<div class="status-label">Load-test + rollback-plan</div>
<div class="status-desc">Ej genomfört · Kritisk inför launch</div>
@@ -752,7 +752,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="status-dot red"></div>
</div>
<div class="status-row">
<div class="status-icon red">🔐</div>
<div class="status-icon red"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7" width="10" height="7" rx="1" stroke="#666" stroke-width="1.5"/><path d="M5 7V5C5 3.3 6.3 2 8 2C9.7 2 11 3.3 11 5V7" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="10.5" r="1" fill="#666"/></svg></div>
<div class="status-text">
<div class="status-label">Pentest / säkerhetsgranskning</div>
<div class="status-desc">Ej genomförd · Måste göras inför launch</div>
@@ -804,7 +804,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="info-card">
<div class="info-card-icon">💡</div>
<div class="info-card-icon"></div>
<div class="info-card-text">
<strong>Nästa kritiska deadline:</strong> PR/pressrelease måste vara klar senast <strong>25 maj</strong> (3 veckor innan launch). Zoomer-rekrytering bör starta omedelbart — mål: 20+ zoomers till soft launch <strong>1 augusti</strong>.
</div>
@@ -815,7 +815,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div><!-- /content -->
<script>
// ── Launch date ──────────────────────────────────────────────────────────
// Launch date
const LAUNCH_DATE = new Date('2026-06-15T00:00:00+02:00');
// We assume campaign started ~90 days before launch for the ring
const CAMPAIGN_START = new Date('2026-03-17T00:00:00+02:00');
@@ -851,7 +851,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
});
}
// ── Milestone state (persisted in localStorage) ───────────────────────────
// Milestone state (persisted in localStorage)
const STORAGE_KEY = 'qx_launch_milestones_v2';
function loadState() {
@@ -904,7 +904,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
applyState();
}
// ── Init ──────────────────────────────────────────────────────────────────
// Init
updateHero();
applyState();
setInterval(updateHero, 60000);
+42 -42
View File
@@ -35,7 +35,7 @@
overflow: hidden;
}
/* ── Header ─────────────────────────────────────────────── */
/* Header */
#header {
position: fixed;
top: 0; left: 0; right: 0;
@@ -72,7 +72,7 @@
#refresh-btn {
width: 32px; height: 32px;
border-radius: 50%;
border-radius: var(--radius-full);
background: var(--ios-blue);
border: none;
cursor: pointer;
@@ -84,7 +84,7 @@
#refresh-btn svg { width: 16px; height: 16px; fill: white; }
/* ── Search/Filter Bar ─────────────────────────────────── */
/* Search/Filter Bar */
#search-bar {
position: fixed;
top: calc(var(--safe-top) + 60px);
@@ -118,7 +118,7 @@
height: 36px;
background: var(--ios-white);
border: none;
border-radius: 10px;
border-radius: var(--radius-md);
padding: 0 10px 0 32px;
font-size: 15px;
font-family: inherit;
@@ -133,7 +133,7 @@
height: 36px;
background: var(--ios-white);
border: none;
border-radius: 10px;
border-radius: var(--radius-md);
padding: 0 10px;
font-size: 13px;
font-family: inherit;
@@ -143,7 +143,7 @@
min-width: 100px;
}
/* ── Map ────────────────────────────────────────────────── */
/* Map */
#map {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
@@ -160,14 +160,14 @@
width: 36px !important;
height: 36px !important;
line-height: 36px !important;
border-radius: 10px !important;
border-radius: var(--radius-md) !important;
}
/* ── Custom Pins ─────────────────────────────────────────── */
/* Custom Pins */
.mission-pin {
width: 36px;
height: 36px;
border-radius: 50%;
border-radius: var(--radius-full);
border: 2.5px solid white;
display: flex;
align-items: center;
@@ -184,9 +184,9 @@
.pin-assigned { background: var(--ios-orange); }
.pin-completed { background: var(--ios-green); }
/* ── Popup ─────────────────────────────────────────────── */
/* Popup */
.leaflet-popup-content-wrapper {
border-radius: 14px !important;
border-radius: var(--radius-lg) !important;
box-shadow: 0 8px 30px rgba(0,0,0,0.18) !important;
padding: 0 !important;
overflow: hidden;
@@ -212,7 +212,7 @@
letter-spacing: 0.5px;
color: var(--ios-blue);
background: rgba(0,122,255,0.1);
border-radius: 6px;
border-radius: var(--radius-sm);
padding: 3px 8px;
margin-bottom: 8px;
}
@@ -233,7 +233,7 @@
.popup-meta-item {
background: var(--ios-gray6);
border-radius: 10px;
border-radius: var(--radius-md);
padding: 8px 10px;
}
@@ -256,7 +256,7 @@
gap: 4px;
font-size: 12px;
font-weight: 600;
border-radius: 20px;
border-radius: var(--radius-xl);
padding: 4px 10px;
margin-top: 10px;
}
@@ -280,7 +280,7 @@
gap: 5px;
font-size: 11px;
font-weight: 700;
border-radius: 8px;
border-radius: var(--radius-md);
padding: 4px 9px;
letter-spacing: 0.2px;
}
@@ -290,7 +290,7 @@
.risk-L4 { background: rgba(255,59,48,0.13); color: #a01a10; }
.risk-hint { font-size: 11px; color: var(--ios-label2); line-height: 1.3; flex: 1; text-align: right; }
/* ── Bottom Sheet ─────────────────────────────────────── */
/* Bottom Sheet */
#bottom-sheet {
position: fixed;
bottom: 0; left: 0; right: 0;
@@ -299,13 +299,13 @@
-webkit-backdrop-filter: blur(20px) saturate(180%);
backdrop-filter: blur(20px) saturate(180%);
border-top: 0.5px solid var(--ios-separator);
border-radius: 20px 20px 0 0;
border-radius: var(--radius-xl) 20px 0 0;
padding: 12px 16px calc(var(--safe-bottom) + 12px);
}
#sheet-handle {
width: 36px; height: 4px;
border-radius: 2px;
border-radius: var(--radius-sm);
background: var(--ios-label3);
margin: 0 auto 14px;
}
@@ -319,7 +319,7 @@
.stat-card {
flex: 1;
background: var(--ios-white);
border-radius: 14px;
border-radius: var(--radius-lg);
padding: 12px 14px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
@@ -358,10 +358,10 @@
.legend-dot {
width: 10px; height: 10px;
border-radius: 50%;
border-radius: var(--radius-full);
}
/* ── Loading Indicator ────────────────────────────────── */
/* Loading Indicator */
#loading-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
@@ -381,7 +381,7 @@
width: 40px; height: 40px;
border: 3px solid var(--ios-gray5);
border-top-color: var(--ios-blue);
border-radius: 50%;
border-radius: var(--radius-full);
animation: spin 0.8s linear infinite;
}
@@ -392,7 +392,7 @@
color: var(--ios-label2);
}
/* ── Urban Intelligence Layer ──────────────────────── */
/* Urban Intelligence Layer */
#ui-toggle {
position: fixed;
top: calc(var(--safe-top) + 110px);
@@ -402,7 +402,7 @@
-webkit-backdrop-filter: blur(20px) saturate(180%);
backdrop-filter: blur(20px) saturate(180%);
border: 0.5px solid var(--ios-separator);
border-radius: 12px;
border-radius: var(--radius-md);
padding: 8px 12px;
display: flex;
align-items: center;
@@ -416,27 +416,27 @@
transition: opacity 0.2s;
}
#ui-toggle:active { opacity: 0.7; }
#ui-dot { width:10px;height:10px;border-radius:50%;background:var(--ios-blue);flex-shrink:0; }
#ui-dot { width:10px;height:10px;border-radius: var(--radius-full);background:var(--ios-blue);flex-shrink:0; }
#ui-toggle.off #ui-dot { background: var(--ios-gray2); }
.ui-popup { font-family: -apple-system,BlinkMacSystemFont,'SF Pro Text',sans-serif; min-width:220px; }
.ui-popup-header { display:flex;align-items:center;justify-content:space-between;margin-bottom:8px; }
.ui-popup-title { font-size:13px;font-weight:700;letter-spacing:-0.2px;color:#1c1c1e; }
.ui-confidence { font-size:11px;color:#8e8e93;background:#f2f2f7;border-radius:6px;padding:2px 7px;font-weight:500; }
.ui-confidence { font-size:11px;color:#8e8e93;background:#f2f2f7;border-radius: var(--radius-sm);padding:2px 7px;font-weight:500; }
.ui-scores { display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px; }
.ui-score-item { background:#f2f2f7;border-radius:8px;padding:6px 8px; }
.ui-score-item { background:#f2f2f7;border-radius: var(--radius-md);padding:6px 8px; }
.ui-score-label { font-size:10px;color:#8e8e93;font-weight:500;margin-bottom:2px; }
.ui-score-bar { height:4px;border-radius:2px;background:#e5e5ea;margin-bottom:2px; }
.ui-score-fill { height:100%;border-radius:2px; }
.ui-score-bar { height:4px;border-radius: var(--radius-sm);background:#e5e5ea;margin-bottom:2px; }
.ui-score-fill { height:100%;border-radius: var(--radius-sm); }
.ui-score-val { font-size:12px;font-weight:700;color:#1c1c1e; }
.ui-contradiction { border-radius:8px;padding:7px 10px;font-size:12px;line-height:1.5;margin-bottom:8px; }
.ui-contradiction { border-radius: var(--radius-md);padding:7px 10px;font-size:12px;line-height:1.5;margin-bottom:8px; }
.ui-contradiction.low { background:rgba(52,199,89,0.12);color:#1c7a3a; }
.ui-contradiction.medium { background:rgba(255,149,0,0.12);color:#a05a00; }
.ui-contradiction.high { background:rgba(255,59,48,0.12);color:#a01a10; }
.ui-contradiction-label { font-weight:700;display:block;margin-bottom:2px; }
.ui-assessment { font-size:11px;color:#8e8e93;border-top:0.5px solid #e5e5ea;padding-top:6px;line-height:1.4; }
/* ── Refresh pulse ────────────────────────────────────── */
/* Refresh pulse */
@keyframes pulse-ring {
0% { box-shadow: 0 0 0 0 rgba(0,122,255,0.4); }
70% { box-shadow: 0 0 0 8px rgba(0,122,255,0); }
@@ -545,13 +545,13 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<script>
// ── Config ────────────────────────────────────────────────
// Config
const API_URL = '/zoomer/missions?limit=100';
const REFRESH_INTERVAL = 30000;
const SWEDEN_CENTER = [62.0, 16.5];
const INITIAL_ZOOM = 5;
const PIN_EMOJI = { open: '📍', assigned: '🔵', completed: '' };
const PIN_EMOJI = { open: '', assigned: '', completed: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' };
const STATUS_COLOR = { open: '#007AFF', assigned: '#FF9500', completed: '#34C759' };
const STATUS_LABEL = { open: 'Öppen', assigned: 'Pågående', completed: 'Klar' };
@@ -573,12 +573,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
return 'L1';
}
// ── State ─────────────────────────────────────────────────
// State
let allMissions = [];
let markers = {};
let refreshTimer = null;
// ── Map Init ──────────────────────────────────────────────
// Map Init
const map = L.map('map', {
center: SWEDEN_CENTER,
zoom: INITIAL_ZOOM,
@@ -594,7 +594,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
// Subtle attribution
L.control.attribution({ position: 'bottomright', prefix: '© OSM' }).addTo(map);
// ── Marker creation ────────────────────────────────────────
// Marker creation
function createPinIcon(status) {
const color = STATUS_COLOR[status] || '#8E8E93';
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="44" viewBox="0 0 36 44">
@@ -636,7 +636,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<div class="popup-status-badge ${statusClass}">
<span></span> ${STATUS_LABEL[m.status] || m.status}
<span></span> ${STATUS_LABEL[m.status] || m.status}
</div>
<div class="popup-risk">
<span class="risk-badge risk-${rl}">${RISK_LEVELS[rl].label}</span>
@@ -649,7 +649,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Load Missions ─────────────────────────────────────────
// Load Missions
async function loadMissions(manual = false) {
const btn = document.getElementById('refresh-btn');
btn.classList.add('loading');
@@ -677,7 +677,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
document.getElementById('loading-overlay').classList.add('hidden');
} catch (e) {
console.error('Failed to load missions:', e);
document.getElementById('loading-text').textContent = 'Kunde inte ladda missioner ⚠️';
document.getElementById('loading-text').textContent = 'Kunde inte ladda missioner <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg>';
} finally {
btn.classList.remove('loading');
// Schedule next refresh
@@ -735,7 +735,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
: `${totalReward}`;
}
// ── Urban Intelligence Layer ──────────────────────────
// Urban Intelligence Layer
// Demo zones — production: fetch from /api/urban-intelligence/zones
const UI_ZONES = [
@@ -765,7 +765,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
darkred: {color:'#8B0000',fillOpacity:0.30,weight:2.5,dash:'4 3'}
};
const C_LABELS = {low:'Contradiction Score: Låg',medium:'Contradiction Score: Medel',high:'Contradiction Score: HÖG ⚠️'};
const C_LABELS = {low:'Contradiction Score: Låg',medium:'Contradiction Score: Medel',high:'Contradiction Score: HÖG <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg>'};
let uiGroup = L.layerGroup().addTo(map);
let uiOn = true;
@@ -813,7 +813,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
tog.onclick = () => { uiOn=!uiOn; tog.classList.toggle('off',!uiOn); renderUI(); };
document.body.appendChild(tog);
// ── Start ─────────────────────────────────────────────────
// Start
loadMissions();
</script>
</body>
+49 -49
View File
@@ -52,7 +52,7 @@
/* Hero Card */
.hero-card {
background: var(--background-secondary);
border-radius: 16px;
border-radius: var(--radius-lg);
margin: 16px 0;
overflow: hidden;
border: 0.5px solid var(--separator);
@@ -62,7 +62,7 @@
padding: 16px;
}
.hero-icon {
width: 52px; height: 52px; border-radius: 12px;
width: 52px; height: 52px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 26px; flex-shrink: 0;
}
@@ -103,7 +103,7 @@
}
.section-card {
background: var(--background-secondary);
border-radius: 14px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.section-row {
@@ -122,7 +122,7 @@
font-size: 15px; line-height: 1.55;
color: var(--label-primary); padding: 14px 16px;
background: var(--background-secondary);
border-radius: 14px;
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
}
@@ -133,7 +133,7 @@
position: relative;
}
.step-num {
width: 28px; height: 28px; border-radius: 50%;
width: 28px; height: 28px; border-radius: var(--radius-full);
background: var(--ios-blue); color: #fff;
display: flex; align-items: center; justify-content: center;
font-size: 14px; font-weight: 700; flex-shrink: 0;
@@ -146,9 +146,9 @@
/* Photo Section */
.photo-wrap { padding: 14px 16px; }
.photo-grid { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.photo-thumb { width: 80px; height: 80px; border-radius: 10px; object-fit: cover; border: 0.5px solid var(--separator); }
.photo-thumb { width: 80px; height: 80px; border-radius: var(--radius-md); object-fit: cover; border: 0.5px solid var(--separator); }
.photo-add-btn {
width: 80px; height: 80px; border-radius: 10px;
width: 80px; height: 80px; border-radius: var(--radius-md);
background: var(--fill-secondary);
border: 1.5px dashed var(--ios-blue);
display: flex; flex-direction: column;
@@ -170,7 +170,7 @@
}
.action-btn {
flex: 1; padding: 14px;
border: none; border-radius: 14px;
border: none; border-radius: var(--radius-lg);
font-family: var(--font-text); font-size: 16px; font-weight: 700;
cursor: pointer; transition: transform 0.1s, opacity 0.1s;
letter-spacing: -0.2px;
@@ -184,7 +184,7 @@
/* Badge */
.status-badge {
display: inline-block; padding: 4px 10px; border-radius: 20px;
display: inline-block; padding: 4px 10px; border-radius: var(--radius-xl);
font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px;
}
.badge-open { background: rgba(52,199,89,0.15); color: var(--ios-green); }
@@ -195,10 +195,10 @@
/* Loader */
.loader { display:flex; justify-content:center; padding:80px; }
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { width:32px; height:32px; border-radius:50%; border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; }
.spinner { width:32px; height:32px; border-radius: var(--radius-full); border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; }
/* Toast */
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius:20px; padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius: var(--radius-xl); padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
/* Icons bg */
@@ -211,7 +211,7 @@
.gps-chip {
display: inline-flex; align-items: center; gap: 4px;
padding: 4px 10px; border-radius: 20px;
padding: 4px 10px; border-radius: var(--radius-xl);
background: rgba(52,199,89,0.15); color: var(--ios-green);
font-size: 12px; font-weight: 600;
}
@@ -318,14 +318,14 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
function missionTypeConfig(type) {
const map = {
facade_scan: { icon:'🏢', cls:'icon-facade', label:'Fasadscanning' },
roof_scan: { icon:'🏠', cls:'icon-roof', label:'Takscanning' },
road_inspection: { icon:'🛣️', cls:'icon-road', label:'Väginspekt.' },
interior_scan: { icon:'🏚️', cls:'icon-interior', label:'Interi ör' },
vegetation_survey: { icon:'🌳', cls:'icon-vegetation', label:'Vegetationssurv.' },
inspection: { icon:'🔍', cls:'icon-inspection', label:'Inspektion' },
facade_scan: { 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 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg>', cls:'icon-facade', label:'Fasadscanning' },
roof_scan: { icon:'', cls:'icon-roof', label:'Takscanning' },
road_inspection: { icon:'', cls:'icon-road', label:'Väginspekt.' },
interior_scan: { icon:'', cls:'icon-interior', label:'Interi ör' },
vegetation_survey: { icon:'', cls:'icon-vegetation', label:'Vegetationssurv.' },
inspection: { 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>', cls:'icon-inspection', label:'Inspektion' },
};
return map[type] || { icon:'📍', cls:'icon-inspection', label: type };
return map[type] || { icon:'', cls:'icon-inspection', label: type };
}
function getInstructions(type) {
@@ -369,7 +369,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution:'© OpenStreetMap' }).addTo(map);
const icon = L.divIcon({
html: `<div style="width:40px;height:40px;border-radius:50%;background:var(--ios-blue);display:flex;align-items:center;justify-content:center;font-size:20px;box-shadow:0 2px 10px rgba(0,0,0,0.4);border:2.5px solid white;">${missionTypeConfig(mission.mission_type).icon}</div>`,
html: `<div style="width:40px;height:40px;border-radius: var(--radius-full);background:var(--ios-blue);display:flex;align-items:center;justify-content:center;font-size:20px;box-shadow:0 2px 10px rgba(0,0,0,0.4);border:2.5px solid white;">${missionTypeConfig(mission.mission_type).icon}</div>`,
className:'', iconSize:[40,40], iconAnchor:[20,20]
});
L.marker([lat, lon], {icon}).addTo(map).openPopup();
@@ -395,7 +395,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="hero-icon ${cfg.cls}">${cfg.icon}</div>
<div class="hero-info">
<div class="hero-title">${mission.title}</div>
<div class="hero-address">📍 ${mission.address || 'Okänd plats'}</div>
<div class="hero-address"> ${mission.address || 'Okänd plats'}</div>
<div style="margin-top:6px">${statusBadge(mission.status)}</div>
</div>
</div>
@@ -409,7 +409,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="stat-label">Uppdragstyp</div>
</div>
<div class="stat-cell">
<div class="stat-value gps-chip ${!userLat ? 'searching' : ''}">${!userLat ? '📡 GPS' : '📍 ' + distText}</div>
<div class="stat-value gps-chip ${!userLat ? 'searching' : ''}">${!userLat ? ' GPS' : ' ' + distText}</div>
<div class="stat-label">Avstånd</div>
</div>
</div>
@@ -424,7 +424,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-title">Information</div>
<div class="section-card">
<div class="section-row">
<div class="row-icon">📅</div>
<div class="row-icon"></div>
<div class="row-content">
<div class="row-title">Skapat</div>
<div class="row-sub">${formatDate(mission.created_at)}</div>
@@ -432,7 +432,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
${mission.accepted_at ? `
<div class="section-row">
<div class="row-icon"></div>
<div class="row-icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<div class="row-content">
<div class="row-title">Accepterat</div>
<div class="row-sub">${formatDate(mission.accepted_at)}</div>
@@ -440,7 +440,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>` : ''}
${mission.submitted_at ? `
<div class="section-row">
<div class="row-icon">📤</div>
<div class="row-icon"></div>
<div class="row-content">
<div class="row-title">Inskickat</div>
<div class="row-sub">${formatDate(mission.submitted_at)}</div>
@@ -448,14 +448,14 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>` : ''}
${mission.approved_at ? `
<div class="section-row">
<div class="row-icon">🎉</div>
<div class="row-icon"></div>
<div class="row-content">
<div class="row-title">Godkänt & betalt</div>
<div class="row-sub">${formatDate(mission.approved_at)} · Ref: ${mission.payment_ref || '—'}</div>
</div>
</div>` : ''}
<div class="section-row">
<div class="row-icon">🆔</div>
<div class="row-icon"></div>
<div class="row-content">
<div class="row-title">Uppdragsnr</div>
<div class="row-sub" style="font-family:var(--font-mono);font-size:12px">${mission.id}</div>
@@ -480,7 +480,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
${(isMyMission && mission.status === 'assigned') ? `
<div class="section">
<div class="section-title">📷 Dokumentation</div>
<div class="section-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg> Dokumentation</div>
<div class="section-card">
<div class="photo-wrap">
<div style="font-size:13px;color:var(--label-secondary);margin-bottom:6px">
@@ -488,7 +488,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="photo-grid" id="photo-grid">
<div class="photo-add-btn" onclick="document.getElementById('photo-input').click()">
📷<span>Lägg till</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg><span>Lägg till</span>
</div>
</div>
</div>
@@ -508,16 +508,16 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (mission.status === 'open') {
bar.innerHTML = `
<button class="action-btn btn-decline" onclick="history.back()"> Avböj</button>
<button class="action-btn btn-accept" onclick="acceptMission()"> Acceptera uppdrag</button>`;
<button class="action-btn btn-decline" onclick="history.back()"><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> Avböj</button>
<button class="action-btn btn-accept" onclick="acceptMission()"><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> Acceptera uppdrag</button>`;
} else if (mission.status === 'assigned' && isMyMission) {
bar.innerHTML = `
<button class="action-btn btn-camera" onclick="document.getElementById('photo-input').click()">📷</button>
<button class="action-btn btn-complete" onclick="submitWithPhotos()"> Slutför & skicka in</button>`;
<button class="action-btn btn-camera" onclick="document.getElementById('photo-input').click()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg></button>
<button class="action-btn btn-complete" onclick="submitWithPhotos()"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Slutför & skicka in</button>`;
} else if (mission.status === 'submitted') {
bar.innerHTML = `<button class="action-btn btn-disabled">⏳ Väntar på granskning</button>`;
} else if (mission.status === 'approved') {
bar.innerHTML = `<button class="action-btn btn-disabled" style="background:rgba(52,199,89,0.15);color:var(--ios-green)">🎉 Godkänt & betalt</button>`;
bar.innerHTML = `<button class="action-btn btn-disabled" style="background:rgba(52,199,89,0.15);color:var(--ios-green)"> Godkänt & betalt</button>`;
} else {
bar.innerHTML = `<button class="action-btn btn-decline" onclick="history.back()">← Tillbaka</button>`;
}
@@ -538,15 +538,15 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (r.ok) {
mission = data;
setActiveMission(data);
toast(' Uppdrag accepterat! Dra till platsen och starta.');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Uppdrag accepterat! Dra till platsen och starta.');
renderContent();
} else {
toast('⚠️ ' + (data.error || 'Kunde inte acceptera'));
if (btn) { btn.textContent = ' Acceptera uppdrag'; btn.disabled = false; }
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> ' + (data.error || 'Kunde inte acceptera'));
if (btn) { btn.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> Acceptera uppdrag'; btn.disabled = false; }
}
} catch(e) {
toast('⚠️ Nätverksfel');
if (btn) { btn.textContent = ' Acceptera uppdrag'; btn.disabled = false; }
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Nätverksfel');
if (btn) { btn.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> Acceptera uppdrag'; btn.disabled = false; }
}
}
@@ -565,16 +565,16 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
grid.insertBefore(img, grid.firstChild);
});
toast(`📷 ${uploadedFiles.length} foto valda`);
toast(`<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg> ${uploadedFiles.length} foto valda`);
const bar = document.getElementById('bottom-actions');
bar.innerHTML = `
<button class="action-btn btn-camera" onclick="document.getElementById('photo-input').click()">📷 (${uploadedFiles.length})</button>
<button class="action-btn btn-complete" onclick="submitWithPhotos()"> Skicka in ${uploadedFiles.length} bilder</button>`;
<button class="action-btn btn-camera" onclick="document.getElementById('photo-input').click()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg> (${uploadedFiles.length})</button>
<button class="action-btn btn-complete" onclick="submitWithPhotos()"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Skicka in ${uploadedFiles.length} bilder</button>`;
});
async function submitWithPhotos() {
if (uploadedFiles.length === 0) {
toast('📷 Ta minst ett foto innan du slutför');
toast('<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg> Ta minst ett foto innan du slutför');
return;
}
const btn = document.querySelector('.btn-complete');
@@ -589,16 +589,16 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (r.ok) {
mission = data.mission;
setActiveMission(null);
toast('🎉 Inskickat! Väntar på granskning och utbetalning.');
toast(' Inskickat! Väntar på granskning och utbetalning.');
uploadedFiles = [];
setTimeout(() => window.location.href = 'zoomer-app.html', 2000);
} else {
toast('⚠️ ' + (data.error || 'Fel vid inskick'));
if (btn) { btn.textContent = ' Skicka in'; btn.disabled = false; }
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> ' + (data.error || 'Fel vid inskick'));
if (btn) { btn.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Skicka in'; btn.disabled = false; }
}
} catch(e) {
toast('⚠️ Nätverksfel — försök igen');
if (btn) { btn.textContent = ' Skicka in'; btn.disabled = false; }
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Nätverksfel — försök igen');
if (btn) { btn.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Skicka in'; btn.disabled = false; }
}
}
@@ -628,7 +628,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
initMap(mission.lat, mission.lon);
renderContent();
} catch(e) {
document.getElementById('main-content').innerHTML = `<div style="padding:40px;text-align:center"><div style="font-size:48px">😵</div><div style="margin-top:12px;color:var(--label-secondary)">Kunde inte ladda uppdrag</div></div>`;
document.getElementById('main-content').innerHTML = `<div style="padding:40px;text-align:center"><div style="font-size:48px"></div><div style="margin-top:12px;color:var(--label-secondary)">Kunde inte ladda uppdrag</div></div>`;
}
}
+27 -27
View File
@@ -31,7 +31,7 @@
.logo { font-size: 18px; font-weight: 700; letter-spacing: -0.5px; }
.logo span { color: var(--accent); }
.badge-live { background: var(--accent); color: #fff; font-size: 10px; font-weight: 700;
padding: 2px 6px; border-radius: 4px; letter-spacing: 0.5px; animation: pulse 2s infinite; }
padding: 2px 6px; border-radius: var(--radius-sm); letter-spacing: 0.5px; animation: pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.6} }
/* Stats bar */
@@ -55,7 +55,7 @@
}
.filter-btn {
background: var(--surface); color: var(--text2); border: 1px solid var(--border);
border-radius: 8px; padding: 5px 14px; font-size: 13px; cursor: pointer; transition: all 0.15s;
border-radius: var(--radius-md); padding: 5px 14px; font-size: 13px; cursor: pointer; transition: all 0.15s;
font-family: var(--font);
}
.filter-btn:hover { border-color: rgba(255,255,255,0.2); color: var(--text); }
@@ -65,26 +65,26 @@
#map { flex: 1; }
/* Popup styling */
.leaflet-popup-content-wrapper { background: #F2F2F7; border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.6); }
.leaflet-popup-content-wrapper { background: #F2F2F7; border: 1px solid rgba(255,255,255,0.12); border-radius: var(--radius-md); box-shadow: 0 8px 32px rgba(0,0,0,0.6); }
.leaflet-popup-tip { background: #F2F2F7; }
.leaflet-popup-content { color: #1c1c1e; margin: 0; padding: 0; }
.popup { padding: 14px; min-width: 220px; }
.popup-title { font-size: 14px; font-weight: 600; margin-bottom: 8px; }
.popup-row { display: flex; justify-content: space-between; font-size: 12px; color: #555; margin-bottom: 4px; }
.popup-row span:last-child { color: #f5f5f7; }
.popup-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; text-transform: uppercase; margin-top: 8px; }
.popup-badge { display: inline-block; padding: 2px 8px; border-radius: var(--radius-sm); font-size: 10px; font-weight: 600; text-transform: uppercase; margin-top: 8px; }
.badge-urgent { background: rgba(255,59,48,0.2); color: #ff3b30; }
.badge-normal { background: rgba(52,199,89,0.15); color: #34c759; }
.badge-pending { background: rgba(255,159,10,0.15); color: #ff9f0a; }
/* Status dot */
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 5px; }
.status-dot { width: 8px; height: 8px; border-radius: var(--radius-full); display: inline-block; margin-right: 5px; }
.dot-active { background: var(--accent2); animation: glow 1.5s infinite; }
.dot-pending { background: #ff9f0a; }
@keyframes glow { 0%,100%{box-shadow:0 0 0 0 rgba(52,199,89,0.4)} 50%{box-shadow:0 0 0 4px rgba(52,199,89,0)} }
/* Connection status */
.conn-badge { font-size: 11px; padding: 3px 8px; border-radius: 6px; }
.conn-badge { font-size: 11px; padding: 3px 8px; border-radius: var(--radius-sm); }
.conn-connected { background: rgba(52,199,89,0.2); color: var(--accent2); }
.conn-disconnected { background: rgba(255,59,48,0.2); color: var(--accent); }
.conn-connecting { background: rgba(255,159,10,0.2); color: #ff9f0a; }
@@ -133,7 +133,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="header">
<div class="header-left">
<div class="logo">qui<span>X</span>zoom</div>
<span class="badge-live"> LIVE</span>
<span class="badge-live"> LIVE</span>
</div>
<div style="display:flex;align-items:center;gap:10px;">
<span id="conn-status" class="conn-badge conn-connecting">Ansluter…</span>
@@ -171,7 +171,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ── Map init ─────────────────────────────────────────────────────────────
// Map init
const map = L.map('map', {
center: [7.9519, 98.2849],
zoom: 12,
@@ -184,7 +184,7 @@ L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
maxZoom: 19,
}).addTo(map);
// ── State ────────────────────────────────────────────────────────────────
// State
let missionMarkers = {};
let zoomerMarkers = {};
let currentFilter = 'all';
@@ -192,12 +192,12 @@ let currentMissions = [];
let currentZoomers = [];
let clientCount = 0;
// ── Icons ────────────────────────────────────────────────────────────────
// Icons
function missionIcon(priority, status) {
const color = status === 'urgent' || priority === 'urgent' ? '#ff3b30' : status === 'completed' ? '#636366' : '#ff9f0a';
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="40" viewBox="0 0 32 40">
<path d="M16 0C7.16 0 0 7.16 0 16c0 12 16 24 16 24s16-12 16-24C32 7.16 24.84 0 16 0z" fill="${color}" opacity="0.95"/>
<text x="16" y="21" text-anchor="middle" font-size="14" fill="white">📷</text>
<text x="16" y="21" text-anchor="middle" font-size="14" fill="white"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg></text>
</svg>`;
return L.divIcon({ html: svg, iconSize: [32, 40], iconAnchor: [16, 40], popupAnchor: [0, -40], className: '' });
}
@@ -215,7 +215,7 @@ function zoomerIcon(name) {
return L.divIcon({ html: svg, iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14], className: '' });
}
// ── Filter ───────────────────────────────────────────────────────────────
// Filter
function setFilter(filter) {
currentFilter = filter;
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
@@ -223,7 +223,7 @@ function setFilter(filter) {
renderMissions(currentMissions);
}
// ── Render missions ───────────────────────────────────────────────────────
// Render missions
function renderMissions(missions) {
currentMissions = missions;
// Remove old markers
@@ -237,14 +237,14 @@ function renderMissions(missions) {
const marker = L.marker([mission.lat, mission.lng], { icon }).addTo(map);
const priorityBadge = mission.priority === 'urgent'
? '<span class="popup-badge badge-urgent">🔴 Urgent</span>'
? '<span class="popup-badge badge-urgent"> Urgent</span>'
: mission.status === 'pending'
? '<span class="popup-badge badge-pending">⏳ Väntande</span>'
: '<span class="popup-badge badge-normal"> Aktiv</span>';
: '<span class="popup-badge badge-normal"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Aktiv</span>';
marker.bindPopup(`
<div class="popup">
<div class="popup-title">📷 ${mission.title}</div>
<div class="popup-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg> ${mission.title}</div>
<div class="popup-row"><span>Kund</span><span>${mission.customer}</span></div>
<div class="popup-row"><span>Typ</span><span>${mission.type}</span></div>
<div class="popup-row"><span>Status</span><span><span class="status-dot dot-${mission.status === 'active' ? 'active' : 'pending'}"></span>${mission.status}</span></div>
@@ -257,7 +257,7 @@ function renderMissions(missions) {
});
}
// ── Render zoomers ────────────────────────────────────────────────────────
// Render zoomers
function renderZoomers(zoomers) {
currentZoomers = zoomers;
// Move existing or create new
@@ -271,7 +271,7 @@ function renderZoomers(zoomers) {
<div class="popup">
<div class="popup-title">🟢 ${z.name}</div>
<div class="popup-row"><span>ID</span><span>${z.id}</span></div>
<div class="popup-row"><span>Betyg</span><span> ${z.rating}</span></div>
<div class="popup-row"><span>Betyg</span><span> ${z.rating}</span></div>
<div class="popup-row"><span>Status</span><span>Online</span></div>
</div>
`, { maxWidth: 220 });
@@ -280,7 +280,7 @@ function renderZoomers(zoomers) {
});
}
// ── Stats ─────────────────────────────────────────────────────────────────
// Stats
function updateStats(stats) {
document.getElementById('stat-active').textContent = stats.active_missions ?? '—';
document.getElementById('stat-zoomers').textContent = stats.zoomers_online ?? '—';
@@ -289,7 +289,7 @@ function updateStats(stats) {
document.getElementById('stat-clients').textContent = clientCount;
}
// ── WebSocket ─────────────────────────────────────────────────────────────
// WebSocket
let ws;
let reconnectTimer;
@@ -299,7 +299,7 @@ function connect() {
ws = new WebSocket(wsUrl);
ws.onopen = () => {
document.getElementById('conn-status').textContent = ' Ansluten';
document.getElementById('conn-status').textContent = ' Ansluten';
document.getElementById('conn-status').className = 'conn-badge conn-connected';
clearTimeout(reconnectTimer);
};
@@ -322,12 +322,12 @@ function connect() {
delete missionMarkers[d.missionId];
}
// Toast
showToast(` Uppdrag klart: ${d.title}`);
showToast(`<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Uppdrag klart: ${d.title}`);
}
};
ws.onclose = () => {
document.getElementById('conn-status').textContent = ' Frånkopplad';
document.getElementById('conn-status').textContent = ' Frånkopplad';
document.getElementById('conn-status').className = 'conn-badge conn-disconnected';
reconnectTimer = setTimeout(() => {
document.getElementById('conn-status').textContent = '… Återansluter';
@@ -342,7 +342,7 @@ function connect() {
};
}
// ── Fallback polling (if WS not available) ────────────────────────────────
// Fallback polling (if WS not available)
let pollInterval;
function fallbackPolling() {
if (pollInterval) return;
@@ -361,16 +361,16 @@ function fallbackPolling() {
}, 3000);
}
// ── Toast notification ────────────────────────────────────────────────────
// Toast notification
function showToast(msg) {
const t = document.createElement('div');
t.style.cssText = 'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1c1c1e;color:#f5f5f7;border:1px solid rgba(255,255,255,0.12);border-radius:10px;padding:10px 20px;font-size:14px;z-index:9999;box-shadow:0 4px 20px rgba(0,0,0,0.5);';
t.style.cssText = 'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1c1c1e;color:#f5f5f7;border:1px solid rgba(255,255,255,0.12);border-radius: var(--radius-md);padding:10px 20px;font-size:14px;z-index:9999;box-shadow:0 4px 20px rgba(0,0,0,0.5);';
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 4000);
}
// ── Check URL params for mission focus ────────────────────────────────────
// Check URL params for mission focus
const urlParams = new URLSearchParams(location.search);
const focusMission = urlParams.get('mission');
+149
View File
@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>quiXzoom — Rakam Dunia. Dapatkan Bayaran. | Jadi Zoomer</title>
<meta name="description" content="quiXzoom membayar anda untuk merakam infrastruktur, alam semula jadi, dan persekitaran bandar. Tiada yuran platform. Dilancarkan Ogos 2026.">
<link rel="canonical" href="https://quixzoom.asia/ms/">
<link rel="alternate" hreflang="ms" href="https://quixzoom.asia/ms/">
<link rel="alternate" hreflang="zh-CN" href="https://quixzoom.asia/zh-cn/">
<link rel="alternate" hreflang="ja" href="https://quixzoom.asia/ja/">
<link rel="alternate" hreflang="en" href="https://www.quixzoom.com/">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: #0a0a0a;
color: #ffffff;
line-height: 1.6;
}
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
header {
padding: 24px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 24px; font-weight: 700; color: #0066ff; }
.lang-switcher { display: flex; gap: 16px; }
.lang-switcher a {
color: rgba(255,255,255,0.6);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.lang-switcher a:hover, .lang-switcher a.active { color: #fff; }
.hero {
padding: 120px 0 80px;
text-align: center;
}
.hero h1 {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 800;
line-height: 1.2;
margin-bottom: 24px;
}
.hero p {
font-size: 1.25rem;
color: rgba(255,255,255,0.7);
max-width: 600px;
margin: 0 auto 40px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 16px 32px;
background: #0066ff;
color: #fff;
text-decoration: none;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0,102,255,0.3);
}
.features {
padding: 80px 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 32px;
}
.feature {
background: rgba(255,255,255,0.05);
padding: 32px;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.1);
}
.feature h3 {
font-size: 1.25rem;
margin-bottom: 12px;
color: #fff;
}
.feature p {
color: rgba(255,255,255,0.6);
font-size: 0.9375rem;
}
.currency-note {
text-align: center;
padding: 40px 0;
color: rgba(255,255,255,0.5);
font-size: 0.875rem;
}
footer {
padding: 40px 0;
text-align: center;
color: rgba(255,255,255,0.4);
font-size: 0.875rem;
border-top: 1px solid rgba(255,255,255,0.1);
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">quiXzoom</div>
<nav class="lang-switcher">
<a href="/ms/" class="active">Bahasa Melayu</a>
<a href="/zh-cn/"></a>
<a href="/ja/"></a>
<a href="/en/">EN</a>
</nav>
</header>
<section class="hero">
<h1>Rakam Dunia.<br>Dapatkan Bayaran.</h1>
<p>quiXzoom membayar anda untuk merakam infrastruktur, persekitaran semula jadi, dan landskap bandar. Tiada yuran platform. Bayaran terus untuk setiap misi yang diluluskan.</p>
<a href="https://www.quixzoom.com/register" class="btn">Daftar sebagai Zoomer</a>
</section>
<section class="features">
<div class="feature">
<h3>Rakam dan Hasilkan</h3>
<p>Terima misi berhampiran dan rakam mengikut arahan. Dapatkan RM50-500 untuk setiap misi yang diluluskan.</p>
</div>
<div class="feature">
<h3>Bayaran Pantas</h3>
<p>Bayaran terus ke akaun bank atau Touch 'n Go eWallet dalam masa 24 jam selepas diluluskan.</p>
</div>
<div class="feature">
<h3>Fleksibel dan Bebas</h3>
<p>Bekerja bila-bila masa, di mana sahaja. Tiada keperluan minimum, tiada jadual. Anda memutuskan.</p>
</div>
</section>
<div class="currency-note">
Harga dipaparkan dalam Ringgit Malaysia (MYR). Akan ditukar mengikut kadar semasa semasa pembayaran.
</div>
<footer>
<p>quiXzoom Asia | Sokongan: asia@quixzoom.com</p>
</footer>
</div>
</body>
</html>
+4 -4
View File
@@ -54,7 +54,7 @@
.pricing-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 20px;
border-radius: var(--radius-xl);
padding: 40px 32px;
position: relative;
}
@@ -78,12 +78,12 @@
display: flex; align-items: center; gap: 8px;
}
.pricing-card ul li::before {
content: ""; color: var(--blue); font-weight: 700;
content: "<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>"; color: var(--blue); font-weight: 700;
}
.btn {
display: block; width: 100%; padding: 14px;
background: var(--blue); color: #fff;
border: none; border-radius: 12px;
border: none; border-radius: var(--radius-md);
font-size: 1rem; font-weight: 600;
text-align: center; text-decoration: none;
cursor: pointer; transition: background 0.2s;
@@ -101,7 +101,7 @@
.faq-item {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
border-radius: var(--radius-md);
padding: 24px;
margin-bottom: 12px;
}
+1 -1
View File
@@ -52,7 +52,7 @@
li { margin-bottom: 6px; }
.row { display: flex; align-items: flex-start; gap: 12px; padding: 12px 0; border-bottom: 0.5px solid var(--separator); }
.row:last-child { border-bottom: none; }
.row-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.row-icon { width: 36px; height: 36px; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.blue { background: rgba(0,102,255,0.12); }
.green { background: rgba(52,199,89,0.12); }
.orange { background: rgba(255,149,0,0.12); }
+15 -15
View File
@@ -22,7 +22,7 @@
body { background: var(--bg); color: var(--text); font-family: var(--font); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 16px; }
.widget {
background: var(--surface); border-radius: 20px; padding: 28px 24px;
background: var(--surface); border-radius: var(--radius-xl); padding: 28px 24px;
max-width: 380px; width: 100%; box-shadow: 0 4px 32px rgba(0,0,0,0.12);
}
@@ -32,9 +32,9 @@
.widget-subtitle { font-size: 14px; color: var(--text2); margin-top: 4px; }
/* Zoomer profile */
.zoomer-profile { display: flex; align-items: center; gap: 14px; padding: 14px; background: var(--bg); border-radius: 14px; margin-bottom: 20px; }
.zoomer-profile { display: flex; align-items: center; gap: 14px; padding: 14px; background: var(--bg); border-radius: var(--radius-lg); margin-bottom: 20px; }
.zoomer-avatar {
width: 48px; height: 48px; border-radius: 50%; background: linear-gradient(135deg, #34c759, #30b952);
width: 48px; height: 48px; border-radius: var(--radius-full); background: linear-gradient(135deg, #34c759, #30b952);
display: flex; align-items: center; justify-content: center; font-weight: 700; color: white; font-size: 18px; flex-shrink: 0;
}
.zoomer-name { font-size: 16px; font-weight: 600; }
@@ -53,7 +53,7 @@
/* Comment */
.comment-label { font-size: 13px; font-weight: 500; margin-bottom: 6px; }
.comment-input {
width: 100%; background: var(--bg); border: 1.5px solid var(--border); border-radius: 12px;
width: 100%; background: var(--bg); border: 1.5px solid var(--border); border-radius: var(--radius-md);
padding: 12px 14px; font-size: 14px; color: var(--text); font-family: var(--font);
resize: none; transition: border-color 0.15s;
}
@@ -63,7 +63,7 @@
/* Submit */
.submit-btn {
width: 100%; margin-top: 16px; padding: 14px; background: var(--accent);
color: white; border: none; border-radius: 14px; font-size: 16px; font-weight: 600;
color: white; border: none; border-radius: var(--radius-lg); font-size: 16px; font-weight: 600;
cursor: pointer; font-family: var(--font); transition: opacity 0.15s;
}
.submit-btn:hover:not(:disabled) { opacity: 0.88; }
@@ -135,11 +135,11 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div id="rating-form">
<div class="stars-label">Hur var upplevelsen?</div>
<div class="stars" id="stars">
<span class="star" data-val="1"></span>
<span class="star" data-val="2"></span>
<span class="star" data-val="3"></span>
<span class="star" data-val="4"></span>
<span class="star" data-val="5"></span>
<span class="star" data-val="1"></span>
<span class="star" data-val="2"></span>
<span class="star" data-val="3"></span>
<span class="star" data-val="4"></span>
<span class="star" data-val="5"></span>
</div>
<div class="star-labels">
<span>Dålig</span>
@@ -158,7 +158,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="success" id="success">
<div class="success-icon"></div>
<div class="success-icon"></div>
<div class="success-title">Tack för ditt betyg!</div>
<div class="success-sub">Ditt omdöme hjälper andra kunder att välja rätt Zoomer.</div>
</div>
@@ -179,7 +179,7 @@ const missionId = params.get('missionId') || 'm001';
let selectedRating = 0;
// ── Stars interaction ─────────────────────────────────────────────────────
// Stars interaction
const stars = document.querySelectorAll('.star');
stars.forEach(star => {
star.addEventListener('mouseenter', () => highlightStars(parseInt(star.dataset.val)));
@@ -198,7 +198,7 @@ function highlightStars(n) {
});
}
// ── Load zoomer info ──────────────────────────────────────────────────────
// Load zoomer info
async function loadZoomer() {
try {
const r = await fetch(`/api/quixzoom/ratings/zoomer/${zoomerId}`);
@@ -222,14 +222,14 @@ function renderReviews(reviews) {
}
list.innerHTML = reviews.map(r => `
<div class="review">
<div class="review-stars">${''.repeat(r.rating)}${''.repeat(5 - r.rating)}</div>
<div class="review-stars">${''.repeat(r.rating)}${''.repeat(5 - r.rating)}</div>
${r.comment ? `<div class="review-comment">"${r.comment}"</div>` : ''}
<div class="review-meta">${new Date(r.createdAt).toLocaleDateString('sv-SE')}</div>
</div>
`).join('');
}
// ── Submit rating ─────────────────────────────────────────────────────────
// Submit rating
async function submitRating() {
const comment = document.getElementById('comment').value;
const btn = document.getElementById('submit-btn');
+177
View File
@@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bli Zoomer — quiXzoom</title>
<link rel="stylesheet" href="/design-tokens.css">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #000;
color: #fff;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
.logo {
font-size: 32px;
font-weight: 800;
background: linear-gradient(135deg, #0A84FF 0%, #5E5CE6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 8px;
}
.subtitle {
color: rgba(255,255,255,0.6);
font-size: 16px;
margin-bottom: 40px;
}
.form-card {
background: rgba(28,28,30,0.9);
border: 0.5px solid rgba(255,255,255,0.1);
border-radius: 20px;
padding: 32px;
width: 100%;
max-width: 400px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
color: rgba(255,255,255,0.8);
}
input {
width: 100%;
padding: 14px 16px;
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.15);
background: rgba(255,255,255,0.05);
color: #fff;
font-size: 16px;
outline: none;
transition: border-color 0.2s;
}
input:focus {
border-color: #0A84FF;
}
input::placeholder {
color: rgba(255,255,255,0.3);
}
.btn {
width: 100%;
padding: 16px;
border-radius: 14px;
border: none;
background: #0A84FF;
color: #fff;
font-size: 17px;
font-weight: 700;
cursor: pointer;
transition: opacity 0.2s;
margin-top: 8px;
}
.btn:active {
opacity: 0.8;
}
.login-link {
text-align: center;
margin-top: 20px;
color: rgba(255,255,255,0.5);
font-size: 15px;
}
.login-link a {
color: #0A84FF;
text-decoration: none;
font-weight: 600;
}
.features {
display: flex;
gap: 16px;
margin-top: 32px;
justify-content: center;
flex-wrap: wrap;
}
.feature {
text-align: center;
color: rgba(255,255,255,0.5);
font-size: 13px;
}
.feature-icon {
font-size: 24px;
margin-bottom: 4px;
}
</style>
</head>
<body>
<div class="logo">quiXzoom</div>
<div class="subtitle">Bli en Zoomer — tjana pengar pa faltet</div>
<div class="form-card">
<form onsubmit="handleRegister(event)">
<div class="form-group">
<label for="name">Fullstandigt namn</label>
<input type="text" id="name" placeholder="Anna Svensson" required>
</div>
<div class="form-group">
<label for="email">E-post</label>
<input type="email" id="email" placeholder="anna@exempel.se" required>
</div>
<div class="form-group">
<label for="phone">Telefonnummer</label>
<input type="tel" id="phone" placeholder="+46 70 123 45 67" required>
</div>
<div class="form-group">
<label for="password">Losenord</label>
<input type="password" id="password" placeholder="Minst 8 tecken" required minlength="8">
</div>
<button type="submit" class="btn">Skapa konto</button>
</form>
<div class="login-link">
Har du redan ett konto? <a href="/login">Logga in</a>
</div>
</div>
<div class="features">
<div class="feature">
<div class="feature-icon">📷</div>
<div>Fotografera</div>
</div>
<div class="feature">
<div class="feature-icon">💰</div>
<div>Tjana pengar</div>
</div>
<div class="feature">
<div class="feature-icon">🌍</div>
<div>Flexibelt</div>
</div>
</div>
<script>
function handleRegister(e) {
e.preventDefault();
const name = document.getElementById('name').value;
const email = document.getElementById('email').value;
// Demo: store in localStorage
localStorage.setItem('qz_user', JSON.stringify({
name: name,
email: email,
registered: new Date().toISOString()
}));
alert('Valkommen till quiXzoom, ' + name + '!\n\nI demo-laget: Omdirigerar till appen...');
window.location.href = '/zoomer-app.html';
}
</script>
</body>
</html>
+34 -34
View File
@@ -38,7 +38,7 @@
.container { max-width: 680px; margin: 0 auto; padding: 24px 16px 48px; }
/* Section */
.section { background: var(--surface); border-radius: 16px; padding: 20px; margin-bottom: 16px; }
.section { background: var(--surface); border-radius: var(--radius-lg); padding: 20px; margin-bottom: 16px; }
.section-title { font-size: 17px; font-weight: 600; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
/* Form fields */
@@ -47,7 +47,7 @@
.field-label { font-size: 13px; font-weight: 500; color: var(--text2); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.3px; }
.field-input {
width: 100%; background: var(--surface2); border: 1.5px solid var(--border);
border-radius: 12px; padding: 12px 14px; font-size: 15px; color: var(--text);
border-radius: var(--radius-md); padding: 12px 14px; font-size: 15px; color: var(--text);
font-family: var(--font); transition: border-color 0.15s;
}
.field-input:focus { outline: none; border-color: var(--blue); }
@@ -56,7 +56,7 @@
select.field-input { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%236e6e73' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 14px center; padding-right: 36px; }
/* Map picker */
#location-map { width: 100%; height: 220px; border-radius: 12px; overflow: hidden; margin-top: 10px; border: 1.5px solid var(--border); }
#location-map { width: 100%; height: 220px; border-radius: var(--radius-md); overflow: hidden; margin-top: 10px; border: 1.5px solid var(--border); }
.location-addr { margin-top: 8px; font-size: 13px; color: var(--text2); display: flex; align-items: center; gap: 6px; }
.location-pin { color: var(--accent); font-size: 16px; }
@@ -64,12 +64,12 @@
.slider-wrap { margin-top: 8px; }
.budget-display { font-size: 24px; font-weight: 700; color: var(--accent); margin-bottom: 10px; }
input[type="range"] {
-webkit-appearance: none; width: 100%; height: 4px; border-radius: 2px;
-webkit-appearance: none; width: 100%; height: 4px; border-radius: var(--radius-sm);
background: linear-gradient(to right, var(--accent) 0%, var(--accent) var(--pct, 20%), var(--surface2) var(--pct, 20%), var(--surface2) 100%);
outline: none; cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; width: 22px; height: 22px; border-radius: 50%;
-webkit-appearance: none; width: 22px; height: 22px; border-radius: var(--radius-full);
background: var(--accent); box-shadow: 0 2px 8px rgba(0,0,0,0.25); cursor: pointer;
}
.budget-labels { display: flex; justify-content: space-between; font-size: 11px; color: var(--text2); margin-top: 4px; }
@@ -77,7 +77,7 @@
/* Camera count */
.camera-selector { display: flex; align-items: center; gap: 14px; }
.count-btn {
width: 36px; height: 36px; border-radius: 50%; background: var(--surface2);
width: 36px; height: 36px; border-radius: var(--radius-full); background: var(--surface2);
border: 1.5px solid var(--border); font-size: 20px; cursor: pointer; display: flex;
align-items: center; justify-content: center; color: var(--text); transition: background 0.1s;
}
@@ -87,7 +87,7 @@
/* Type selector */
.type-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.type-btn {
background: var(--surface2); border: 1.5px solid var(--border); border-radius: 12px;
background: var(--surface2); border: 1.5px solid var(--border); border-radius: var(--radius-md);
padding: 12px 8px; text-align: center; cursor: pointer; transition: all 0.15s;
}
.type-btn:hover { border-color: var(--blue); }
@@ -99,7 +99,7 @@
.submit-section { padding: 0 0 8px; }
.submit-btn {
width: 100%; padding: 16px; background: var(--accent); color: white; border: none;
border-radius: 16px; font-size: 17px; font-weight: 600; cursor: pointer;
border-radius: var(--radius-lg); font-size: 17px; font-weight: 600; cursor: pointer;
font-family: var(--font); transition: opacity 0.15s; display: flex; align-items: center; justify-content: center; gap: 8px;
}
.submit-btn:hover:not(:disabled) { opacity: 0.88; }
@@ -111,14 +111,14 @@
.confirm-title { font-size: 26px; font-weight: 700; letter-spacing: -0.5px; margin-bottom: 8px; }
.confirm-sub { font-size: 15px; color: var(--text2); margin-bottom: 28px; line-height: 1.5; }
.mission-id-box {
background: var(--surface2); border: 1px solid var(--border); border-radius: 14px;
background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius-lg);
padding: 16px; margin-bottom: 24px;
}
.mission-id-label { font-size: 12px; color: var(--text2); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
.mission-id-val { font-size: 22px; font-weight: 700; font-feature-settings: 'tnum'; letter-spacing: 2px; }
.track-btn {
display: inline-block; padding: 14px 32px; background: var(--blue); color: white;
border-radius: 14px; font-size: 16px; font-weight: 600; text-decoration: none;
border-radius: var(--radius-lg); font-size: 16px; font-weight: 600; text-decoration: none;
transition: opacity 0.15s;
}
.track-btn:hover { opacity: 0.88; }
@@ -174,14 +174,14 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Location -->
<div class="section">
<div class="section-title">📍 Plats</div>
<div class="section-title"> Plats</div>
<div class="field">
<div class="field-label">Adress</div>
<input class="field-input" id="address" type="text" placeholder="Ange adress eller klicka på kartan…" oninput="updateAddress(this.value)"/>
</div>
<div id="location-map"></div>
<div class="location-addr">
<span class="location-pin">📌</span>
<span class="location-pin"></span>
<span id="chosen-location">Klicka på kartan för att välja plats</span>
</div>
<input type="hidden" id="lat" value="7.9519"/>
@@ -190,7 +190,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Mission details -->
<div class="section">
<div class="section-title">📋 Uppdragsdetaljer</div>
<div class="section-title"><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> Uppdragsdetaljer</div>
<div class="field">
<div class="field-label">Titel / Beskrivning av uppdrag</div>
<input class="field-input" id="title" type="text" placeholder="T.ex. Hotell-lobby fotografering…"/>
@@ -203,27 +203,27 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="field-label">Typ av uppdrag</div>
<div class="type-grid">
<div class="type-btn active" data-type="photography" onclick="selectType(this)">
<div class="type-icon">📷</div>
<div class="type-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg></div>
<div class="type-label">Foto</div>
</div>
<div class="type-btn" data-type="video" onclick="selectType(this)">
<div class="type-icon">🎬</div>
<div class="type-icon"></div>
<div class="type-label">Video</div>
</div>
<div class="type-btn" data-type="drone" onclick="selectType(this)">
<div class="type-icon">🚁</div>
<div class="type-icon"><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 class="type-label">Drone</div>
</div>
<div class="type-btn" data-type="livestream" onclick="selectType(this)">
<div class="type-icon">📡</div>
<div class="type-icon"></div>
<div class="type-label">Livestream</div>
</div>
<div class="type-btn" data-type="360" onclick="selectType(this)">
<div class="type-icon">🔄</div>
<div class="type-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<div class="type-label">360°</div>
</div>
<div class="type-btn" data-type="documentary" onclick="selectType(this)">
<div class="type-icon">🎥</div>
<div class="type-icon"></div>
<div class="type-label">Dokumentär</div>
</div>
</div>
@@ -233,7 +233,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Date & time -->
<div class="section">
<div class="section-title">📅 Datum & Tid</div>
<div class="section-title"> Datum & Tid</div>
<div class="field">
<div class="field-label">Startdatum och tid</div>
<input class="field-input" id="date-time" type="datetime-local"/>
@@ -242,7 +242,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Cameras -->
<div class="section">
<div class="section-title">📸 Antal Zoomers</div>
<div class="section-title"> Antal Zoomers</div>
<div class="field">
<div class="field-label">Önskat antal kameror / Zoomers</div>
<div class="camera-selector">
@@ -256,7 +256,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Budget -->
<div class="section">
<div class="section-title">💰 Budget</div>
<div class="section-title"> Budget</div>
<div class="field">
<div class="field-label">Budget (SEK)</div>
<div class="slider-wrap">
@@ -270,7 +270,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Contact -->
<div class="section">
<div class="section-title">👤 Dina uppgifter</div>
<div class="section-title"> Dina uppgifter</div>
<div class="field">
<div class="field-label">Namn</div>
<input class="field-input" id="contact-name" type="text" placeholder="För- och efternamn"/>
@@ -288,7 +288,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Submit -->
<div class="submit-section">
<button class="submit-btn" id="submit-btn" onclick="submitMission()">
📤 Skicka beställning
Skicka beställning
</button>
<div style="text-align:center;font-size:12px;color:var(--text2);margin-top:10px;">
Du bekräftar att uppdraget följer quiXzooms riktlinjer. Vi återkommer inom 30 minuter.
@@ -299,19 +299,19 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Confirmation screen -->
<div class="confirm-screen" id="confirm-screen">
<div class="confirm-icon">🎉</div>
<div class="confirm-icon"></div>
<div class="confirm-title">Uppdrag beställt!</div>
<div class="confirm-sub">Vi har tagit emot din beställning och matchar dig med rätt Zoomer. Du får en bekräftelse via e-post inom kort.</div>
<div class="mission-id-box">
<div class="mission-id-label">Uppdragets ID</div>
<div class="mission-id-val" id="confirm-mission-id"></div>
</div>
<a class="track-btn" id="track-link" href="#">🗺️ Följ uppdraget live</a>
<a class="track-btn" id="track-link" href="#"> Följ uppdraget live</a>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ── Map ───────────────────────────────────────────────────────────────────
// Map
const map = L.map('location-map', { center: [7.9519, 98.2849], zoom: 12 });
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap',
@@ -339,21 +339,21 @@ function updateAddress(addr) {
document.getElementById('chosen-location').textContent = addr || 'Klicka på kartan för att välja plats';
}
// ── Type selector ─────────────────────────────────────────────────────────
// Type selector
function selectType(el) {
document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
el.classList.add('active');
document.getElementById('mission-type').value = el.dataset.type;
}
// ── Camera count ──────────────────────────────────────────────────────────
// Camera count
let cameraCount = 1;
function changeCount(delta) {
cameraCount = Math.max(1, Math.min(10, cameraCount + delta));
document.getElementById('camera-count').textContent = cameraCount;
}
// ── Budget slider ─────────────────────────────────────────────────────────
// Budget slider
function updateBudget(val) {
document.getElementById('budget').value = val;
document.getElementById('budget-display').textContent = parseInt(val).toLocaleString('sv-SE') + ' kr';
@@ -362,13 +362,13 @@ function updateBudget(val) {
}
updateBudget(5000);
// ── Default date/time (tomorrow 10:00) ────────────────────────────────────
// Default date/time (tomorrow 10:00)
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(10, 0, 0, 0);
document.getElementById('date-time').value = tomorrow.toISOString().slice(0, 16);
// ── Submit ────────────────────────────────────────────────────────────────
// Submit
async function submitMission() {
const title = document.getElementById('title').value.trim();
const contactName = document.getElementById('contact-name').value.trim();
@@ -410,12 +410,12 @@ async function submitMission() {
document.getElementById('track-link').href = `/quixzoom/mission-map.html?mission=${d.mission.id}`;
} else {
btn.disabled = false;
btn.textContent = '📤 Skicka beställning';
btn.textContent = ' Skicka beställning';
alert(d.error || 'Något gick fel. Försök igen.');
}
} catch(e) {
btn.disabled = false;
btn.textContent = '📤 Skicka beställning';
btn.textContent = ' Skicka beställning';
alert('Nätverksfel. Kontrollera din anslutning.');
}
}
+10 -13
View File
@@ -48,7 +48,7 @@
.nav-links{display:flex;align-items:center;gap:32px;flex:1;justify-content:center;list-style:none}
.nav-links a{color:rgba(255,255,255,.75);font-size:var(--text-base);font-weight:500;transition:color .15s}
.nav-links a:hover{color:#fff}
.btn-nav{background:var(--blue);color:#fff;border:none;border-radius:var(--radius-lg);padding:10px 24px;font-size:var(--text-base);font-weight:700;transition:background .15s;white-space:nowrap;flex-shrink:0;text-decoration:none;display:inline-block}
.btn-nav{background:var(--blue);color:#fff;border:none;border-radius: var(--radius-lg);padding:10px 24px;font-size:var(--text-base);font-weight:700;transition:background .15s;white-space:nowrap;flex-shrink:0;text-decoration:none;display:inline-block}
.btn-nav:hover{background:var(--blue-dark)}
@media(max-width:768px){.nav-links{display:none}.btn-nav{display:none}}
@@ -75,7 +75,7 @@
.resource-row{
display:flex;align-items:center;justify-content:space-between;gap:24px;
background:#fff;border:1.5px solid #EAEAEA;border-radius:var(--radius-md);
background:#fff;border:1.5px solid #EAEAEA;border-radius: var(--radius-md);
padding:20px 24px;
transition:border-color .2s,box-shadow .2s,transform .2s;
}
@@ -92,7 +92,7 @@
.resource-tag{
flex-shrink:0;
font-size:.7rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase;
background:var(--blue-pale);color:var(--blue);border-radius:6px;padding:4px 10px;
background:var(--blue-pale);color:var(--blue);border-radius: var(--radius-sm);padding:4px 10px;
}
.resource-arrow{
@@ -108,16 +108,16 @@
.footer-brand-col .footer-logo .x{color:var(--qx-blue,#0066FF)}
.footer-brand-col .footer-desc{font-size:.8rem;color:rgba(255,255,255,.75);line-height:1.6;margin-bottom:20px}
.footer-social-row{display:flex;gap:10px}
.footer-soc-btn{width:36px;height:36px;border-radius:8px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.7);text-decoration:none;transition:background .15s,color .15s}
.footer-soc-btn{width:36px;height:36px;border-radius: var(--radius-md);background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.7);text-decoration:none;transition:background .15s,color .15s}
.footer-soc-btn:hover{background:rgba(255,255,255,.16);color:#fff}
.footer-col-title{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.75);margin-bottom:16px}
.footer-col-links{list-style:none;padding:0;margin:0}
.footer-col-links li{margin-bottom:10px}
.footer-col-links a{font-size:.875rem;color:rgba(255,255,255,.65);text-decoration:none;transition:color .15s}
.footer-col-links a:hover{color:#fff}
.badge-new{font-size:.65rem;font-weight:700;background:var(--qx-blue,#0066FF);color:#fff;padding:1px 5px;border-radius:4px;margin-left:5px;vertical-align:middle}
.badge-new{font-size:.65rem;font-weight:700;background:var(--qx-blue,#0066FF);color:#fff;padding:1px 5px;border-radius: var(--radius-sm);margin-left:5px;vertical-align:middle}
.footer-trust{display:flex;align-items:center;gap:16px;margin-top:20px;flex-wrap:wrap}
.trust-badge{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:8px 12px;font-size:.72rem;font-weight:700;color:rgba(255,255,255,.7);display:flex;align-items:center;gap:6px;letter-spacing:.02em}
.trust-badge{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius: var(--radius-md);padding:8px 12px;font-size:.72rem;font-weight:700;color:rgba(255,255,255,.7);display:flex;align-items:center;gap:6px;letter-spacing:.02em}
.footer-bottom{max-width:1200px;margin:0 auto;padding:20px 32px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px}
.footer-copy{font-size:.8rem;color:rgba(255,255,255,.75)}
.footer-legal-links{display:flex;gap:20px}
@@ -125,7 +125,7 @@
.footer-legal-links a:hover{color:rgba(255,255,255,.8)}
.footer-lang-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.footer-lang-row .fl-lbl{font-size:.72rem;color:rgba(255,255,255,.75)}
.flang{font-size:.78rem;color:rgba(255,255,255,.75);text-decoration:none;padding:3px 6px;border-radius:4px}
.flang{font-size:.78rem;color:rgba(255,255,255,.75);text-decoration:none;padding:3px 6px;border-radius: var(--radius-sm)}
.flang:hover,.flang.active{background:rgba(255,255,255,.1);color:#fff}
@media(max-width:1024px){.footer-grid{grid-template-columns:1fr 1fr 1fr}.footer-brand-col{grid-column:1/-1}}
@@ -152,7 +152,6 @@
<ul class="nav-links">
<li><a href="/#how">How it works</a></li>
<li><a href="/for-organisations/">For organisations</a></li>
<li><a href="/developer">Developers</a></li>
<li><a href="/resources/">Resources</a></li>
<li><a href="/about">About</a></li>
</ul>
@@ -201,9 +200,9 @@
<div class="group-label">Developer</div>
<div class="resource-list">
<a href="/developer/" class="resource-row">
<a href="/developer/openapi.json" class="resource-row">
<div class="resource-info">
<h3>API documentation</h3>
<h3>API documentation (OpenAPI spec)</h3>
<p>Full reference for the quiXzoom REST API. Authentication, endpoints, webhooks and rate limits.</p>
</div>
<span class="resource-tag">Docs</span>
@@ -301,8 +300,6 @@
<div>
<div class="footer-col-title">Developers</div>
<ul class="footer-col-links">
<li><a href="/developer">API overview</a></li>
<li><a href="/developer/#quickstart">Quick start</a></li>
<li><a href="/developer/openapi.json">OpenAPI spec</a></li>
<li><a href="/changelog">Changelog</a></li>
<li><a href="/status">System status</a></li>
@@ -327,7 +324,7 @@
<ul class="footer-col-links">
<li><a href="privacy/index.html">Privacy policy</a></li>
<li><a href="terms/index.html">Terms of service</a></li>
<li><a href="/terms#dac7">DAC7 reporting</a></li>
<li><a href="/terms#DAC7B2>DAC7 reporting</a></li>
<li><a href="/privacy#gdpr">GDPR rights</a></li>
<li><a href="/security#responsible">Responsible disclosure</a></li>
</ul>
+77 -77
View File
@@ -23,7 +23,7 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:-apple-
.sidebar-logo .meta{font-size:11px;color:rgba(255,255,255,.75);margin-top:3px}
.sidebar-section{padding:16px 12px 6px}
.sidebar-section-label{font-size:10px;font-weight:700;letter-spacing:.1em;color:rgba(255,255,255,.75);text-transform:uppercase;padding:0 8px;margin-bottom:6px}
.sidebar-item{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:8px;cursor:pointer;color:rgba(255,255,255,.6);font-size:13px;font-weight:500;transition:all .15s;border:none;background:none;width:100%;text-align:left}
.sidebar-item{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius: var(--radius-md);cursor:pointer;color:rgba(255,255,255,.6);font-size:13px;font-weight:500;transition:all .15s;border:none;background:none;width:100%;text-align:left}
.sidebar-item:hover{background:rgba(255,255,255,.08);color:#fff}
.sidebar-item.active{background:var(--qx);color:#fff}
.sidebar-item .icon{font-size:15px;width:22px;text-align:center;flex-shrink:0}
@@ -35,27 +35,27 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:-apple-
.topbar-breadcrumb{font-size:12px;color:var(--text3)}
.topbar-actions{display:flex;gap:8px;align-items:center}
.content{padding:24px 28px;flex:1}
.tabs{display:flex;gap:4px;background:var(--surface);border-radius:10px;padding:4px;border:1px solid var(--border);width:fit-content;margin-bottom:20px}
.tab{padding:7px 18px;border-radius:7px;font-size:13px;font-weight:600;cursor:pointer;color:var(--text2);border:none;background:none;transition:all .15s}
.tabs{display:flex;gap:4px;background:var(--surface);border-radius: var(--radius-md);padding:4px;border:1px solid var(--border);width:fit-content;margin-bottom:20px}
.tab{padding:7px 18px;border-radius: var(--radius-sm);font-size:13px;font-weight:600;cursor:pointer;color:var(--text2);border:none;background:none;transition:all .15s}
.tab.active{background:var(--card);color:var(--text);box-shadow:0 1px 4px rgba(0,0,0,.08)}
.view{display:none}.view.active{display:block}
.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:none;transition:all .15s}
.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius: var(--radius-md);font-size:13px;font-weight:600;cursor:pointer;border:none;transition:all .15s}
.btn-primary{background:var(--qx);color:#fff}.btn-primary:hover{background:var(--accent2)}
.btn-secondary{background:var(--surface);color:var(--text);border:1px solid var(--border)}.btn-secondary:hover{background:var(--border)}
.btn-sm{padding:5px 12px;font-size:12px}
.btn-xs{padding:3px 8px;font-size:11px}
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:14px;margin-bottom:24px}
.stat-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px 20px;position:relative;overflow:hidden}
.stat-card::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:var(--qx);border-radius:3px 3px 0 0}
.stat-card{background:var(--card);border:1px solid var(--border);border-radius: var(--radius-md);padding:18px 20px;position:relative;overflow:hidden}
.stat-card::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:var(--qx);border-radius: var(--radius-sm) 3px 0 0}
.stat-card.g::after{background:var(--green)}.stat-card.r::after{background:var(--red)}.stat-card.b::after{background:var(--blue)}.stat-card.p::after{background:var(--purple)}.stat-card.y::after{background:var(--yellow)}
.stat-card .label{font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}
.stat-card .value{font-size:24px;font-weight:800;letter-spacing:-.02em;line-height:1}
.stat-card .sub{font-size:11px;color:var(--text3);margin-top:5px}
.stat-card .change{font-size:11px;font-weight:700;padding:2px 7px;border-radius:20px;display:inline-block;margin-top:5px}
.stat-card .change{font-size:11px;font-weight:700;padding:2px 7px;border-radius: var(--radius-xl);display:inline-block;margin-top:5px}
.change-up{background:#dcfce7;color:#166534}.change-flat{background:#f1f5f9;color:#475569}
.badge{display:inline-flex;align-items:center;gap:4px;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:700;white-space:nowrap}
.badge{display:inline-flex;align-items:center;gap:4px;padding:2px 9px;border-radius: var(--radius-xl);font-size:11px;font-weight:700;white-space:nowrap}
.badge-green{background:#dcfce7;color:#166534}.badge-red{background:#fee2e2;color:#991b1b}.badge-yellow{background:#fef9c3;color:#854d0e}.badge-blue{background:#dbeafe;color:#1e40af}.badge-gray{background:#f1f5f9;color:#475569}.badge-orange{background:#ffedd5;color:#9a3412}.badge-purple{background:#ede9fe;color:#4c1d95}
.table-wrap{background:var(--card);border:1px solid var(--border);border-radius:12px;overflow:hidden;margin-bottom:20px}
.table-wrap{background:var(--card);border:1px solid var(--border);border-radius: var(--radius-md);overflow:hidden;margin-bottom:20px}
.table-header{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border);gap:10px;flex-wrap:wrap}
.table-header h3{font-size:14px;font-weight:700}
.table-header-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
@@ -70,40 +70,40 @@ td.mono{font-variant-numeric:tabular-nums;font-family:'SF Mono',monospace;font-s
.tbl-scroll{overflow-x:auto}
.section-title{font-size:13px;font-weight:700;color:var(--text2);margin-bottom:14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
/* CHARTS */
.chart-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:20px}
.chart-card{background:var(--card);border:1px solid var(--border);border-radius: var(--radius-md);padding:20px;margin-bottom:20px}
.chart-card h3{font-size:14px;font-weight:700;margin-bottom:16px}
.bar-chart{display:flex;align-items:flex-end;gap:6px;height:120px;padding-top:8px}
.bar-wrap{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px}
.bar{width:100%;border-radius:4px 4px 0 0;background:var(--qx);opacity:.8;transition:opacity .15s;cursor:pointer;position:relative}
.bar{width:100%;border-radius: var(--radius-sm) 4px 0 0;background:var(--qx);opacity:.8;transition:opacity .15s;cursor:pointer;position:relative}
.bar:hover{opacity:1}
.bar-label{font-size:10px;color:var(--text3);white-space:nowrap}
.bar-value{font-size:10px;font-weight:700;color:var(--text2)}
/* ZOOMER CARDS */
.zoomer-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;margin-bottom:20px}
.zoomer-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px;transition:box-shadow .15s}
.zoomer-card{background:var(--card);border:1px solid var(--border);border-radius: var(--radius-md);padding:18px;transition:box-shadow .15s}
.zoomer-card:hover{box-shadow:0 4px 16px rgba(0,0,0,.07)}
.zoomer-card .zc-header{display:flex;align-items:center;gap:12px;margin-bottom:12px}
.zc-avatar{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--qx),var(--purple));display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:15px;flex-shrink:0}
.zc-avatar{width:40px;height:40px;border-radius: var(--radius-full);background:linear-gradient(135deg,var(--qx),var(--purple));display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:15px;flex-shrink:0}
.zc-name{font-size:14px;font-weight:700}
.zc-role{font-size:11px;color:var(--text3)}
.zc-stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}
.zc-stat{text-align:center;background:var(--surface);border-radius:8px;padding:8px 4px}
.zc-stat{text-align:center;background:var(--surface);border-radius: var(--radius-md);padding:8px 4px}
.zc-stat .cs-val{font-size:15px;font-weight:800;color:var(--text)}
.zc-stat .cs-lbl{font-size:9px;font-weight:700;text-transform:uppercase;color:var(--text3);letter-spacing:.06em}
/* GEO */
.geo-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}
.geo-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px}
.geo-card{background:var(--card);border:1px solid var(--border);border-radius: var(--radius-md);padding:16px}
.geo-card .gc-name{font-size:13px;font-weight:700;margin-bottom:6px}
.geo-bar-wrap{height:6px;background:var(--surface);border-radius:3px;margin-bottom:6px;overflow:hidden}
.geo-bar{height:100%;background:var(--qx);border-radius:3px;transition:width .5s}
.geo-bar-wrap{height:6px;background:var(--surface);border-radius: var(--radius-sm);margin-bottom:6px;overflow:hidden}
.geo-bar{height:100%;background:var(--qx);border-radius: var(--radius-sm);transition:width .5s}
.geo-meta{font-size:11px;color:var(--text2)}
/* DAC7 */
.dac7-panel{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:20px}
.dac7-donut{width:120px;height:120px;border-radius:50%;position:relative;margin:0 auto 12px}
.compliance-meter{height:12px;background:var(--surface);border-radius:6px;overflow:hidden;margin-bottom:8px}
.compliance-fill{height:100%;border-radius:6px;background:var(--green);transition:width .5s}
.dac7-panel{background:var(--card);border:1px solid var(--border);border-radius: var(--radius-md);padding:20px;margin-bottom:20px}
.dac7-donut{width:120px;height:120px;border-radius: var(--radius-full);position:relative;margin:0 auto 12px}
.compliance-meter{height:12px;background:var(--surface);border-radius: var(--radius-sm);overflow:hidden;margin-bottom:8px}
.compliance-fill{height:100%;border-radius: var(--radius-sm);background:var(--green);transition:width .5s}
/* PAYOUT */
.payout-card{background:linear-gradient(135deg,#1A1A2E,#16213E);color:#fff;border-radius:14px;padding:24px;margin-bottom:20px}
.payout-card{background:linear-gradient(135deg,#1A1A2E,#16213E);color:#fff;border-radius: var(--radius-lg);padding:24px;margin-bottom:20px}
.payout-card .pc-title{font-size:12px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:rgba(255,255,255,.75);margin-bottom:8px}
.payout-card .pc-amount{font-size:36px;font-weight:900;letter-spacing:-.02em;margin-bottom:4px}
.payout-card .pc-sub{font-size:13px;color:rgba(255,255,255,.6)}
@@ -147,17 +147,17 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">quiXzoom</div>
<button class="sidebar-item" onclick="location.href='launch.html'"><span class="icon">🚀</span>Launch Center</button>
<button class="sidebar-item active"><span class="icon">💰</span>Revenue Dashboard</button>
<button class="sidebar-item" onclick="location.href='admin.html'"><span class="icon">⚙️</span>Admin</button>
<button class="sidebar-item" onclick="location.href='dac7-reporting.html'"><span class="icon">🏛️</span>DAC7 Reporting</button>
<button class="sidebar-item" onclick="location.href='zoomer-earnings.html'"><span class="icon">📊</span>Zoomer Earnings</button>
<button class="sidebar-item" onclick="location.href='launch.html'"><span class="icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg></span>Launch Center</button>
<button class="sidebar-item active"><span class="icon"></span>Revenue Dashboard</button>
<button class="sidebar-item" onclick="location.href='admin.html'"><span class="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></span>Admin</button>
<button class="sidebar-item" onclick="location.href='dac7-reporting.html'"><span class="icon"></span>DAC7 Reporting</button>
<button class="sidebar-item" onclick="location.href='zoomer-earnings.html'"><span class="icon"></span>Zoomer Earnings</button>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Utbetalningar</div>
<button class="sidebar-item"><span class="icon"></span>Godkända</button>
<button class="sidebar-item"><span class="icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>Godkända</button>
<button class="sidebar-item"><span class="icon"></span>Väntande</button>
<button class="sidebar-item"><span class="icon"></span>Misslyckade</button>
<button class="sidebar-item"><span class="icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg></span>Misslyckade</button>
</div>
<div class="sidebar-footer">
<button class="sidebar-item" onclick="location.href='../ouroboros/index.html'"><span class="icon"></span>Ouroboros</button>
@@ -170,7 +170,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="topbar-left">
<span class="topbar-breadcrumb">quiXzoom</span>
<span style="color:var(--text3)"></span>
<span class="topbar-title">💰 Revenue Dashboard</span>
<span class="topbar-title"> Revenue Dashboard</span>
</div>
<div class="topbar-actions">
<select class="btn btn-secondary btn-sm" id="periodFilter" onchange="renderAll()" style="border:1px solid var(--border)">
@@ -179,7 +179,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<option value="apr2026">April 2026</option>
<option value="q2">Q2 2026</option>
</select>
<button class="btn btn-primary btn-sm" onclick="exportCSV()">📥 Exportera CSV</button>
<button class="btn btn-primary btn-sm" onclick="exportCSV()"> Exportera CSV</button>
</div>
</div>
@@ -221,11 +221,11 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- TABS -->
<div class="tabs">
<button class="tab active" onclick="switchTab('overview',this)">📊 Översikt</button>
<button class="tab" onclick="switchTab('zoomers',this)">👤 Per zoomer</button>
<button class="tab" onclick="switchTab('payouts',this)">💸 Utbetalningar</button>
<button class="tab" onclick="switchTab('geo',this)">🌍 Geografi</button>
<button class="tab" onclick="switchTab('dac7',this)">🏛️ DAC7</button>
<button class="tab active" onclick="switchTab('overview',this)"> Översikt</button>
<button class="tab" onclick="switchTab('zoomers',this)"> Per zoomer</button>
<button class="tab" onclick="switchTab('payouts',this)"> Utbetalningar</button>
<button class="tab" onclick="switchTab('geo',this)"> Geografi</button>
<button class="tab" onclick="switchTab('dac7',this)"> DAC7</button>
</div>
<!-- OVERVIEW -->
@@ -234,24 +234,24 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="payout-card">
<div class="pc-title">Nästa utbetalningsdatum</div>
<div class="pc-amount" id="pcAmount">132 450 kr</div>
<div class="pc-sub">📅 <span id="pcDate">1 augusti 2026</span><span id="pcCount">23 zoomers</span> väntar på utbetalning</div>
<div class="pc-sub"> <span id="pcDate">1 augusti 2026</span><span id="pcCount">23 zoomers</span> väntar på utbetalning</div>
<div style="margin-top:16px;display:flex;gap:8px">
<button class="btn btn-sm" style="background:var(--qx);color:#fff">🚀 Initiera utbetalning</button>
<button class="btn btn-sm" style="background:rgba(255,255,255,.15);color:#fff;border:1px solid rgba(255,255,255,.2)">📋 Granska lista</button>
<button class="btn btn-sm" style="background:var(--qx);color:#fff"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> Initiera utbetalning</button>
<button class="btn btn-sm" style="background:rgba(255,255,255,.15);color:#fff;border:1px solid rgba(255,255,255,.2)"><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> Granska lista</button>
</div>
</div>
<!-- REVENUE CHART -->
<div class="chart-card">
<h3>📈 Intjänat per månad (2026)</h3>
<h3><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> Intjänat per månad (2026)</h3>
<div class="bar-chart" id="revenueChart"></div>
</div>
<!-- STRIPE CONNECT SUMMARY -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-bottom:20px">
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px">
<div style="background:var(--card);border:1px solid var(--border);border-radius: var(--radius-md);padding:18px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<div style="width:32px;height:32px;background:#635BFF;border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px">$</div>
<div style="width:32px;height:32px;background:#635BFF;border-radius: var(--radius-md);display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px">$</div>
<div>
<div style="font-size:12px;font-weight:700">Stripe Connect</div>
<div style="font-size:11px;color:var(--text3)">Betalningsplattform</div>
@@ -259,27 +259,27 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div style="font-size:11px;color:var(--text2)">Anslutna konton: <strong id="stripeConnected">0</strong></div>
<div style="font-size:11px;color:var(--text2)">Under granskning: <strong id="stripeReview">0</strong></div>
<div style="margin-top:10px"><span class="badge badge-green"> Live</span></div>
<div style="margin-top:10px"><span class="badge badge-green"> Live</span></div>
</div>
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px">
<div style="font-size:12px;font-weight:700;margin-bottom:12px">📊 Utbetalningsstatus</div>
<div style="background:var(--card);border:1px solid var(--border);border-radius: var(--radius-md);padding:18px">
<div style="font-size:12px;font-weight:700;margin-bottom:12px"> Utbetalningsstatus</div>
<div id="payoutStatusChart"></div>
</div>
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px">
<div style="font-size:12px;font-weight:700;margin-bottom:12px"> Genomsnittlig rating</div>
<div style="background:var(--card);border:1px solid var(--border);border-radius: var(--radius-md);padding:18px">
<div style="font-size:12px;font-weight:700;margin-bottom:12px"> Genomsnittlig rating</div>
<div style="font-size:36px;font-weight:900;letter-spacing:-.02em;color:var(--yellow)" id="avgRating">4.7</div>
<div style="font-size:11px;color:var(--text2)">Baserat på <span id="ratingCount">0</span> omdömen</div>
<div style="margin-top:8px;font-size:18px">⭐⭐⭐⭐⭐</div>
<div style="margin-top:8px;font-size:18px"></div>
</div>
</div>
</div>
<!-- PER ZOOMER -->
<div class="view" id="view-zoomers">
<div class="table-header" style="background:var(--card);border:1px solid var(--border);border-radius:12px 12px 0 0;padding:14px 20px;margin-bottom:0">
<h3>👤 Per zoomer</h3>
<div class="table-header" style="background:var(--card);border:1px solid var(--border);border-radius: var(--radius-md) 12px 0 0;padding:14px 20px;margin-bottom:0">
<h3> Per zoomer</h3>
<div class="table-header-actions">
<input type="text" class="btn btn-secondary btn-sm" placeholder="🔍 Sök zoomer..." id="zoomerSearch" oninput="filterZoomers()" style="min-width:200px;text-align:left">
<input type="text" class="btn btn-secondary btn-sm" placeholder="<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> Sök zoomer..." id="zoomerSearch" oninput="filterZoomers()" style="min-width:200px;text-align:left">
<select class="btn btn-secondary btn-sm" id="zoomerStatusFilter" onchange="filterZoomers()" style="border:1px solid var(--border)">
<option value="">Alla</option>
<option value="active">Aktiva</option>
@@ -287,7 +287,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</select>
</div>
</div>
<div class="tbl-scroll" style="background:var(--card);border:1px solid var(--border);border-top:none;border-radius:0 0 12px 12px;margin-bottom:20px">
<div class="tbl-scroll" style="background:var(--card);border:1px solid var(--border);border-top:none;border-radius: 0 0 12px 12px;margin-bottom:20px">
<table>
<thead>
<tr><th>Zoomer</th><th>Land</th><th class="right">Uppdrag</th><th class="right">Intjänat</th><th class="right">Rating</th><th>Status</th><th>Utbetalning</th><th>Åtgärd</th></tr>
@@ -301,7 +301,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="view" id="view-payouts">
<div class="table-wrap">
<div class="table-header">
<h3>💸 Utbetalningshistorik</h3>
<h3> Utbetalningshistorik</h3>
<div class="table-header-actions">
<select class="btn btn-secondary btn-sm" id="payoutStatusFilter2" onchange="filterPayouts()" style="border:1px solid var(--border)">
<option value="">Alla statusar</option>
@@ -322,7 +322,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- GEO -->
<div class="view" id="view-geo">
<div class="section-title">🌍 Geografisk fördelning</div>
<div class="section-title"> Geografisk fördelning</div>
<div class="geo-grid" id="geoGrid"></div>
</div>
@@ -331,10 +331,10 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="dac7-panel">
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:20px">
<div>
<h3 style="font-size:15px;font-weight:700;margin-bottom:4px">🏛️ DAC7 Compliance</h3>
<h3 style="font-size:15px;font-weight:700;margin-bottom:4px"> DAC7 Compliance</h3>
<p style="font-size:12px;color:var(--text2)">EU-direktiv 2021/514 — rapportering av plattformsekonomin</p>
</div>
<button class="btn btn-primary btn-sm">📥 Exportera DAC7-rapport</button>
<button class="btn btn-primary btn-sm"> Exportera DAC7-rapport</button>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:20px">
@@ -350,8 +350,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:10px;padding:14px;font-size:12px;color:#854d0e">
<strong>⚠️ DAC7 deadline:</strong> Rapportering för kalenderår 2026 ska lämnas till Skatteverket senast <strong>31 januari 2027</strong>. Zoomers med intäkter &gt; 2 000 EUR eller &gt; 25 transaktioner måste ha verifierat TIN/PNR.
<div style="background:#fffbeb;border:1px solid #fde68a;border-radius: var(--radius-md);padding:14px;font-size:12px;color:#854d0e">
<strong><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> DAC7 deadline:</strong> Rapportering för kalenderår 2026 ska lämnas till Skatteverket senast <strong>31 januari 2027</strong>. Zoomers med intäkter &gt; 2 000 EUR eller &gt; 25 transaktioner måste ha verifierat TIN/PNR.
</div>
</div>
</div>
@@ -361,7 +361,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<script>
// ── DATA ─────────────────────────────────────────────────────────────────
// DATA
const zoomers = [];
const payoutHistory = [];
@@ -383,15 +383,15 @@ const monthlyRevenue = [
];
const geoData = [
{ country:'Sverige', flag:'🇸🇪', zoomers:5, earned:142000, pct:52 },
{ country:'Norge', flag:'🇳🇴', zoomers:2, earned:52300, pct:19 },
{ country:'Danmark', flag:'🇩🇰', zoomers:2, earned:41100, pct:15 },
{ country:'Tyskland',flag:'🇩🇪', zoomers:3, earned:66600, pct:24 },
{ country:'Nederländerna',flag:'🇳🇱', zoomers:2, earned:31200, pct:11 },
{ country:'Portugal',flag:'🇵🇹', zoomers:1, earned:8500, pct:3 },
{ country:'Sverige', flag:'<span class="flag-se">SE</span>', zoomers:5, earned:142000, pct:52 },
{ country:'Norge', flag:'', zoomers:2, earned:52300, pct:19 },
{ country:'Danmark', flag:'', zoomers:2, earned:41100, pct:15 },
{ country:'Tyskland',flag:'', zoomers:3, earned:66600, pct:24 },
{ country:'Nederländerna',flag:'', zoomers:2, earned:31200, pct:11 },
{ country:'Portugal',flag:'', zoomers:1, earned:8500, pct:3 },
];
// ── COMPUTED ──────────────────────────────────────────────────────────────────
// COMPUTED
const totalEarned = zoomers.reduce((s,z) => s+z.earned, 0);
const totalAssignments = zoomers.reduce((s,z) => s+z.assignments, 0);
const activeCount = zoomers.filter(z => z.status==='active').length;
@@ -402,7 +402,7 @@ const dac7Pct = Math.round(tinVerified/zoomers.length*100);
const avgRating = (zoomers.reduce((s,z) => s+z.rating, 0)/zoomers.length).toFixed(1);
const totalRatingCount = zoomers.reduce((s,z) => s+z.assignments, 0);
// ── RENDER ALL ────────────────────────────────────────────────────────────────
// RENDER ALL
function renderAll() {
// Stats
document.getElementById('statTotal').textContent = fmtK(totalEarned)+' kr';
@@ -444,9 +444,9 @@ function renderPayoutStatusChart() {
const failed = payoutHistory.filter(p=>p.status==='failed').length;
document.getElementById('payoutStatusChart').innerHTML = `
<div style="display:flex;flex-direction:column;gap:8px">
<div style="display:flex;justify-content:space-between;align-items:center"><span class="badge badge-green"> Sent</span><strong>${sent}</strong></div>
<div style="display:flex;justify-content:space-between;align-items:center"><span class="badge badge-green"><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> Sent</span><strong>${sent}</strong></div>
<div style="display:flex;justify-content:space-between;align-items:center"><span class="badge badge-yellow">⏳ Pending</span><strong>${pending}</strong></div>
<div style="display:flex;justify-content:space-between;align-items:center"><span class="badge badge-red"> Failed</span><strong>${failed}</strong></div>
<div style="display:flex;justify-content:space-between;align-items:center"><span class="badge badge-red"> Failed</span><strong>${failed}</strong></div>
</div>`;
}
@@ -455,7 +455,7 @@ function renderZoomerTable(data) {
<tr>
<td>
<div style="display:flex;align-items:center;gap:10px">
<div style="width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,var(--qx),var(--purple));display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:12px;flex-shrink:0">${z.name.split(' ').map(n=>n[0]).join('')}</div>
<div style="width:32px;height:32px;border-radius: var(--radius-full);background:linear-gradient(135deg,var(--qx),var(--purple));display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:12px;flex-shrink:0">${z.name.split(' ').map(n=>n[0]).join('')}</div>
<div>
<div style="font-weight:700;font-size:13px">${z.name}</div>
<div style="font-size:11px;color:var(--text3)">${z.email}</div>
@@ -465,8 +465,8 @@ function renderZoomerTable(data) {
<td>${z.flag} ${z.country}</td>
<td class="right">${z.assignments}</td>
<td class="right mono"><strong>${fmt(z.earned)} kr</strong></td>
<td class="right"> ${z.rating}</td>
<td><span class="badge ${z.status==='active'?'badge-green':'badge-gray'}">${z.status==='active'?' Aktiv':' Inaktiv'}</span></td>
<td class="right"> ${z.rating}</td>
<td><span class="badge ${z.status==='active'?'badge-green':'badge-gray'}">${z.status==='active'?' Aktiv':' Inaktiv'}</span></td>
<td><span class="badge ${payoutBadge(z.payout)}">${payoutLabel(z.payout)}</span></td>
<td><button class="btn btn-xs btn-secondary">Detaljer</button></td>
</tr>`).join('');
@@ -517,16 +517,16 @@ function renderDAC7() {
document.getElementById('dac7Total').textContent = zoomers.length;
document.getElementById('dac7StatusList').innerHTML = `
<div style="display:flex;flex-direction:column;gap:8px">
<div style="display:flex;justify-content:space-between"><span style="font-size:12px;color:var(--text2)"> TIN verifierat</span><strong style="color:var(--green)">${tinVerified}</strong></div>
<div style="display:flex;justify-content:space-between"><span style="font-size:12px;color:var(--text2)"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> TIN verifierat</span><strong style="color:var(--green)">${tinVerified}</strong></div>
<div style="display:flex;justify-content:space-between"><span style="font-size:12px;color:var(--text2)">⏳ Saknar TIN</span><strong style="color:var(--red)">${zoomers.length-tinVerified}</strong></div>
<div style="display:flex;justify-content:space-between"><span style="font-size:12px;color:var(--text2)">📋 Rapporteringspliktiga</span><strong>${zoomers.filter(z=>z.earned>25000||z.assignments>25).length}</strong></div>
<div style="display:flex;justify-content:space-between"><span style="font-size:12px;color:var(--text2)">🏳️ Under tröskel</span><strong style="color:var(--text3)">${zoomers.filter(z=>z.earned<=25000&&z.assignments<=25).length}</strong></div>
<div style="display:flex;justify-content:space-between"><span style="font-size:12px;color:var(--text2)"><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> Rapporteringspliktiga</span><strong>${zoomers.filter(z=>z.earned>25000||z.assignments>25).length}</strong></div>
<div style="display:flex;justify-content:space-between"><span style="font-size:12px;color:var(--text2)"> Under tröskel</span><strong style="color:var(--text3)">${zoomers.filter(z=>z.earned<=25000&&z.assignments<=25).length}</strong></div>
</div>`;
}
// ── UTILS ─────────────────────────────────────────────────────────────────────
// UTILS
function payoutBadge(s) { return {sent:'badge-green',pending:'badge-yellow',failed:'badge-red'}[s]||'badge-gray'; }
function payoutLabel(s) { return {sent:' Sent',pending:'⏳ Pending',failed:' Failed'}[s]||s; }
function payoutLabel(s) { return {sent:'<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> Sent',pending:'⏳ Pending',failed:' Failed'}[s]||s; }
function fmt(n) { return Math.round(n).toLocaleString('sv-SE'); }
function fmtK(n) { return n>=1000000?(n/1000000).toFixed(1)+'M':n>=1000?Math.round(n/1000)+'k':Math.round(n).toString(); }
+10 -10
View File
@@ -39,7 +39,7 @@ body { font-family: var(--font-text); background: var(--background-secondary); c
position: absolute;
top: -60px; right: -60px;
width: 300px; height: 300px;
border-radius: 50%;
border-radius: var(--radius-full);
background: rgba(255,255,255,0.08);
}
.hero::after {
@@ -47,14 +47,14 @@ body { font-family: var(--font-text); background: var(--background-secondary); c
position: absolute;
bottom: -80px; left: -40px;
width: 250px; height: 250px;
border-radius: 50%;
border-radius: var(--radius-full);
background: rgba(255,255,255,0.06);
}
.hero-logo {
width: 80px; height: 80px;
background: rgba(255,255,255,0.2);
border-radius: 20px;
border-radius: var(--radius-xl);
display: flex; align-items: center; justify-content: center;
font-size: 36px; font-weight: 900;
color: white; margin: 0 auto 24px;
@@ -80,7 +80,7 @@ body { font-family: var(--font-text); background: var(--background-secondary); c
background: rgba(255,255,255,0.18);
backdrop-filter: blur(20px);
border: 1px solid rgba(255,255,255,0.3);
border-radius: 20px; padding: 16px 28px;
border-radius: var(--radius-xl); padding: 16px 28px;
position: relative; z-index: 1;
}
.agg-score {
@@ -130,7 +130,7 @@ body { font-family: var(--font-text); background: var(--background-secondary); c
}
.review-card {
background: var(--background-primary);
border-radius: 20px;
border-radius: var(--radius-xl);
border: 0.5px solid var(--separator);
padding: 24px;
transition: transform 0.2s, box-shadow 0.2s;
@@ -146,7 +146,7 @@ body { font-family: var(--font-text); background: var(--background-secondary); c
}
.reviewer-info { display: flex; align-items: center; gap: 12px; }
.reviewer-avatar {
width: 44px; height: 44px; border-radius: 50%;
width: 44px; height: 44px; border-radius: var(--radius-full);
background: linear-gradient(135deg, var(--ios-orange), var(--ios-yellow));
display: flex; align-items: center; justify-content: center;
font-size: 18px; font-weight: 700; color: white;
@@ -185,7 +185,7 @@ body { font-family: var(--font-text); background: var(--background-secondary); c
/* CTA */
.cta-section {
background: linear-gradient(135deg, #FF6B00, #FF9500);
border-radius: 24px; padding: 40px 32px;
border-radius: var(--radius-xl); padding: 40px 32px;
text-align: center; margin: 0 20px 32px;
max-width: 920px; margin-left: auto; margin-right: auto;
}
@@ -200,7 +200,7 @@ body { font-family: var(--font-text); background: var(--background-secondary); c
display: inline-block;
background: white; color: #FF6B00;
font-size: 17px; font-weight: 700;
padding: 16px 32px; border-radius: 14px;
padding: 16px 32px; border-radius: var(--radius-lg);
text-decoration: none;
transition: transform 0.15s, box-shadow 0.15s;
}
@@ -264,7 +264,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="agg-rating">
<div class="agg-score" style="font-size:1.25rem;font-weight:700;">Coming soon</div>
<div class="agg-info">
<div class="stars" style="opacity:0;">★★★★★</div>
<div class="stars" style="opacity:0;"></div>
<div class="agg-count">Reviews after launch</div>
</div>
</div>
@@ -283,7 +283,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="reviews-grid" id="reviewsGrid">
<div class="review-card" style="text-align:center;padding:48px 24px;">
<div style="font-size:3rem;margin-bottom:16px;">📝</div>
<div style="font-size:3rem;margin-bottom:16px;"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 13V10L10 3L13 6L6 13H3Z" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 4L12 7" stroke="#666" stroke-width="1.5"/></svg></div>
<h3 style="font-size:1.25rem;font-weight:700;margin-bottom:12px;color:var(--label-primary);">Reviews coming soon</h3>
<p style="color:var(--label-secondary);line-height:1.6;max-width:400px;margin:0 auto;">quiXzoom launches in August 2026. Reviews from Zoomers and mission orderers will be published here once the platform is live.</p>
</div>
+4 -4
View File
@@ -137,7 +137,7 @@
.nav-logo .x { color: var(--blue); }
.hero { background: var(--white); padding: 64px 24px; text-align: center; border-bottom: 1px solid #e8eaf0; }
.hero img { max-width: 100%; width: 800px; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,.1); margin-bottom: 32px; }
.hero img { max-width: 100%; width: 800px; border-radius: var(--radius-lg); box-shadow: 0 8px 32px rgba(0,0,0,.1); margin-bottom: 32px; }
h1 { font-size: clamp(1.75rem, 4vw, 2.5rem); font-weight: 900; letter-spacing: -.04em; color: var(--dark); margin-bottom: 12px; }
h1 .x { color: var(--blue); }
.hero p { font-size: 1.0625rem; color: var(--muted); max-width: 600px; margin: 0 auto 24px; line-height: 1.65; }
@@ -147,7 +147,7 @@
main { max-width: 900px; margin: 0 auto; padding: 56px 24px; }
.variant-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; margin: 32px 0; }
.variant-card { background: var(--white); border-radius: 12px; padding: 20px; border: 1px solid #e8eaf0; }
.variant-card { background: var(--white); border-radius: var(--radius-md); padding: 20px; border: 1px solid #e8eaf0; }
.variant-card .spelling { font-size: 1.125rem; font-weight: 700; color: var(--dark); margin-bottom: 4px; }
.variant-card .note { font-size: .875rem; color: var(--muted); }
.variant-card.correct { border-color: var(--blue); }
@@ -193,7 +193,7 @@
<div class="variant-grid">
<div class="variant-card correct">
<div class="spelling">quiXzoom</div>
<div class="note"> Official spelling — capital X</div>
<div class="note"><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 spelling — capital X</div>
</div>
<div class="variant-card">
<div class="spelling">quixzoom</div>
@@ -253,7 +253,7 @@
title="quiXzoom App — iOS and Android"
width="800" height="420"
loading="lazy"
style="max-width:100%; border-radius:16px; box-shadow:0 8px 32px rgba(0,0,0,.1);"
style="max-width:100%; border-radius: var(--radius-lg); box-shadow:0 8px 32px rgba(0,0,0,.1);"
>
</div>
+15 -15
View File
@@ -114,7 +114,7 @@
.contact-btn:last-child { border-bottom: none; }
.btn-icon {
width: 44px; height: 44px;
border-radius: 10px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 22px;
flex-shrink: 0;
@@ -177,12 +177,12 @@
font-size: 13px;
font-weight: 600;
padding: 4px 12px;
border-radius: 20px;
border-radius: var(--radius-xl);
}
.status-dot {
width: 7px; height: 7px;
background: var(--green);
border-radius: 50%;
border-radius: var(--radius-full);
}
footer {
text-align: center;
@@ -252,7 +252,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="faq-item" onclick="toggle(this)">
<div class="faq-q">
How do I start taking missions as a Zoomer?
<span class="faq-arrow"></span>
<span class="faq-arrow"></span>
</div>
<div class="faq-a">
Download the quiXzoom app, create an account and verify your identity with a photo ID. Once your account is approved you will see available missions on the map. Tap a mission to view details and accept it. You then have the stated time limit to complete and upload the documentation.
@@ -262,7 +262,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="faq-item" onclick="toggle(this)">
<div class="faq-q">
How and when do I get paid?
<span class="faq-arrow"></span>
<span class="faq-arrow"></span>
</div>
<div class="faq-a">
Payment for an approved mission is processed within 5 business days to your registered bank account. You can view your full payout history under Settings → Payments in the app. The minimum payout amount is 100 SEK. You are responsible for your own tax reporting.
@@ -272,7 +272,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="faq-item" onclick="toggle(this)">
<div class="faq-q">
My mission was rejected what do I do?
<span class="faq-arrow"></span>
<span class="faq-arrow"></span>
</div>
<div class="faq-a">
The reason for rejection is shown in the mission details in the app. Common reasons: photos too dark or blurry, wrong location photographed, or too few images. In some cases you can supplement and re-upload this will be indicated in the app. Contact support below if you believe the decision is incorrect.
@@ -282,7 +282,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="faq-item" onclick="toggle(this)">
<div class="faq-q">
Can I cancel a mission I have accepted?
<span class="faq-arrow"></span>
<span class="faq-arrow"></span>
</div>
<div class="faq-a">
Yes, but cancellation should be done as soon as possible so the mission can be assigned to another Zoomer. Go to the mission card and select "Cancel". Repeated cancellations may affect your priority for future missions. If you have difficulty reaching the location, contact support directly.
@@ -292,7 +292,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="faq-item" onclick="toggle(this)">
<div class="faq-q">
How do I delete my account?
<span class="faq-arrow"></span>
<span class="faq-arrow"></span>
</div>
<div class="faq-a">
Go to Settings → Account → Close account in the app. Outstanding payments will be processed within 14 days. Your personal data will be deleted within 90 days, except for accounting data which we are legally required to retain for 7 years. You can also send a written request to <a href="mailto:privacy@quixzoom.com">privacy@quixzoom.com</a>.
@@ -306,7 +306,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="card-title">Contact us</div>
<a class="contact-btn" href="tel:+46812345678">
<div class="btn-icon blue-bg">📞</div>
<div class="btn-icon blue-bg"></div>
<div class="btn-text">
<div class="btn-label">Phone</div>
<div class="btn-sub">+46 8 123 45 67 · MonFri 09:0017:00 CET</div>
@@ -315,7 +315,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</a>
<a class="contact-btn" href="mailto:support@quixzoom.com">
<div class="btn-icon blue-bg">✉️</div>
<div class="btn-icon blue-bg"></div>
<div class="btn-text">
<div class="btn-label">Email</div>
<div class="btn-sub">support@quixzoom.com · Reply within 1 business day</div>
@@ -324,7 +324,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</a>
<a class="contact-btn" href="mailto:support@quixzoom.com?subject=Urgent%20matter">
<div class="btn-icon orange-bg">🚨</div>
<div class="btn-icon orange-bg"></div>
<div class="btn-text">
<div class="btn-label">Urgent matter</div>
<div class="btn-sub">Mark "URGENT" in the subject line priority handling</div>
@@ -333,7 +333,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</a>
<a class="contact-btn" href="mailto:legal@quixzoom.com">
<div class="btn-icon green-bg">⚖️</div>
<div class="btn-icon green-bg"></div>
<div class="btn-text">
<div class="btn-label">Legal & data protection</div>
<div class="btn-sub">legal@quixzoom.com / privacy@quixzoom.com</div>
@@ -379,15 +379,15 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="card">
<div class="card-title">Response times</div>
<div style="display:flex; gap:12px; flex-wrap:wrap;">
<div style="flex:1; min-width:140px; background:rgba(52,199,89,0.08); border-radius:10px; padding:12px; text-align:center;">
<div style="flex:1; min-width:140px; background:rgba(52,199,89,0.08); border-radius: var(--radius-md); padding:12px; text-align:center;">
<div style="font-size:24px; font-weight:700; color:#34C759;">24h</div>
<div style="font-size:12px; color:var(--label3); margin-top:2px;">Standard enquiries</div>
</div>
<div style="flex:1; min-width:140px; background:rgba(255,149,0,0.08); border-radius:10px; padding:12px; text-align:center;">
<div style="flex:1; min-width:140px; background:rgba(255,149,0,0.08); border-radius: var(--radius-md); padding:12px; text-align:center;">
<div style="font-size:24px; font-weight:700; color:#FF9500;">4h</div>
<div style="font-size:12px; color:var(--label3); margin-top:2px;">URGENT matters</div>
</div>
<div style="flex:1; min-width:140px; background:rgba(0,122,255,0.08); border-radius:10px; padding:12px; text-align:center;">
<div style="flex:1; min-width:140px; background:rgba(0,122,255,0.08); border-radius: var(--radius-md); padding:12px; text-align:center;">
<div style="font-size:24px; font-weight:700; color:#007AFF;">30 days</div>
<div style="font-size:12px; color:var(--label3); margin-top:2px;">GDPR requests (legal requirement)</div>
</div>
+71 -71
View File
@@ -31,7 +31,7 @@
line-height: 1.6;
}
/* ── Nav ──────────────────────────────────────────────────── */
/* Nav */
nav {
background: var(--navy);
padding: 0 2rem;
@@ -63,7 +63,7 @@
color: var(--white);
border: none;
padding: 0.5rem 1.25rem;
border-radius: 6px;
border-radius: var(--radius-sm);
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
@@ -72,7 +72,7 @@
}
.nav-cta:hover { background: var(--amber2); }
/* ── Hero ─────────────────────────────────────────────────── */
/* Hero */
.hero {
background: var(--navy);
color: var(--white);
@@ -106,7 +106,7 @@
.launch-badge .dot {
width: 7px; height: 7px;
background: var(--amber);
border-radius: 50%;
border-radius: var(--radius-full);
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
@@ -138,7 +138,7 @@
color: var(--white);
border: none;
padding: 0.85rem 2rem;
border-radius: 8px;
border-radius: var(--radius-md);
font-size: 1rem;
font-weight: 700;
cursor: pointer;
@@ -151,7 +151,7 @@
color: var(--white);
border: 2px solid rgba(255,255,255,0.4);
padding: 0.85rem 2rem;
border-radius: 8px;
border-radius: var(--radius-md);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
@@ -160,7 +160,7 @@
}
.btn-outline:hover { border-color: rgba(255,255,255,0.8); }
/* ── Countdown ────────────────────────────────────────────── */
/* Countdown */
.countdown-section {
background: var(--navy);
padding-bottom: 4rem;
@@ -199,7 +199,7 @@
margin-top: 0.3rem;
}
/* ── Section base ─────────────────────────────────────────── */
/* Section base */
section { padding: 5rem 2rem; }
.container { max-width: 1100px; margin: 0 auto; }
.section-tag {
@@ -225,7 +225,7 @@
margin-bottom: 3rem;
}
/* ── Stats strip ──────────────────────────────────────────── */
/* Stats strip */
.stats-strip {
background: var(--white);
border-top: 1px solid var(--border);
@@ -248,7 +248,7 @@
}
.stat-label { font-size: 0.85rem; color: var(--muted); margin-top: 0.25rem; }
/* ── How it works ─────────────────────────────────────────── */
/* How it works */
.steps-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
@@ -264,7 +264,7 @@
width: 36px; height: 36px;
background: var(--amber);
color: var(--white);
border-radius: 50%;
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
@@ -275,7 +275,7 @@
.step-card h3 { font-size: 1rem; font-weight: 700; margin-bottom: 0.5rem; color: var(--navy); }
.step-card p { font-size: 0.9rem; color: var(--muted); line-height: 1.5; }
/* ── Cities ───────────────────────────────────────────────── */
/* Cities */
.cities-section { background: var(--navy); color: var(--white); }
.cities-section h2 { color: var(--white); }
.cities-section .lead { color: rgba(255,255,255,0.75); }
@@ -306,13 +306,13 @@
font-size: 0.68rem;
font-weight: 700;
padding: 0.15rem 0.5rem;
border-radius: 4px;
border-radius: var(--radius-sm);
margin-top: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.06em;
}
/* ── Features ─────────────────────────────────────────────── */
/* Features */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
@@ -327,7 +327,7 @@
.feature-icon {
width: 44px; height: 44px;
background: rgba(200,118,58,0.1);
border-radius: 10px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
@@ -346,11 +346,11 @@
font-size: 0.72rem;
font-weight: 600;
padding: 0.2rem 0.6rem;
border-radius: 4px;
border-radius: var(--radius-sm);
margin-top: 0.75rem;
}
/* ── DAC7 + Skatt ─────────────────────────────────────────── */
/* DAC7 + Skatt */
.compliance-section { background: #F4F2EE; }
.compliance-grid {
display: grid;
@@ -379,7 +379,7 @@
.compliance-list li:last-child { border-bottom: none; }
.compliance-list .check { color: var(--green); font-weight: 700; flex-shrink: 0; }
/* ── Earnings calculator ──────────────────────────────────── */
/* Earnings calculator */
.calculator-section { background: var(--white); }
.calc-box {
background: var(--cream);
@@ -400,7 +400,7 @@
.calc-result {
background: var(--navy);
color: var(--white);
border-radius: 8px;
border-radius: var(--radius-md);
padding: 1.25rem 1.5rem;
margin-top: 1.5rem;
display: flex;
@@ -412,7 +412,7 @@
/* ── Registration / CTA ───────────────────────────────────── */
/* Registration / CTA */
.cta-section {
background: var(--navy);
color: var(--white);
@@ -434,7 +434,7 @@
min-width: 200px;
padding: 0.85rem 1.1rem;
border: 1.5px solid rgba(255,255,255,0.2);
border-radius: 8px;
border-radius: var(--radius-md);
background: rgba(255,255,255,0.08);
color: var(--white);
font-size: 0.95rem;
@@ -454,14 +454,14 @@
background: rgba(45,122,79,0.2);
border: 1px solid rgba(45,122,79,0.5);
color: #6ECFA0;
border-radius: 8px;
border-radius: var(--radius-md);
padding: 1rem 1.5rem;
max-width: 480px;
margin: 1rem auto 0;
font-size: 0.95rem;
}
/* ── Footer ───────────────────────────────────────────────── */
/* Footer */
footer {
background: #000D22;
color: rgba(255,255,255,0.75);
@@ -480,7 +480,7 @@
}
.footer-logo span { color: var(--amber); }
/* ── Responsive ───────────────────────────────────────────── */
/* Responsive */
@media (max-width: 640px) {
.nav-links { display: none; }
.hero { padding: 3rem 1.5rem 3rem; }
@@ -514,7 +514,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</head>
<body>
<!-- ── Nav ──────────────────────────────────────────────────────────────── -->
<!-- Nav -->
<nav>
<div class="nav-logo">qui<span>X</span>zoom</div>
<div class="nav-links">
@@ -526,7 +526,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<a href="#register" class="nav-cta">Bli Zoomer</a>
</nav>
<!-- ── Hero ─────────────────────────────────────────────────────────────── -->
<!-- Hero -->
<header class="hero">
<div class="launch-badge">
<span class="dot"></span>
@@ -542,7 +542,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</header>
<!-- ── Countdown ─────────────────────────────────────────────────────────── -->
<!-- Countdown -->
<div class="countdown-section">
<div class="countdown-container" id="countdown">
<div class="countdown-unit">
@@ -567,7 +567,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ── Stats strip ────────────────────────────────────────────────────────── -->
<!-- Stats strip -->
<div class="stats-strip">
<div class="stats-grid">
<div>
@@ -593,7 +593,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ── How it works ───────────────────────────────────────────────────────── -->
<!-- How it works -->
<section id="how-it-works" style="background: var(--cream);">
<div class="container">
<div class="section-tag">Hur det fungerar</div>
@@ -624,7 +624,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</section>
<!-- ── Cities ─────────────────────────────────────────────────────────────── -->
<!-- Cities -->
<section id="cities" class="cities-section">
<div class="container">
<div class="section-tag" style="color: rgba(200,118,58,0.9);">Sverige-launch</div>
@@ -632,40 +632,40 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<p class="lead">quiXzoom öppnar i Sveriges fem största städer den 1 augusti 2026 — med fler städer planerade under H2.</p>
<div class="cities-grid">
<div class="city-card">
<div class="city-icon">🏙️</div>
<div class="city-icon"></div>
<div class="city-name">Stockholm</div>
<div class="city-missions">89 aktiva zoomers • 312 uppdrag</div>
<div class="city-live-badge"> Live 15 jun</div>
<div class="city-live-badge"><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> Live 15 jun</div>
</div>
<div class="city-card">
<div class="city-icon"></div>
<div class="city-icon"></div>
<div class="city-name">Göteborg</div>
<div class="city-missions">41 aktiva zoomers • 148 uppdrag</div>
<div class="city-live-badge"> Live 15 jun</div>
<div class="city-live-badge"><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> Live 15 jun</div>
</div>
<div class="city-card">
<div class="city-icon">🌉</div>
<div class="city-icon"></div>
<div class="city-name">Malmö</div>
<div class="city-missions">28 aktiva zoomers • 97 uppdrag</div>
<div class="city-live-badge"> Live 15 jun</div>
<div class="city-live-badge"><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> Live 15 jun</div>
</div>
<div class="city-card">
<div class="city-icon">🎓</div>
<div class="city-icon"></div>
<div class="city-name">Uppsala</div>
<div class="city-missions">15 aktiva zoomers • 54 uppdrag</div>
<div class="city-live-badge"> Live 15 jun</div>
<div class="city-live-badge"><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> Live 15 jun</div>
</div>
<div class="city-card">
<div class="city-icon">⚙️</div>
<div class="city-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="city-name">Västerås</div>
<div class="city-missions">9 aktiva zoomers • 31 uppdrag</div>
<div class="city-live-badge"> Live 15 jun</div>
<div class="city-live-badge"><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> Live 15 jun</div>
</div>
</div>
</div>
</section>
<!-- ── Features ───────────────────────────────────────────────────────────── -->
<!-- Features -->
<section style="background: var(--cream);">
<div class="container">
<div class="section-tag">Plattformen</div>
@@ -673,46 +673,46 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<p class="lead">Allt du behöver för att driva din fotografi- eller videoverksamhet via quiXzoom — utan mellanhänder.</p>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">💸</div>
<div class="feature-icon"></div>
<h3>Stripe Connect-utbetalning</h3>
<p>Pengarna är hos dig inom minuter efter att kunden godkänt leveransen. Inga banköverföringar som tar dagar.</p>
<span class="feature-badge"> Direktbetalning i SEK</span>
<span class="feature-badge"><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> Direktbetalning i SEK</span>
</div>
<div class="feature-card">
<div class="feature-icon">🗺️</div>
<div class="feature-icon"></div>
<h3>Uppdrag på kartan</h3>
<p>Realtidskarta visar öppna uppdrag i din stad. Filtrera på typ, ersättning och tidpunkt.</p>
<span class="feature-badge"> Livekarta med WebSocket</span>
<span class="feature-badge"><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> Livekarta med WebSocket</span>
</div>
<div class="feature-card">
<div class="feature-icon">🤖</div>
<div class="feature-icon"></div>
<h3>AI-matchning</h3>
<p>Vi matchar dig med uppdrag baserat på din utrustning, rating och plats — maximala möjligheter, minimalt letande.</p>
<span class="feature-badge"> Automatisk matching</span>
<span class="feature-badge"><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> Automatisk matching</span>
</div>
<div class="feature-card">
<div class="feature-icon">🆔</div>
<div class="feature-icon"></div>
<h3>Personnummer-KYC</h3>
<p>Säker identitetsverifiering via Skatteverket SPAR. Krävs för alla svenska zoomers — skyddar både dig och kunden.</p>
<span class="feature-badge"> SPAR-integrerat</span>
<span class="feature-badge"><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> SPAR-integrerat</span>
</div>
<div class="feature-card">
<div class="feature-icon">📊</div>
<div class="feature-icon"></div>
<h3>Skatteunderlag & DAC7</h3>
<p>quiXzoom sköter automatisk DAC7-rapportering till Skatteverket. Du får ett klart årsunderlag för din deklaration.</p>
<span class="feature-badge"> EU Direktiv 2021/514</span>
<span class="feature-badge"><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> EU Direktiv 2021/514</span>
</div>
<div class="feature-card">
<div class="feature-icon"></div>
<div class="feature-icon"></div>
<h3>Rating & badges</h3>
<p>Bygg ditt rykte med omdömen och badges — Topp-zoomer, HD-expert, Drönarpilot. Högre rating = fler uppdrag.</p>
<span class="feature-badge"> Viktad 30-dagarsrating</span>
<span class="feature-badge"><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> Viktad 30-dagarsrating</span>
</div>
</div>
</div>
</section>
<!-- ── Earnings calculator ────────────────────────────────────────────────── -->
<!-- Earnings calculator -->
<section id="earnings" class="calculator-section">
<div class="container">
<div class="section-tag">Förtjänst</div>
@@ -747,7 +747,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</section>
<!-- ── DAC7 & Compliance ───────────────────────────────────────────────────── -->
<!-- DAC7 & Compliance -->
<section id="compliance" class="compliance-section">
<div class="container">
<div class="section-tag">Skatt & Efterlevnad</div>
@@ -757,28 +757,28 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="compliance-box">
<h3>DAC7 — EU Direktiv 2021/514</h3>
<ul class="compliance-list">
<li><span class="check"></span> Automatisk rapportering av zoomer-inkomster till Skatteverket</li>
<li><span class="check"></span> Rapporteringströskel: >30 uppdrag ELLER >2 000 EUR per år</li>
<li><span class="check"></span> Personnummer-verifiering via Skatteverket SPAR</li>
<li><span class="check"></span> Årsrapport skickas senast 31 januari varje år</li>
<li><span class="check"></span> Du får en kopia som skatteunderlag</li>
<li><span class="check"><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></span> Automatisk rapportering av zoomer-inkomster till Skatteverket</li>
<li><span class="check"><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></span> Rapporteringströskel: >30 uppdrag ELLER >2 000 EUR per år</li>
<li><span class="check"><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></span> Personnummer-verifiering via Skatteverket SPAR</li>
<li><span class="check"><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></span> Årsrapport skickas senast 31 januari varje år</li>
<li><span class="check"><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></span> Du får en kopia som skatteunderlag</li>
</ul>
</div>
<div class="compliance-box">
<h3>Stripe Connect-utbetalningar</h3>
<ul class="compliance-list">
<li><span class="check"></span> Stripe Connect — säker utbetalning</li>
<li><span class="check"></span> Kräver svenska mobilnummer (07X XXXXXXXX)</li>
<li><span class="check"></span> Utbetalning i SEK — inga valutaomräkningar</li>
<li><span class="check"></span> Minimibelopp: 100 SEK per utbetalning</li>
<li><span class="check"></span> Transaktionslogg tillgänglig i appen</li>
<li><span class="check"><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></span> Stripe Connect — säker utbetalning</li>
<li><span class="check"><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></span> Kräver svenska mobilnummer (07X XXXXXXXX)</li>
<li><span class="check"><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></span> Utbetalning i SEK — inga valutaomräkningar</li>
<li><span class="check"><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></span> Minimibelopp: 100 SEK per utbetalning</li>
<li><span class="check"><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></span> Transaktionslogg tillgänglig i appen</li>
</ul>
</div>
</div>
</div>
</section>
<!-- ── Registration CTA ───────────────────────────────────────────────────── -->
<!-- Registration CTA -->
<section id="register" class="cta-section">
<div class="container">
<div class="section-tag" style="color:rgba(200,118,58,0.9)">Sverige-launch 1 augusti 2026</div>
@@ -793,12 +793,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<strong id="waitlist-display">482</strong> zoomers i kö — bli nummer <strong id="waitlist-next">483</strong>
</div>
<div class="success-msg" id="success-msg">
🎉 Välkommen i kön! Vi hör av oss senast 1 augusti med dina inloggningsuppgifter och instruktioner för KYC.
Välkommen i kön! Vi hör av oss senast 1 augusti med dina inloggningsuppgifter och instruktioner för KYC.
</div>
</div>
</section>
<!-- ── Footer ─────────────────────────────────────────────────────────────── -->
<!-- Footer -->
<footer>
<div class="footer-logo">qui<span>X</span>zoom Sverige</div>
<div style="margin-bottom:0.75rem">
@@ -812,7 +812,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</footer>
<script>
// ── Countdown till 1 augusti 2026 ───────────────────────────────────────────
// Countdown till 1 augusti 2026
const LAUNCH = new Date('2026-06-15T08:00:00+02:00').getTime();
function updateCountdown() {
@@ -830,7 +830,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
updateCountdown();
setInterval(updateCountdown, 1000);
// ── Earnings calculator ───────────────────────────────────────────────────
// Earnings calculator
function updateCalc() {
const missions = parseInt(document.getElementById('missions-slider').value, 10);
const rate = parseInt(document.getElementById('rate-slider').value, 10);
@@ -844,7 +844,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
}
updateCalc();
// ── Waitlist ──────────────────────────────────────────────────────────────
// Waitlist
let waitlistCount = 482;
async function loadWaitlist() {
+11 -11
View File
@@ -101,7 +101,7 @@
.row:last-child { border-bottom: none; }
.row-icon {
width: 36px; height: 36px;
border-radius: 8px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 18px;
flex-shrink: 0;
@@ -201,21 +201,21 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-num">§ 2</div>
<div class="card-title">Requirements to become a Zoomer</div>
<div class="row">
<div class="row-icon blue">🪪</div>
<div class="row-icon blue"></div>
<div class="row-content">
<div class="row-label">Age and identity</div>
<div class="row-sub">You must be at least 18 years old. We verify your identity using a valid photo ID (passport or national ID card).</div>
</div>
</div>
<div class="row">
<div class="row-icon green">📱</div>
<div class="row-icon green"></div>
<div class="row-content">
<div class="row-label">Equipment</div>
<div class="row-sub">Smartphone with camera ≥12 MP and GPS. iOS 16+ or Android 12+. Stable internet connection.</div>
</div>
</div>
<div class="row">
<div class="row-icon orange">💼</div>
<div class="row-icon orange"></div>
<div class="row-content">
<div class="row-label">Tax</div>
<div class="row-sub">You are responsible for your own tax reporting in accordance with applicable law in your country.</div>
@@ -240,21 +240,21 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-num">§ 4</div>
<div class="card-title">Image requirements and quality</div>
<div class="row">
<div class="row-icon blue">📷</div>
<div class="row-icon blue"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg></div>
<div class="row-content">
<div class="row-label">Technical requirements</div>
<div class="row-sub">Min. 2 MB per image, unedited (no filters, no edits). EXIF data must be preserved.</div>
</div>
</div>
<div class="row">
<div class="row-icon green"></div>
<div class="row-icon green"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<div class="row-content">
<div class="row-label">Accuracy</div>
<div class="row-sub">Images must match the specified object and location. Manipulation or fabrication is prohibited and will result in immediate suspension and legal action.</div>
</div>
</div>
<div class="row">
<div class="row-icon orange">🏘️</div>
<div class="row-icon orange"></div>
<div class="row-content">
<div class="row-label">Privacy</div>
<div class="row-sub">Do not photograph individuals without consent. Avoid including private property outside the scope of the mission. You are responsible for obtaining any necessary permissions.</div>
@@ -321,14 +321,14 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-num">§ 9</div>
<div class="card-title">Account closure</div>
<div class="row">
<div class="row-icon blue">🚪</div>
<div class="row-icon blue"></div>
<div class="row-content">
<div class="row-label">You can close your account at any time</div>
<div class="row-sub">Go to Settings → Account → Close account. Outstanding compensation will be paid out within 14 days.</div>
</div>
</div>
<div class="row">
<div class="row-icon red">🚫</div>
<div class="row-icon red"></div>
<div class="row-content">
<div class="row-label">We may close accounts for violations</div>
<div class="row-sub">We reserve the right to immediately close accounts that violate these terms, without prior notice for serious violations.</div>
@@ -346,14 +346,14 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="card">
<div class="card-title">Contact</div>
<div class="row">
<div class="row-icon blue">⚖️</div>
<div class="row-icon blue"></div>
<div class="row-content">
<div class="row-label">Legal enquiries</div>
<div class="row-sub"><a href="mailto:legal@quixzoom.com">legal@quixzoom.com</a></div>
</div>
</div>
<div class="row">
<div class="row-icon green">🆘</div>
<div class="row-icon green"></div>
<div class="row-content">
<div class="row-label">Support</div>
<div class="row-sub"><a href="/support">quixzoom.com/support</a></div>
+150
View File
@@ -0,0 +1,150 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>quiXzoom — ถ่ายภาพโลก รับรายได้ | เป็น Zoomer</title>
<meta name="description" content="quiXzoom จ่ายเงินให้คุณถ่ายภาพโครงสร้างพื้นฐาน ธรรมชาติ และสภาพแวดล้อมในเมือง ไม่มีค่าธรรมเนียมแพลตฟอร์ม เปิดตัวสิงหาคม 2026">
<link rel="canonical" href="https://quixzoom.asia/th/">
<link rel="alternate" hreflang="th" href="https://quixzoom.asia/th/">
<link rel="alternate" hreflang="zh-CN" href="https://quixzoom.asia/zh-cn/">
<link rel="alternate" hreflang="ja" href="https://quixzoom.asia/ja/">
<link rel="alternate" hreflang="ko" href="https://quixzoom.asia/ko/">
<link rel="alternate" hreflang="en" href="https://www.quixzoom.com/">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Sukhumvit Set', 'Kanit', sans-serif;
background: #0a0a0a;
color: #ffffff;
line-height: 1.6;
}
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
header {
padding: 24px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 24px; font-weight: 700; color: #0066ff; }
.lang-switcher { display: flex; gap: 16px; }
.lang-switcher a {
color: rgba(255,255,255,0.6);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.lang-switcher a:hover, .lang-switcher a.active { color: #fff; }
.hero {
padding: 120px 0 80px;
text-align: center;
}
.hero h1 {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 800;
line-height: 1.2;
margin-bottom: 24px;
}
.hero p {
font-size: 1.25rem;
color: rgba(255,255,255,0.7);
max-width: 600px;
margin: 0 auto 40px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 16px 32px;
background: #0066ff;
color: #fff;
text-decoration: none;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0,102,255,0.3);
}
.features {
padding: 80px 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 32px;
}
.feature {
background: rgba(255,255,255,0.05);
padding: 32px;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.1);
}
.feature h3 {
font-size: 1.25rem;
margin-bottom: 12px;
color: #fff;
}
.feature p {
color: rgba(255,255,255,0.6);
font-size: 0.9375rem;
}
.currency-note {
text-align: center;
padding: 40px 0;
color: rgba(255,255,255,0.5);
font-size: 0.875rem;
}
footer {
padding: 40px 0;
text-align: center;
color: rgba(255,255,255,0.4);
font-size: 0.875rem;
border-top: 1px solid rgba(255,255,255,0.1);
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">quiXzoom</div>
<nav class="lang-switcher">
<a href="/th/" class="active">ไทย</a>
<a href="/zh-cn/"></a>
<a href="/ja/"></a>
<a href="/en/">EN</a>
</nav>
</header>
<section class="hero">
<h1>ถ่ายภาพโลก<br>รับรายได้</h1>
<p>quiXzoom จ่ายเงินให้คุณถ่ายภาพโครงสร้างพื้นฐาน สภาพแวดล้อมธรรมชาติ และทิวทัศน์เมือง ไม่มีค่าธรรมเนียมแพลตฟอร์ม จ่ายโดยตรงสำหรับภารกิจที่ได้รับการอนุมัติ</p>
<a href="https://www.quixzoom.com/register" class="btn">สมัครเป็น Zoomer</a>
</section>
<section class="features">
<div class="feature">
<h3>ถ่ายภาพและรับเงิน</h3>
<p>รับภารกิจใกล้เคียงและถ่ายภาพตามคำแนะนำ รับ ฿500-5,000 ต่อภารกิจที่ได้รับการอนุมัติ</p>
</div>
<div class="feature">
<h3>การจ่ายเงินที่รวดเร็ว</h3>
<p>จ่ายโดยตรงไปยังบัญชีธนาคารหรือพร้อมเพย์ภายใน 24 ชั่วโมงหลังจากได้รับการอนุมัติ</p>
</div>
<div class="feature">
<h3>ยืดหยุ่นและอิสระ</h3>
<p>ทำงานได้ทุกที่ทุกเวลา ไม่มีขั้นต่ำ ไม่มีกำหนดเวลา คุณตัดสินใจเอง</p>
</div>
</section>
<div class="currency-note">
ราคาแสดงเป็นบาทไทย (THB) จะแปลงตามอัตราแลกเปลี่ยนปัจจุบันเมื่อจ่ายเงิน
</div>
<footer>
<p>quiXzoom Asia | สนับสนุน: asia@quixzoom.com</p>
</footer>
</div>
</body>
</html>
+17 -17
View File
@@ -18,55 +18,55 @@
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);overflow:hidden}
#map{position:fixed;top:0;left:0;right:0;bottom:0;z-index:1}
.leaflet-control-zoom{margin-top:calc(var(--safe-top) + 64px) !important;margin-left:16px !important}
.leaflet-control-zoom a{background:var(--surface) !important;color:var(--text) !important;border-color:var(--border) !important;width:34px !important;height:34px !important;line-height:34px !important;border-radius:8px !important;font-size:18px !important}
.leaflet-control-zoom a{background:var(--surface) !important;color:var(--text) !important;border-color:var(--border) !important;width:34px !important;height:34px !important;line-height:34px !important;border-radius: var(--radius-md) !important;font-size:18px !important}
#hdr{position:fixed;top:0;left:0;right:0;z-index:1000;background:rgba(13,17,23,0.9);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-bottom:1px solid var(--border);padding:calc(var(--safe-top) + 10px) 16px 10px}
#hdr-inner{max-width:1200px;margin:0 auto;display:flex;align-items:center;gap:12px}
#logo{font-size:15px;font-weight:700;color:var(--text);text-decoration:none;white-space:nowrap}
#logo span{color:var(--green)}
#hdr-sub{flex:1;font-size:13px;color:var(--text2)}
.badge{font-size:10px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;background:rgba(63,185,80,0.15);color:var(--green);border:1px solid rgba(63,185,80,0.3);border-radius:20px;padding:3px 9px}
#ui-tog{position:fixed;top:calc(var(--safe-top) + 62px);right:16px;z-index:1000;background:rgba(22,27,34,0.92);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--border);border-radius:10px;padding:7px 11px;display:flex;align-items:center;gap:7px;cursor:pointer;font-size:12px;font-weight:600;color:var(--text);user-select:none;transition:opacity 0.2s}
.badge{font-size:10px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;background:rgba(63,185,80,0.15);color:var(--green);border:1px solid rgba(63,185,80,0.3);border-radius: var(--radius-xl);padding:3px 9px}
#ui-tog{position:fixed;top:calc(var(--safe-top) + 62px);right:16px;z-index:1000;background:rgba(22,27,34,0.92);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--border);border-radius: var(--radius-md);padding:7px 11px;display:flex;align-items:center;gap:7px;cursor:pointer;font-size:12px;font-weight:600;color:var(--text);user-select:none;transition:opacity 0.2s}
#ui-tog:active{opacity:0.7}
#ui-led{width:8px;height:8px;border-radius:50%;background:var(--green);flex-shrink:0;transition:background 0.2s}
#ui-led{width:8px;height:8px;border-radius: var(--radius-full);background:var(--green);flex-shrink:0;transition:background 0.2s}
#ui-tog.off #ui-led{background:var(--text2)}
#panel{position:fixed;bottom:0;left:0;right:0;z-index:900;background:rgba(22,27,34,0.94);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-top:1px solid var(--border);padding:10px 16px calc(var(--safe-bottom) + 10px)}
#panel-inner{max-width:1200px;margin:0 auto}
#ph{width:36px;height:4px;border-radius:2px;background:rgba(255,255,255,0.12);margin:0 auto 10px}
#ph{width:36px;height:4px;border-radius: var(--radius-sm);background:rgba(255,255,255,0.12);margin:0 auto 10px}
#leg{display:flex;gap:6px;flex-wrap:wrap;justify-content:center;margin-bottom:8px}
.li{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--text2);background:var(--surface2);border-radius:20px;padding:4px 10px;border:1px solid var(--border)}
.ld{width:9px;height:9px;border-radius:50%;flex-shrink:0}
.li{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--text2);background:var(--surface2);border-radius: var(--radius-xl);padding:4px 10px;border:1px solid var(--border)}
.ld{width:9px;height:9px;border-radius: var(--radius-full);flex-shrink:0}
#note{font-size:11px;color:var(--text2);text-align:center;line-height:1.4}
#note a{color:var(--blue);text-decoration:none}
.leaflet-popup-content-wrapper{background:var(--surface) !important;border:1px solid var(--border) !important;border-radius:12px !important;box-shadow:0 8px 32px rgba(0,0,0,0.6) !important;padding:0 !important}
.leaflet-popup-content-wrapper{background:var(--surface) !important;border:1px solid var(--border) !important;border-radius: var(--radius-md) !important;box-shadow:0 8px 32px rgba(0,0,0,0.6) !important;padding:0 !important}
.leaflet-popup-content{margin:0 !important;width:260px !important;color:var(--text)}
.leaflet-popup-tip{background:var(--surface) !important}
.leaflet-popup-close-button{color:var(--text2) !important;top:8px !important;right:10px !important}
.zpop{padding:14px 16px}
.zpop-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:10px}
.zpop-name{font-size:14px;font-weight:700;color:var(--text);line-height:1.3}
.zpop-conf{font-size:11px;color:var(--text2);background:var(--surface2);border-radius:6px;padding:2px 7px;white-space:nowrap;flex-shrink:0;font-weight:600}
.zpop-conf{font-size:11px;color:var(--text2);background:var(--surface2);border-radius: var(--radius-sm);padding:2px 7px;white-space:nowrap;flex-shrink:0;font-weight:600}
.zpop-scores{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:10px}
.zpop-sc{background:var(--surface2);border-radius:7px;padding:6px 8px}
.zpop-sc{background:var(--surface2);border-radius: var(--radius-sm);padding:6px 8px}
.zpop-sl{font-size:10px;color:var(--text2);font-weight:500;margin-bottom:3px}
.zpop-bar{height:3px;border-radius:2px;background:rgba(255,255,255,0.08);margin-bottom:2px}
.zpop-fill{height:100%;border-radius:2px}
.zpop-bar{height:3px;border-radius: var(--radius-sm);background:rgba(255,255,255,0.08);margin-bottom:2px}
.zpop-fill{height:100%;border-radius: var(--radius-sm)}
.zpop-sv{font-size:12px;font-weight:700;color:var(--text)}
.zpop-c{border-radius:8px;padding:7px 10px;font-size:11px;line-height:1.5;margin-bottom:8px}
.zpop-c{border-radius: var(--radius-md);padding:7px 10px;font-size:11px;line-height:1.5;margin-bottom:8px}
.zpop-c.low{background:rgba(63,185,80,0.1);color:#7ee787;border:1px solid rgba(63,185,80,0.2)}
.zpop-c.medium{background:rgba(240,136,62,0.1);color:#ffa657;border:1px solid rgba(240,136,62,0.2)}
.zpop-c.high{background:rgba(248,81,73,0.1);color:#ff7b72;border:1px solid rgba(248,81,73,0.2)}
.zpop-cl{font-weight:700;display:block;margin-bottom:2px}
.zpop-d{font-size:10px;color:var(--text2);border-top:1px solid var(--border);padding-top:7px;line-height:1.4}
.mpop{padding:14px 16px}
.mpop-cat{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px;color:var(--blue);background:rgba(88,166,255,0.1);border-radius:5px;padding:2px 7px;display:inline-block;margin-bottom:7px}
.mpop-cat{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px;color:var(--blue);background:rgba(88,166,255,0.1);border-radius: var(--radius-sm);padding:2px 7px;display:inline-block;margin-bottom:7px}
.mpop-title{font-size:14px;font-weight:600;color:var(--text);line-height:1.3;margin-bottom:10px}
.mpop-meta{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px}
.mpop-item{background:var(--surface2);border-radius:8px;padding:7px 9px}
.mpop-item{background:var(--surface2);border-radius: var(--radius-md);padding:7px 9px}
.mpop-lbl{font-size:10px;color:var(--text2);margin-bottom:2px;font-weight:500}
.mpop-val{font-size:13px;font-weight:700;color:var(--text)}
.mpop-reward{color:var(--green)}
.mpop-risk{display:flex;align-items:center;justify-content:space-between;gap:8px;border-top:1px solid var(--border);padding-top:9px}
.rb{font-size:10px;font-weight:700;border-radius:6px;padding:3px 8px}
.rb{font-size:10px;font-weight:700;border-radius: var(--radius-sm);padding:3px 8px}
.rb.L1{background:rgba(63,185,80,0.15);color:#7ee787}
.rb.L2{background:rgba(210,153,34,0.15);color:#e3b341}
.rb.L3{background:rgba(240,136,62,0.15);color:#ffa657}
@@ -129,7 +129,7 @@
];
const AS={green:{color:'#3fb950',fo:0.12,w:1.5,d:null},yellow:{color:'#d29922',fo:0.15,w:1.5,d:'5 4'},orange:{color:'#f0883e',fo:0.20,w:2,d:'5 4'},red:{color:'#f85149',fo:0.25,w:2,d:'4 3'},darkred:{color:'#8b0000',fo:0.30,w:2.5,d:'4 3'}};
const CL={low:'Contradiction Score: Low',medium:'Contradiction Score: Medium',high:'Contradiction Score: HIGH ⚠️'};
const CL={low:'Contradiction Score: Low',medium:'Contradiction Score: Medium',high:'Contradiction Score: HIGH <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg>'};
function bar(v){const p=Math.round(v*10),c=v>=7.5?'#3fb950':v>=5?'#d29922':'#f85149';return `<div class="zpop-bar"><div class="zpop-fill" style="width:${p}%;background:${c}"></div></div>`;}
+149
View File
@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>quiXzoom — Chụp ảnh thế giới. Nhận thù lao. | Trở thành Zoomer</title>
<meta name="description" content="quiXzoom trả tiền cho bạn chụp ảnh cơ sở hạ tầng, thiên nhiên và môi trường đô thị. Không phí nền tảng. Ra mắt tháng 8 năm 2026.">
<link rel="canonical" href="https://quixzoom.asia/vi/">
<link rel="alternate" hreflang="vi" href="https://quixzoom.asia/vi/">
<link rel="alternate" hreflang="zh-CN" href="https://quixzoom.asia/zh-cn/">
<link rel="alternate" hreflang="ja" href="https://quixzoom.asia/ja/">
<link rel="alternate" hreflang="en" href="https://www.quixzoom.com/">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: #0a0a0a;
color: #ffffff;
line-height: 1.6;
}
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
header {
padding: 24px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 24px; font-weight: 700; color: #0066ff; }
.lang-switcher { display: flex; gap: 16px; }
.lang-switcher a {
color: rgba(255,255,255,0.6);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.lang-switcher a:hover, .lang-switcher a.active { color: #fff; }
.hero {
padding: 120px 0 80px;
text-align: center;
}
.hero h1 {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 800;
line-height: 1.2;
margin-bottom: 24px;
}
.hero p {
font-size: 1.25rem;
color: rgba(255,255,255,0.7);
max-width: 600px;
margin: 0 auto 40px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 16px 32px;
background: #0066ff;
color: #fff;
text-decoration: none;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0,102,255,0.3);
}
.features {
padding: 80px 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 32px;
}
.feature {
background: rgba(255,255,255,0.05);
padding: 32px;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.1);
}
.feature h3 {
font-size: 1.25rem;
margin-bottom: 12px;
color: #fff;
}
.feature p {
color: rgba(255,255,255,0.6);
font-size: 0.9375rem;
}
.currency-note {
text-align: center;
padding: 40px 0;
color: rgba(255,255,255,0.5);
font-size: 0.875rem;
}
footer {
padding: 40px 0;
text-align: center;
color: rgba(255,255,255,0.4);
font-size: 0.875rem;
border-top: 1px solid rgba(255,255,255,0.1);
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">quiXzoom</div>
<nav class="lang-switcher">
<a href="/vi/" class="active">Tiếng Việt</a>
<a href="/zh-cn/"></a>
<a href="/ja/"></a>
<a href="/en/">EN</a>
</nav>
</header>
<section class="hero">
<h1>Chụp ảnh thế giới.<br>Nhận thù lao.</h1>
<p>quiXzoom trả tiền cho bạn chụp ảnh cơ sở hạ tầng, môi trường tự nhiên và cảnh quan đô thị. Không phí nền tảng. Thanh toán trực tiếp cho mỗi nhiệm vụ được phê duyệt.</p>
<a href="https://www.quixzoom.com/register" class="btn">Đăng ký làm Zoomer</a>
</section>
<section class="features">
<div class="feature">
<h3>Chụp ảnh và kiếm tiền</h3>
<p>Nhận nhiệm vụ gần đó và chụp ảnh theo hướng dẫn. Nhận ₫500,000-5,000,000 cho mỗi nhiệm vụ được phê duyệt.</p>
</div>
<div class="feature">
<h3>Thanh toán nhanh chóng</h3>
<p>Thanh toán trực tiếp vào tài khoản ngân hàng hoặc MoMo trong vòng 24 giờ sau khi được phê duyệt.</p>
</div>
<div class="feature">
<h3>Linh hoạt và tự do</h3>
<p>Làm việc mọi lúc mọi nơi. Không có yêu cầu tối thiểu, không có lịch làm việc. Bạn quyết định.</p>
</div>
</section>
<div class="currency-note">
Giá được hiển thị bằng đồng Việt Nam (VND). Sẽ được chuyển đổi theo tỷ giá hiện tại khi thanh toán.
</div>
<footer>
<p>quiXzoom Asia | Hỗ trợ: asia@quixzoom.com</p>
</footer>
</div>
</body>
</html>
+157
View File
@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>quiXzoom — | Zoomer</title>
<meta name="description" content="quiXzoom 2026820">
<link rel="canonical" href="https://quixzoom.asia/zh-cn/">
<link rel="alternate" hreflang="zh-CN" href="https://quixzoom.asia/zh-cn/">
<link rel="alternate" hreflang="ja" href="https://quixzoom.asia/ja/">
<link rel="alternate" hreflang="ko" href="https://quixzoom.asia/ko/">
<link rel="alternate" hreflang="th" href="https://quixzoom.asia/th/">
<link rel="alternate" hreflang="vi" href="https://quixzoom.asia/vi/">
<link rel="alternate" hreflang="id" href="https://quixzoom.asia/id/">
<link rel="alternate" hreflang="ms" href="https://quixzoom.asia/ms/">
<link rel="alternate" hreflang="hi" href="https://quixzoom.asia/hi/">
<link rel="alternate" hreflang="en" href="https://www.quixzoom.com/">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: #0a0a0a;
color: #ffffff;
line-height: 1.6;
}
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
header {
padding: 24px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 24px; font-weight: 700; color: #0066ff; }
.lang-switcher { display: flex; gap: 16px; }
.lang-switcher a {
color: rgba(255,255,255,0.6);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.lang-switcher a:hover, .lang-switcher a.active { color: #fff; }
.hero {
padding: 120px 0 80px;
text-align: center;
}
.hero h1 {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 800;
line-height: 1.1;
margin-bottom: 24px;
letter-spacing: -0.02em;
}
.hero p {
font-size: 1.25rem;
color: rgba(255,255,255,0.7);
max-width: 600px;
margin: 0 auto 40px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 16px 32px;
background: #0066ff;
color: #fff;
text-decoration: none;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0,102,255,0.3);
}
.features {
padding: 80px 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 32px;
}
.feature {
background: rgba(255,255,255,0.05);
padding: 32px;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.1);
}
.feature h3 {
font-size: 1.25rem;
margin-bottom: 12px;
color: #fff;
}
.feature p {
color: rgba(255,255,255,0.6);
font-size: 0.9375rem;
}
.currency-note {
text-align: center;
padding: 40px 0;
color: rgba(255,255,255,0.5);
font-size: 0.875rem;
}
footer {
padding: 40px 0;
text-align: center;
color: rgba(255,255,255,0.4);
font-size: 0.875rem;
border-top: 1px solid rgba(255,255,255,0.1);
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">quiXzoom</div>
<nav class="lang-switcher">
<a href="/zh-cn/" class="active"></a>
<a href="/ja/"></a>
<a href="/ko/"></a>
<a href="/th/">ไทย</a>
<a href="/en/">EN</a>
</nav>
</header>
<section class="hero">
<h1><br></h1>
<p>quiXzoom </p>
<a href="https://www.quixzoom.com/register" class="btn"> Zoomer</a>
</section>
<section class="features">
<div class="feature">
<h3><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg> </h3>
<p> ¥50-500 </p>
</div>
<div class="feature">
<h3><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> </h3>
<p> 24 </p>
</div>
<div class="feature">
<h3> </h3>
<p></p>
</div>
</section>
<div class="currency-note">
(CNY)
</div>
<footer>
<p>quiXzoom Asia | : asia@quixzoom.com</p>
<p>: </p>
</footer>
</div>
</body>
</html>
+152
View File
@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>quiXzoom — | Zoomer</title>
<meta name="description" content="quiXzoom 2026820">
<link rel="canonical" href="https://quixzoom.asia/zh-tw/">
<link rel="alternate" hreflang="zh-TW" href="https://quixzoom.asia/zh-tw/">
<link rel="alternate" hreflang="zh-CN" href="https://quixzoom.asia/zh-cn/">
<link rel="alternate" hreflang="ja" href="https://quixzoom.asia/ja/">
<link rel="alternate" hreflang="ko" href="https://quixzoom.asia/ko/">
<link rel="alternate" hreflang="en" href="https://www.quixzoom.com/">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang TC', 'Hiragino Sans TC', 'Microsoft JhengHei', sans-serif;
background: #0a0a0a;
color: #ffffff;
line-height: 1.6;
}
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
header {
padding: 24px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 24px; font-weight: 700; color: #0066ff; }
.lang-switcher { display: flex; gap: 16px; }
.lang-switcher a {
color: rgba(255,255,255,0.6);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.lang-switcher a:hover, .lang-switcher a.active { color: #fff; }
.hero {
padding: 120px 0 80px;
text-align: center;
}
.hero h1 {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 800;
line-height: 1.1;
margin-bottom: 24px;
letter-spacing: -0.02em;
}
.hero p {
font-size: 1.25rem;
color: rgba(255,255,255,0.7);
max-width: 600px;
margin: 0 auto 40px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 16px 32px;
background: #0066ff;
color: #fff;
text-decoration: none;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0,102,255,0.3);
}
.features {
padding: 80px 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 32px;
}
.feature {
background: rgba(255,255,255,0.05);
padding: 32px;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.1);
}
.feature h3 {
font-size: 1.25rem;
margin-bottom: 12px;
color: #fff;
}
.feature p {
color: rgba(255,255,255,0.6);
font-size: 0.9375rem;
}
.currency-note {
text-align: center;
padding: 40px 0;
color: rgba(255,255,255,0.5);
font-size: 0.875rem;
}
footer {
padding: 40px 0;
text-align: center;
color: rgba(255,255,255,0.4);
font-size: 0.875rem;
border-top: 1px solid rgba(255,255,255,0.1);
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">quiXzoom</div>
<nav class="lang-switcher">
<a href="/zh-tw/" class="active"></a>
<a href="/zh-cn/"></a>
<a href="/ja/"></a>
<a href="/ko/"></a>
<a href="/en/">EN</a>
</nav>
</header>
<section class="hero">
<h1><br></h1>
<p>quiXzoom </p>
<a href="https://www.quixzoom.com/register" class="btn"> Zoomer</a>
</section>
<section class="features">
<div class="feature">
<h3></h3>
<p> NT$200-2,000 </p>
</div>
<div class="feature">
<h3></h3>
<p> 24 LINE Pay</p>
</div>
<div class="feature">
<h3></h3>
<p></p>
</div>
</section>
<div class="currency-note">
(TWD)
</div>
<footer>
<p>quiXzoom Asia | : asia@quixzoom.com</p>
</footer>
</div>
</body>
</html>
+117 -117
View File
@@ -26,11 +26,11 @@
overflow-x: hidden;
}
/* ── Tab system ── */
/* Tab system */
.tab-page { display: none; }
.tab-page.active { display: block; }
/* ── Nav Bar ── */
/* Nav Bar */
.nav-bar {
position: sticky; top: 0; z-index: 200;
display: flex; align-items: center; justify-content: space-between;
@@ -59,7 +59,7 @@
font-size: 20px; padding: 4px;
}
/* ── Tab Bar ── */
/* Tab Bar */
.tab-bar {
position: fixed; bottom: 0; left: 0; right: 0; z-index: 300;
display: flex;
@@ -82,35 +82,35 @@
.tab-badge-wrap { position: relative; display: inline-block; }
.tab-dot {
position: absolute; top: -2px; right: -4px;
width: 8px; height: 8px; border-radius: 50%;
width: 8px; height: 8px; border-radius: var(--radius-full);
background: var(--ios-red); border: 1.5px solid var(--background-primary);
display: none;
}
.tab-dot.show { display: block; }
/* ── Segment ── */
/* Segment */
.segment-wrap { padding: 8px 16px; }
.segment { display: flex; background: var(--fill-tertiary); border-radius: 9px; padding: 2px; }
.segment { display: flex; background: var(--fill-tertiary); border-radius: var(--radius-md); padding: 2px; }
.seg-btn {
flex: 1; padding: 6px 0; background: none; border: none;
font-family: var(--font-text); font-size: 13px; font-weight: 500;
color: var(--label-secondary); cursor: pointer; border-radius: 7px; transition: all 0.2s;
color: var(--label-secondary); cursor: pointer; border-radius: var(--radius-sm); transition: all 0.2s;
}
.seg-btn.active {
background: var(--background-secondary); color: var(--label-primary); font-weight: 600;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
/* ── Section header ── */
/* Section header */
.section-hdr {
padding: 20px 16px 8px;
font-size: 13px; font-weight: 500;
color: var(--label-secondary); text-transform: uppercase; letter-spacing: 0.5px;
}
/* ══════════════════════════════════
/*
MAP TAB — Karta
══════════════════════════════════ */
*/
#map-full {
width: 100%; height: calc(100dvh - env(safe-area-inset-top, 44px) - 44px - 83px - env(safe-area-inset-bottom, 34px));
min-height: 400px;
@@ -121,7 +121,7 @@
}
.map-filter-bar::-webkit-scrollbar { display: none; }
.filter-chip {
flex-shrink: 0; padding: 6px 14px; border-radius: 20px; border: 1.5px solid var(--separator);
flex-shrink: 0; padding: 6px 14px; border-radius: var(--radius-xl); border: 1.5px solid var(--separator);
background: var(--fill-tertiary); color: var(--label-secondary);
font-size: 13px; font-weight: 500; cursor: pointer; white-space: nowrap; transition: all 0.2s;
}
@@ -130,12 +130,12 @@
position: absolute; bottom: calc(90px + env(safe-area-inset-bottom, 34px) + 8px);
right: 12px; z-index: 500;
background: rgba(28,28,30,0.88); backdrop-filter: blur(20px);
border-radius: 12px; padding: 10px 12px;
border-radius: var(--radius-md); padding: 10px 12px;
border: 0.5px solid var(--separator);
font-size: 11px;
}
.legend-row { display: flex; align-items: center; gap: 6px; padding: 2px 0; color: var(--label-secondary); }
.legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.legend-dot { width: 10px; height: 10px; border-radius: var(--radius-full); flex-shrink: 0; }
/* Mission bottom-sheet */
.mission-sheet {
@@ -148,15 +148,15 @@
.mission-sheet.show { transform: translateY(0); opacity: 1; pointer-events: all; }
.sheet-card {
background: rgba(28,28,30,0.96); backdrop-filter: blur(40px);
border-radius: 18px; padding: 16px;
border-radius: var(--radius-lg); padding: 16px;
border: 0.5px solid var(--separator);
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.sheet-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 10px; }
.sheet-title { font-size: 17px; font-weight: 700; letter-spacing: -0.4px; flex: 1; padding-right: 8px; }
.sheet-close { background: var(--fill-secondary); border: none; border-radius: 50%; width: 28px; height: 28px; cursor: pointer; font-size: 14px; color: var(--label-secondary); flex-shrink: 0; }
.sheet-close { background: var(--fill-secondary); border: none; border-radius: var(--radius-full); width: 28px; height: 28px; cursor: pointer; font-size: 14px; color: var(--label-secondary); flex-shrink: 0; }
.sheet-meta { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.sheet-chip { padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; }
.sheet-chip { padding: 4px 10px; border-radius: var(--radius-xl); font-size: 12px; font-weight: 600; }
.chip-reward { background: rgba(52,199,89,0.15); color: var(--ios-green); }
.chip-city { background: rgba(10,132,255,0.12); color: #0A84FF; }
.chip-type { background: var(--fill-tertiary); color: var(--label-secondary); }
@@ -164,7 +164,7 @@
.sheet-desc { font-size: 14px; color: var(--label-secondary); line-height: 1.5; margin-bottom: 14px; }
.sheet-actions { display: flex; gap: 10px; }
.sheet-btn {
flex: 1; padding: 13px; border-radius: 12px; border: none;
flex: 1; padding: 13px; border-radius: var(--radius-md); border: none;
font-family: var(--font-text); font-size: 15px; font-weight: 600;
cursor: pointer; transition: opacity 0.15s;
}
@@ -172,12 +172,12 @@
.btn-primary { background: #0A84FF; color: #fff; }
.btn-secondary { background: var(--fill-secondary); color: var(--label-primary); }
/* ══════════════════════════════════
/*
MINA UPPDRAG TAB
══════════════════════════════════ */
*/
.missions-list { padding: 0 16px; display: flex; flex-direction: column; gap: 12px; }
.mission-card {
background: var(--background-secondary); border-radius: 16px; overflow: hidden;
background: var(--background-secondary); border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
cursor: pointer; transition: transform 0.12s, opacity 0.12s;
}
@@ -186,7 +186,7 @@
display: flex; align-items: flex-start; gap: 12px; padding: 14px 14px 10px;
}
.mc-icon {
width: 46px; height: 46px; border-radius: 12px;
width: 46px; height: 46px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 22px; flex-shrink: 0;
}
@@ -203,7 +203,7 @@
padding: 10px 14px 14px; border-top: 0.5px solid var(--separator);
}
.badge {
padding: 3px 9px; border-radius: 20px; font-size: 11px; font-weight: 700;
padding: 3px 9px; border-radius: var(--radius-xl); font-size: 11px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.3px;
}
.b-open { background: rgba(52,199,89,0.15); color: var(--ios-green); }
@@ -217,7 +217,7 @@
margin: 12px 16px;
background: linear-gradient(135deg, rgba(10,132,255,0.18), rgba(94,92,230,0.18));
border: 1px solid rgba(10,132,255,0.35);
border-radius: 16px; padding: 16px; display: none;
border-radius: var(--radius-lg); padding: 16px; display: none;
}
.active-banner.show { display: block; }
.ab-label { font-size: 11px; font-weight: 700; color: #0A84FF; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
@@ -225,7 +225,7 @@
.ab-loc { font-size: 13px; color: var(--label-secondary); margin-bottom: 12px; }
.ab-actions { display: flex; gap: 8px; }
.ab-btn {
flex: 1; padding: 11px 8px; border-radius: 11px; border: none;
flex: 1; padding: 11px 8px; border-radius: var(--radius-md); border: none;
font-family: var(--font-text); font-size: 14px; font-weight: 600;
cursor: pointer; transition: opacity 0.15s;
}
@@ -243,9 +243,9 @@
.empty-title { font-size: 20px; font-weight: 700; }
.empty-sub { font-size: 15px; color: var(--label-secondary); line-height: 1.5; }
/* ══════════════════════════════════
/*
INTÄKTER TAB — Earnings
══════════════════════════════════ */
*/
.earnings-hero {
background: linear-gradient(160deg, #061628 0%, #010b1a 50%, #030e21 100%);
padding: 28px 24px 32px; text-align: center; position: relative; overflow: hidden;
@@ -269,7 +269,7 @@
.eh-actions { display: flex; gap: 10px; margin-top: 20px; position: relative; z-index: 1; }
.eh-btn {
flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px;
background: rgba(255,255,255,0.1); border: none; border-radius: 14px;
background: rgba(255,255,255,0.1); border: none; border-radius: var(--radius-lg);
padding: 13px 8px; cursor: pointer; transition: background 0.15s; font-family: var(--font-text);
}
.eh-btn:active { background: rgba(255,255,255,0.18); }
@@ -288,14 +288,14 @@
/* Payout CTA */
.payout-card {
margin: 16px; background: var(--background-secondary);
border-radius: 16px; overflow: hidden; border: 0.5px solid var(--separator);
border-radius: var(--radius-lg); overflow: hidden; border: 0.5px solid var(--separator);
}
.payout-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 16px; border-bottom: 0.5px solid var(--separator);
}
.payout-title { font-size: 16px; font-weight: 600; }
.payout-badge { padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: 700; }
.payout-badge { padding: 4px 10px; border-radius: var(--radius-xl); font-size: 12px; font-weight: 700; }
.pb-pending { background: rgba(255,149,0,0.12); color: var(--ios-orange); }
.payout-row {
@@ -304,7 +304,7 @@
}
.payout-row:last-child { border-bottom: none; }
.pr-icon {
width: 40px; height: 40px; border-radius: 10px;
width: 40px; height: 40px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 20px; flex-shrink: 0;
}
@@ -319,9 +319,9 @@
.pr-amount.pending { color: var(--ios-orange); }
.pr-amount.payout { color: #0A84FF; }
/* ══════════════════════════════════
/*
LEADERBOARD TAB
══════════════════════════════════ */
*/
.lb-hero {
padding: 20px 16px 12px;
background: linear-gradient(135deg, rgba(10,132,255,0.1) 0%, rgba(94,92,230,0.1) 100%);
@@ -334,7 +334,7 @@
}
.lb-total-chip {
display: flex; align-items: center; gap: 6px;
background: var(--fill-secondary); border-radius: 20px;
background: var(--fill-secondary); border-radius: var(--radius-xl);
padding: 6px 12px; font-size: 13px; font-weight: 600;
}
@@ -353,7 +353,7 @@
.rank-3 { color: #CD7F32; }
.rank-n { color: var(--label-tertiary); }
.lb-avatar {
width: 44px; height: 44px; border-radius: 50%;
width: 44px; height: 44px; border-radius: var(--radius-full);
display: flex; align-items: center; justify-content: center;
font-size: 22px; flex-shrink: 0;
}
@@ -377,18 +377,18 @@
.city-name { font-size: 16px; font-weight: 700; }
.city-meta { font-size: 13px; color: var(--label-secondary); margin-top: 1px; }
.city-bar-wrap { flex: 1; }
.city-bar-track { height: 5px; background: var(--fill-secondary); border-radius: 3px; overflow: hidden; }
.city-bar-fill { height: 100%; border-radius: 3px; background: linear-gradient(90deg, #0A84FF, #5E5CE6); }
.city-bar-track { height: 5px; background: var(--fill-secondary); border-radius: var(--radius-sm); overflow: hidden; }
.city-bar-fill { height: 100%; border-radius: var(--radius-sm); background: linear-gradient(90deg, #0A84FF, #5E5CE6); }
.city-count { font-size: 18px; font-weight: 700; font-family: var(--font-display); text-align: right; flex-shrink: 0; }
/* ══════════════════════════════════
/*
SHARED UTILITIES
══════════════════════════════════ */
*/
.toast {
position: fixed; bottom: calc(90px + env(safe-area-inset-bottom, 0px));
left: 50%; transform: translateX(-50%) translateY(20px);
background: rgba(50,50,52,0.96); color: #fff;
padding: 10px 20px; border-radius: 20px; font-size: 14px; font-weight: 500;
padding: 10px 20px; border-radius: var(--radius-xl); font-size: 14px; font-weight: 500;
opacity: 0; transition: all 0.3s; z-index: 1000; white-space: nowrap; pointer-events: none;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
@@ -396,7 +396,7 @@
.loading-row { display: flex; justify-content: center; padding: 48px; }
.spinner {
width: 28px; height: 28px; border: 3px solid var(--fill-secondary);
border-top-color: #0A84FF; border-radius: 50%; animation: spin 0.8s linear infinite;
border-top-color: #0A84FF; border-radius: var(--radius-full); animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@@ -412,18 +412,18 @@
border-top: 0.5px solid var(--separator);
}
.upload-sheet.show { transform: translateY(0); }
.upload-handle { width: 36px; height: 4px; border-radius: 2px; background: var(--fill-primary); margin: 0 auto 20px; }
.upload-handle { width: 36px; height: 4px; border-radius: var(--radius-sm); background: var(--fill-primary); margin: 0 auto 20px; }
.upload-title { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
.upload-sub { font-size: 14px; color: var(--label-secondary); margin-bottom: 20px; }
.upload-previews { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
.preview-thumb {
width: 80px; height: 80px; border-radius: 10px; object-fit: cover;
width: 80px; height: 80px; border-radius: var(--radius-md); object-fit: cover;
border: 0.5px solid var(--separator); background: var(--fill-secondary);
display: flex; align-items: center; justify-content: center; font-size: 28px;
}
.upload-actions { display: flex; gap: 10px; }
.upload-btn {
flex: 1; padding: 14px; border-radius: 12px; border: none;
flex: 1; padding: 14px; border-radius: var(--radius-md); border: none;
font-family: var(--font-text); font-size: 15px; font-weight: 600;
cursor: pointer; transition: opacity 0.15s;
}
@@ -472,7 +472,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<body>
<h1 style="position:absolute;left:-9999px;">Zoomer App - quiXzoom</h1>
<!-- ══════════════════════════════ KARTA TAB ══════════════════════════════ -->
<!-- KARTA TAB -->
<div id="page-karta" class="tab-page active">
<nav class="nav-bar">
<span class="nav-title nav-logo">quiXzoom</span>
@@ -499,7 +499,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="sheet-card">
<div class="sheet-header">
<div class="sheet-title" id="sheet-title"></div>
<button class="sheet-close" onclick="closeSheet()"></button>
<button class="sheet-close" onclick="closeSheet()"></button>
</div>
<div class="sheet-meta" id="sheet-meta"></div>
<div class="sheet-desc" id="sheet-desc"></div>
@@ -510,7 +510,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ══════════════════════════════ MINA UPPDRAG TAB ══════════════════════════════ -->
<!-- MINA UPPDRAG TAB -->
<div id="page-mina" class="tab-page">
<nav class="nav-bar">
<span class="nav-title">Mina uppdrag</span>
@@ -523,13 +523,13 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<div id="active-banner" class="active-banner">
<div class="ab-label">🔥 Aktivt uppdrag</div>
<div class="ab-label"> Aktivt uppdrag</div>
<div class="ab-title" id="ab-title"></div>
<div class="ab-loc" id="ab-loc"></div>
<div class="ab-actions">
<button class="ab-btn btn-cam" onclick="openUploadSheet()">📷 Ladda upp media</button>
<button class="ab-btn btn-done" onclick="completeActiveMission()"> Slutför</button>
<button class="ab-btn btn-cancel-sm" onclick="cancelActiveMission()"></button>
<button class="ab-btn btn-cam" onclick="openUploadSheet()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg> Ladda upp media</button>
<button class="ab-btn btn-done" onclick="completeActiveMission()"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Slutför</button>
<button class="ab-btn btn-cancel-sm" onclick="cancelActiveMission()"></button>
</div>
</div>
<div class="section-hdr" id="mina-section-lbl">Öppna uppdrag</div>
@@ -537,7 +537,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div style="height:20px"></div>
</div>
<!-- ══════════════════════════════ INTÄKTER TAB ══════════════════════════════ -->
<!-- INTÄKTER TAB -->
<div id="page-intakter" class="tab-page">
<nav class="nav-bar">
<span class="nav-title">Intäkter</span>
@@ -548,11 +548,11 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="eh-sub" id="e-pending-lbl">kr 1 250 väntar på verifiering</div>
<div class="eh-actions">
<button class="eh-btn" onclick="requestPayout()">
<span class="eh-btn-icon">💳</span><span class="eh-btn-label">Ta ut</span>
<span class="eh-btn-icon"></span><span class="eh-btn-label">Ta ut</span>
</button>
<button class="eh-btn" onclick="showToast('🧾 Exporterar årssammanfattning...')">
<span class="eh-btn-icon">🧾</span><span class="eh-btn-label">Skatt</span>
<button class="eh-btn" onclick="showToast(' Exporterar årssammanfattning...')">
<span class="eh-btn-icon"></span><span class="eh-btn-label">Skatt</span>
</button>
</div>
</div>
@@ -578,7 +578,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="payout-badge pb-pending">Pågår</div>
</div>
<div class="payout-row">
<div class="pr-icon pri-credit">💰</div>
<div class="pr-icon pri-credit"></div>
<div class="pr-body">
<div class="pr-title">Fastighetsfilm — Lidingö</div>
<div class="pr-sub">Verifieras · förfaller 5 jun</div>
@@ -602,7 +602,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div style="height:20px"></div>
</div>
<!-- ══════════════════════════════ LEADERBOARD TAB ══════════════════════════════ -->
<!-- LEADERBOARD TAB -->
<div id="page-leaderboard" class="tab-page">
<nav class="nav-bar">
<span class="nav-title">Leaderboard</span>
@@ -611,7 +611,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="lb-hero-title">Top Zoomers</div>
<div class="lb-hero-sub">Sverige · Augusti 2026 · Rankings uppdateras live</div>
<div class="lb-totals" id="lb-totals">
<div class="lb-total-chip"> Laddar...</div>
<div class="lb-total-chip"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> Laddar...</div>
</div>
</div>
<div class="segment-wrap">
@@ -625,25 +625,25 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div style="height:20px"></div>
</div>
<!-- ══════════════════════════════ TAB BAR ══════════════════════════════ -->
<!-- TAB BAR -->
<nav class="tab-bar">
<button class="tab-item active" id="tab-karta" onclick="switchTab('karta')">
<span class="tab-icon">🗺️</span>
<span class="tab-icon"></span>
<span class="tab-label">Karta</span>
</button>
<button class="tab-item" id="tab-mina" onclick="switchTab('mina')">
<span class="tab-badge-wrap">
<span class="tab-icon">📋</span>
<span 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></span>
<span class="tab-dot" id="mina-dot"></span>
</span>
<span class="tab-label">Mina</span>
</button>
<button class="tab-item" id="tab-intakter" onclick="switchTab('intakter')">
<span class="tab-icon">💰</span>
<span class="tab-icon"></span>
<span class="tab-label">Intäkter</span>
</button>
<button class="tab-item" id="tab-leaderboard" onclick="switchTab('leaderboard')">
<span class="tab-icon">🏆</span>
<span class="tab-icon"></span>
<span class="tab-label">Topp</span>
</button>
</nav>
@@ -657,7 +657,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="upload-sub">Bifoga foton eller video som bevis på slutfört uppdrag</div>
<div class="upload-previews" id="upload-previews"></div>
<div class="upload-actions">
<button class="upload-btn" style="background:var(--fill-secondary);color:var(--label-primary)" onclick="triggerFileInput()">📷 Välj fil</button>
<button class="upload-btn" style="background:var(--fill-secondary);color:var(--label-primary)" onclick="triggerFileInput()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg> Välj fil</button>
<button class="upload-btn btn-primary" onclick="submitMedia()">Skicka bevis </button>
</div>
</div>
@@ -680,14 +680,14 @@ let lbView = 'zoomers';
let lbData = null;
let uploadedMediaUrls = [];
// ── Toast ──────────────────────────────────────────────────────────────────
// Toast
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg; t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2800);
}
// ── Tab switching ──────────────────────────────────────────────────────────
// Tab switching
function switchTab(tab) {
document.querySelectorAll('.tab-page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
@@ -699,7 +699,7 @@ function switchTab(tab) {
if (tab === 'leaderboard') loadLeaderboard();
}
// ── Active mission (localStorage) ─────────────────────────────────────────
// Active mission (localStorage)
function setActiveMission(m) {
activeMission = m;
if (m) localStorage.setItem('qz_active', JSON.stringify(m));
@@ -715,18 +715,18 @@ function updateMinaDot() {
if (dot) dot.classList.toggle('show', !!getActiveMission());
}
// ══════════════════════════════════════════════════════════════════════════
//
// KARTA TAB
// ══════════════════════════════════════════════════════════════════════════
//
const TYPE_CFG = {
photo: { icon: '📷', color: '#0A84FF', bg: 'rgba(10,132,255,0.2)' },
video: { icon: '🎬', color: '#5E5CE6', bg: 'rgba(94,92,230,0.2)' },
drone: { icon: '🚁', color: '#FF9500', bg: 'rgba(255,149,0,0.2)' },
survey: { icon: '📋', color: '#34C759', bg: 'rgba(52,199,89,0.2)' },
audit: { icon: '🔍', color: '#FF2D55', bg: 'rgba(255,45,85,0.2)' },
photo: { icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg>', color: '#0A84FF', bg: 'rgba(10,132,255,0.2)' },
video: { icon: '', color: '#5E5CE6', bg: 'rgba(94,92,230,0.2)' },
drone: { icon: '<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>', color: '#FF9500', bg: 'rgba(255,149,0,0.2)' },
survey: { 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>', color: '#34C759', bg: 'rgba(52,199,89,0.2)' },
audit: { 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>', color: '#FF2D55', bg: 'rgba(255,45,85,0.2)' },
};
function getCfg(t) { return TYPE_CFG[t] || { icon: '📍', color: '#8E8E93', bg: 'rgba(142,142,147,0.2)' }; }
function getCfg(t) { return TYPE_CFG[t] || { icon: '', color: '#8E8E93', bg: 'rgba(142,142,147,0.2)' }; }
async function fetchMapMissions(city = '') {
try {
@@ -757,7 +757,7 @@ function renderMapMarkers() {
if (!lat || !lon) return;
const cfg = getCfg(m.mission_type);
const icon = L.divIcon({
html: `<div style="width:40px;height:40px;border-radius:50%;background:${cfg.bg};border:2.5px solid ${cfg.color};display:flex;align-items:center;justify-content:center;font-size:18px;box-shadow:0 2px 10px rgba(0,0,0,0.4);">${cfg.icon}</div>`,
html: `<div style="width:40px;height:40px;border-radius: var(--radius-full);background:${cfg.bg};border:2.5px solid ${cfg.color};display:flex;align-items:center;justify-content:center;font-size:18px;box-shadow:0 2px 10px rgba(0,0,0,0.4);">${cfg.icon}</div>`,
className: '', iconSize: [40,40], iconAnchor: [20,20]
});
const marker = L.marker([lat, lon], { icon }).addTo(mapInstance);
@@ -780,7 +780,7 @@ function setCityFilter(city, btn) {
function refreshMap() {
initMap();
showToast('🔄 Uppdaterar karta...');
showToast('<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Uppdaterar karta...');
}
function openMapSheet(mission) {
@@ -818,24 +818,24 @@ async function acceptFromSheet() {
const d = await r.json();
if (d.ok) {
setActiveMission(m);
showToast(' Uppdrag accepterat! Kr ' + m.reward_sek?.toLocaleString('sv-SE'));
showToast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Uppdrag accepterat! Kr ' + m.reward_sek?.toLocaleString('sv-SE'));
allMapMissions = allMapMissions.filter(x => x.id !== m.id);
renderMapMarkers();
} else {
showToast('⚠️ ' + (d.error || 'Fel vid accepterande'));
showToast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> ' + (d.error || 'Fel vid accepterande'));
}
} catch {
// Offline demo fallback
setActiveMission(m);
showToast(' Uppdrag accepterat (offline)! Kr ' + m.reward_sek?.toLocaleString('sv-SE'));
showToast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Uppdrag accepterat (offline)! Kr ' + m.reward_sek?.toLocaleString('sv-SE'));
allMapMissions = allMapMissions.filter(x => x.id !== m.id);
renderMapMarkers();
}
}
// ══════════════════════════════════════════════════════════════════════════
//
// MINA UPPDRAG TAB
// ══════════════════════════════════════════════════════════════════════════
//
async function loadMinaMissions() {
renderActiveBanner();
@@ -885,7 +885,7 @@ function renderMyMissions() {
const el = document.getElementById('mina-list');
if (!list.length) {
el.innerHTML = `<div class="empty-state"><div class="empty-icon">📋</div><div class="empty-title">Inga uppdrag</div><div class="empty-sub">Öppna Karta-fliken och acceptera ett uppdrag för att komma igång!</div></div>`;
el.innerHTML = `<div class="empty-state"><div class="empty-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="empty-title">Inga uppdrag</div><div class="empty-sub">Öppna Karta-fliken och acceptera ett uppdrag för att komma igång!</div></div>`;
return;
}
el.innerHTML = list.map(m => {
@@ -899,13 +899,13 @@ function renderMyMissions() {
<div class="mc-icon" style="background:${cfg.bg}">${cfg.icon}</div>
<div class="mc-body">
<div class="mc-title">${m.title}</div>
<div class="mc-loc">📍 ${m.location?.address || m.location?.city || 'Okänd plats'}${deadline ? ' · ⏰ '+deadline : ''}</div>
<div class="mc-loc"> ${m.location?.address || m.location?.city || 'Okänd plats'}${deadline ? ' · ⏰ '+deadline : ''}</div>
</div>
<div class="mc-reward">${(m.reward_sek||0).toLocaleString('sv-SE')}<small> kr</small></div>
</div>
<div class="mc-bottom">
<span class="badge ${statusCls}">${statusLbl}</span>
<div class="mc-cta">${isActive ? '⚡ Pågår' : m.status === 'completed' ? '✓ Klart' : 'Detaljer'} </div>
<div class="mc-cta">${isActive ? '<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år' : m.status === 'completed' ? '<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> Klart' : 'Detaljer'} </div>
</div>
</div>`;
}).join('');
@@ -917,14 +917,14 @@ function renderActiveBanner() {
if (!m) { banner.classList.remove('show'); return; }
banner.classList.add('show');
document.getElementById('ab-title').textContent = m.title;
document.getElementById('ab-loc').textContent = '📍 ' + (m.location?.address || m.location?.city || '—') + ' · Kr ' + (m.reward_sek||0).toLocaleString('sv-SE');
document.getElementById('ab-loc').textContent = ' ' + (m.location?.address || m.location?.city || '—') + ' · Kr ' + (m.reward_sek||0).toLocaleString('sv-SE');
}
function openMissionDetail(id) {
const m = myMissions.find(x => x.id === id) || (getActiveMission()?.id === id ? getActiveMission() : null);
if (!m) return;
if (m.status === 'open') {
if (getActiveMission()) { showToast('⚠️ Du har redan ett aktivt uppdrag'); return; }
if (getActiveMission()) { showToast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Du har redan ett aktivt uppdrag'); return; }
if (confirm(`Acceptera "${m.title}" för ${(m.reward_sek||0).toLocaleString('sv-SE')} kr?`)) {
acceptMission(m);
}
@@ -943,12 +943,12 @@ async function acceptMission(m) {
const d = await r.json();
if (d.ok || true) {
setActiveMission(m);
showToast(' Uppdrag accepterat!');
showToast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Uppdrag accepterat!');
loadMinaMissions();
}
} catch {
setActiveMission(m);
showToast(' Uppdrag accepterat!');
showToast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Uppdrag accepterat!');
loadMinaMissions();
}
}
@@ -967,14 +967,14 @@ async function completeActiveMission() {
if (d.ok || d.pay_released) {
setActiveMission(null);
uploadedMediaUrls = [];
showToast('🎉 Uppdrag slutfört! Kr ' + (m.reward_sek||0).toLocaleString('sv-SE') + ' utbetalas');
showToast(' Uppdrag slutfört! Kr ' + (m.reward_sek||0).toLocaleString('sv-SE') + ' utbetalas');
loadMinaMissions();
return;
}
} catch { /* fallthrough */ }
setActiveMission(null);
uploadedMediaUrls = [];
showToast('🎉 Uppdrag slutfört! Betalning på väg.');
showToast(' Uppdrag slutfört! Betalning på väg.');
loadMinaMissions();
}
@@ -985,7 +985,7 @@ function cancelActiveMission() {
loadMinaMissions();
}
// ── Upload sheet ─────────────────────────────────────────────────────────
// Upload sheet
function openUploadSheet() {
document.getElementById('upload-sheet').classList.add('show');
document.getElementById('upload-overlay').style.display = 'block';
@@ -1006,7 +1006,7 @@ document.getElementById('file-input').addEventListener('change', function() {
uploadedMediaUrls.push(fakeUploadUrl);
});
renderUploadPreviews();
if (files.length) showToast('📎 ' + files.length + ' fil(er) bifogade');
if (files.length) showToast(' ' + files.length + ' fil(er) bifogade');
});
function renderUploadPreviews() {
const el = document.getElementById('upload-previews');
@@ -1015,18 +1015,18 @@ function renderUploadPreviews() {
return;
}
el.innerHTML = uploadedMediaUrls.map((u,i) =>
`<div class="preview-thumb" title="${u}">📷</div>`
`<div class="preview-thumb" title="${u}"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg></div>`
).join('') + `<div class="preview-thumb" style="cursor:pointer;font-size:20px" onclick="triggerFileInput()">+</div>`;
}
function submitMedia() {
if (!uploadedMediaUrls.length) { showToast('⚠️ Välj minst en fil'); return; }
if (!uploadedMediaUrls.length) { showToast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Välj minst en fil'); return; }
closeUploadSheet();
showToast('📤 Media uppladdad! Klicka Slutför när du är klar.');
showToast(' Media uppladdad! Klicka Slutför när du är klar.');
}
// ══════════════════════════════════════════════════════════════════════════
//
// INTÄKTER TAB
// ══════════════════════════════════════════════════════════════════════════
//
async function loadEarnings() {
try {
@@ -1059,9 +1059,9 @@ function renderTransactions(txs) {
</div>`).join('');
}
// ══════════════════════════════════════════════════════════════════════════
//
// LEADERBOARD TAB
// ══════════════════════════════════════════════════════════════════════════
//
async function loadLeaderboard() {
try {
@@ -1085,25 +1085,25 @@ function renderLbTotals() {
if (!lbData?.platform_totals) return;
const t = lbData.platform_totals;
document.getElementById('lb-totals').innerHTML = `
<div class="lb-total-chip">🎯 ${t.missions_completed?.toLocaleString('sv-SE')} uppdrag</div>
<div class="lb-total-chip">💰 ${(t.total_paid_out_sek/1000).toFixed(0)}k kr utbetalt</div>
<div class="lb-total-chip">👤 ${t.registered_zoomers} zoomers</div>
<div class="lb-total-chip">🏙 ${t.cities_live} städer</div>`;
<div class="lb-total-chip"> ${t.missions_completed?.toLocaleString('sv-SE')} uppdrag</div>
<div class="lb-total-chip"> ${(t.total_paid_out_sek/1000).toFixed(0)}k kr utbetalt</div>
<div class="lb-total-chip"> ${t.registered_zoomers} zoomers</div>
<div class="lb-total-chip"> ${t.cities_live} städer</div>`;
}
const ZOOMER_AVATARS = ['🦅','🦊','🐺','🦁','🐯','🦝','🦋','🦈'];
const ZOOMER_AVATARS = ['','','','','','','',''];
function renderLbZoomers() {
const list = (lbData?.top_zoomers || DEMO_LEADERBOARD.top_zoomers);
document.getElementById('lb-zoomers-list').innerHTML = list.map((z,i) => {
const rankCls = i===0?'rank-1':i===1?'rank-2':i===2?'rank-3':'rank-n';
const rankTxt = i===0?'🥇':i===1?'🥈':i===2?'🥉':'#'+(i+1);
const rankTxt = i===0?'':i===1?'':i===2?'':'#'+(i+1);
const bg = i===0?'rgba(255,215,0,0.15)':i===1?'rgba(192,192,192,0.12)':i===2?'rgba(205,127,50,0.12)':'var(--fill-tertiary)';
return `<div class="lb-row">
<div class="lb-rank ${rankCls}">${rankTxt}</div>
<div class="lb-avatar" style="background:${bg}">${ZOOMER_AVATARS[i%ZOOMER_AVATARS.length]}</div>
<div class="lb-body">
<div class="lb-name">${z.name}</div>
<div class="lb-meta">📍 ${z.city} · ${z.rating} · ${z.skill_level}</div>
<div class="lb-meta"> ${z.city} · ${z.rating} · ${z.skill_level}</div>
</div>
<div class="lb-right">
<div class="lb-missions" style="color:${i===0?'#FFD700':i===1?'#C0C0C0':i===2?'#CD7F32':'var(--label-primary)'}">${z.missions_completed}</div>
@@ -1113,15 +1113,15 @@ function renderLbZoomers() {
}).join('');
}
const CITY_ICONS = { Stockholm:'🏙', Göteborg:'', Malmö:'🌉', Uppsala:'🏛', Västerås:'⚙️' };
const CITY_ICONS = { Stockholm:'', Göteborg:'', Malmö:'', Uppsala:'', Västerås:'<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>' };
function renderLbCities() {
const list = (lbData?.top_cities || DEMO_LEADERBOARD.top_cities);
const maxM = list[0]?.missions_completed || 1;
document.getElementById('lb-cities-list').innerHTML = list.map((c,i) => {
const pct = Math.round((c.missions_completed / maxM) * 100);
return `<div class="city-row">
<div class="city-rank" style="color:${i===0?'#FFD700':i===1?'#C0C0C0':i===2?'#CD7F32':'var(--label-tertiary)'}">${i===0?'🥇':i===1?'🥈':i===2?'🥉':'#'+(i+1)}</div>
<div class="city-icon">${CITY_ICONS[c.city]||'🏙'}</div>
<div class="city-rank" style="color:${i===0?'#FFD700':i===1?'#C0C0C0':i===2?'#CD7F32':'var(--label-tertiary)'}">${i===0?'':i===1?'':i===2?'':'#'+(i+1)}</div>
<div class="city-icon">${CITY_ICONS[c.city]||''}</div>
<div class="city-body">
<div class="city-name">${c.city}</div>
<div style="margin-top:6px;max-width:160px;">
@@ -1134,13 +1134,13 @@ function renderLbCities() {
}).join('');
}
// ── Helpers ────────────────────────────────────────────────────────────────
// Helpers
function getDemoToken() {
const payload = btoa(JSON.stringify({ sub: DEMO_ZOOMER_ID, email: 'erik@demo.se', iat: Date.now() }));
return 'eyJhbGciOiJIUzI1NiJ9.' + payload + '.demo-sig';
}
// ── Demo data ──────────────────────────────────────────────────────────────
// Demo data
const DEMO_MISSIONS = [
{ id:'qx-m-001', title:'Fastighetsfilm — Lidingö villa', description:'Professionell fastighetsfilm (4K) av villa.', location:{city:'Stockholm',address:'Lidingövägen 12',lat:59.3664,lon:18.1499}, reward_sek:2800, deadline:'2026-06-05T17:00:00Z', mission_type:'video', skill_required:'professional', status:'open' },
{ id:'qx-m-002', title:'Drönarbild — Kista Science City', description:'Aerial 4K-film av kontorspark. Tillstånd finns.', location:{city:'Stockholm',address:'Kistagången 20',lat:59.4044,lon:17.9491}, reward_sek:3200, deadline:'2026-06-08T16:00:00Z', mission_type:'drone', skill_required:'drone', status:'open' },
@@ -1155,13 +1155,13 @@ const DEMO_MISSIONS = [
const DEMO_EARNINGS = { available_sek: 0, pending_sek: 0, this_month_sek: 0, missions_completed: 0, total_earned_sek: 0 };
const DEMO_TX = [
{ type:'credit', icon:'💰', title:'Drönarbild — Gamla Stan', date:'Idag · automatisk utbetalning', amount:2800 },
{ type:'credit', icon:'💰', title:'Butiksfilm — Nordstan', date:'Igår 16:40', amount:2400 },
{ type:'credit', icon:'', title:'Drönarbild — Gamla Stan', date:'Idag · automatisk utbetalning', amount:2800 },
{ type:'credit', icon:'', title:'Butiksfilm — Nordstan', date:'Igår 16:40', amount:2400 },
{ type:'pending', icon:'⏳', title:'Hotellfilm Grand (granskas)', date:'2 dagar sedan · kunden granskar', amount:7200 },
{ type:'payout', icon:'↑', title:'Utbetalning via Stripe', date:'19 maj 2026', amount:9800 },
{ type:'credit', icon:'💰', title:'Event-foto Berns Salonger', date:'17 maj 2026', amount:5500 },
{ type:'credit', icon:'💰', title:'Campus-film Uppsala', date:'15 maj 2026', amount:5200 },
{ type:'credit', icon:'💰', title:'Restaurangfoto Vollmers', date:'12 maj 2026', amount:4800 },
{ type:'credit', icon:'', title:'Event-foto Berns Salonger', date:'17 maj 2026', amount:5500 },
{ type:'credit', icon:'', title:'Campus-film Uppsala', date:'15 maj 2026', amount:5200 },
{ type:'credit', icon:'', title:'Restaurangfoto Vollmers', date:'12 maj 2026', amount:4800 },
{ type:'payout', icon:'↑', title:'Utbetalning via Stripe', date:'1 maj 2026', amount:14400 },
];
@@ -1171,7 +1171,7 @@ const DEMO_LEADERBOARD = {
top_cities: [],
};
// ── Init ───────────────────────────────────────────────────────────────────
// Init
updateMinaDot();
initMap();
</script>
+26 -26
View File
@@ -87,7 +87,7 @@
}
.section-card {
background: var(--background-secondary);
border-radius: 14px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
@@ -99,7 +99,7 @@
}
.payout-row:last-child { border-bottom: none; }
.payout-icon {
width: 38px; height: 38px; border-radius: 10px;
width: 38px; height: 38px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 18px; flex-shrink: 0;
}
@@ -111,7 +111,7 @@
/* Connect card */
.connect-card {
background: var(--background-secondary);
border-radius: 14px;
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
overflow: hidden;
}
@@ -120,7 +120,7 @@
display: flex; gap: 14px; align-items: center;
}
.connect-icon {
width: 48px; height: 48px; border-radius: 12px;
width: 48px; height: 48px; border-radius: var(--radius-md);
background: rgba(0,122,255,0.15);
display: flex; align-items: center; justify-content: center;
font-size: 24px; flex-shrink: 0;
@@ -145,7 +145,7 @@
display: flex; align-items: center; gap: 6px;
}
.status-dot {
width: 8px; height: 8px; border-radius: 50%;
width: 8px; height: 8px; border-radius: var(--radius-full);
}
.status-dot.active { background: var(--ios-green); }
.status-dot.pending { background: var(--ios-orange); }
@@ -164,7 +164,7 @@
/* Loader */
.loader { display:flex; justify-content:center; padding:40px; }
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { width:28px; height:28px; border-radius:50%; border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; }
.spinner { width:28px; height:28px; border-radius: var(--radius-full); border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; }
/* Tab Bar */
.tab-bar {
@@ -185,7 +185,7 @@
.tab-item.active .tab-label { color: var(--ios-blue); }
/* Toast */
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius:20px; padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius: var(--radius-xl); padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
@@ -272,7 +272,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Payout account</div>
<div class="connect-card">
<div class="connect-top">
<div class="connect-icon">💳</div>
<div class="connect-icon"></div>
<div class="connect-info">
<div class="connect-title">Stripe Connect</div>
<div class="connect-desc">Verify your account to receive payouts directly to your bank</div>
@@ -283,7 +283,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
Loading account status...
</div>
<button class="connect-btn" id="connect-btn" onclick="openConnectOnboarding()">
🏦 Configure payout account →
Configure payout account →
</button>
</div>
</div>
@@ -293,7 +293,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Payout schedule</div>
<div class="section-card">
<div class="schedule-row">
<span class="schedule-label">🗓 Frekvens</span>
<span class="schedule-label"> Frekvens</span>
<span class="schedule-value">Every Monday</span>
</div>
<div class="schedule-row">
@@ -301,15 +301,15 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="schedule-value">13 bankdagar</span>
</div>
<div class="schedule-row">
<span class="schedule-label">💳 Method</span>
<span class="schedule-label"> Method</span>
<span class="schedule-value">Stripe Connect</span>
</div>
<div class="schedule-row">
<span class="schedule-label">🏦 Currency</span>
<span class="schedule-label"> Currency</span>
<span class="schedule-value">SEK</span>
</div>
<div class="schedule-row">
<span class="schedule-label">📋 Min. payout</span>
<span class="schedule-label"><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> Min. payout</span>
<span class="schedule-value">500 kr</span>
</div>
</div>
@@ -321,15 +321,15 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Tab Bar -->
<nav class="tab-bar">
<a class="tab-item" href="zoomer-app.html">
<span class="tab-icon">🎯</span>
<span class="tab-icon"></span>
<span class="tab-label">Missions</span>
</a>
<a class="tab-item active" href="zoomer-earnings.html">
<span class="tab-icon">💰</span>
<span class="tab-icon"></span>
<span class="tab-label">Earnings</span>
</a>
<a class="tab-item" href="zoomer-profile.html">
<span class="tab-icon">👤</span>
<span class="tab-icon"></span>
<span class="tab-label">Profile</span>
</a>
</nav>
@@ -356,8 +356,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
}
function missionTypeEmoji(type) {
const m = { facade_scan:'🏢', roof_scan:'🏠', road_inspection:'🛣️', interior_scan:'🏚️', vegetation_survey:'🌳', inspection:'🔍' };
return m[type] || '📍';
const m = { facade_scan:'<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 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg>', roof_scan:'', road_inspection:'', interior_scan:'', vegetation_survey:'', inspection:'<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>' };
return m[type] || '';
}
function missionTypeBg(type) {
@@ -397,7 +397,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (recent.length === 0) {
payoutsCard.innerHTML = `
<div style="padding:32px;text-align:center">
<div style="font-size:40px;margin-bottom:8px">💸</div>
<div style="font-size:40px;margin-bottom:8px"></div>
<div style="font-size:16px;font-weight:600;margin-bottom:4px">Inga utbetalningar än</div>
<div style="font-size:14px;color:var(--label-secondary)">Slutför ditt första uppdrag för att börja tjäna</div>
</div>`;
@@ -431,7 +431,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
console.error(e);
document.getElementById('payouts-card').innerHTML = `
<div style="padding:24px;text-align:center;color:var(--label-secondary)">
<div>⚠️ Could not load earnings data</div>
<div><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Could not load earnings data</div>
</div>`;
}
}
@@ -446,7 +446,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (r.ok) {
const data = await r.json();
statusEl.innerHTML = `<div class="status-dot active"></div> Account activated — ready for payouts`;
btnEl.textContent = '⚙️ Hantera konto →';
btnEl.textContent = '<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> Hantera konto →';
if (data.url) { btnEl.dataset.url = data.url; }
} else if (r.status === 401) {
statusEl.innerHTML = `<div class="status-dot pending"></div> Inloggning krävs för att verifiera konto`;
@@ -473,16 +473,16 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (r.ok) {
const data = await r.json();
if (data.url) { window.open(data.url, '_blank'); }
else { toast(' Konto verifierat!'); }
else { toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Konto verifierat!'); }
} else if (r.status === 401) {
toast('🔐 Behörighet saknas — kontakta support');
toast('<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7" width="10" height="7" rx="1" stroke="#666" stroke-width="1.5"/><path d="M5 7V5C5 3.3 6.3 2 8 2C9.7 2 11 3.3 11 5V7" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="10.5" r="1" fill="#666"/></svg> Behörighet saknas — kontakta support');
} else {
toast('⚠️ Stripe Connect onboarding — kontakta support@quixzoom.com');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Stripe Connect onboarding — kontakta support@quixzoom.com');
}
} catch {
toast(' Contact support to configure your payout account');
toast(' Contact support to configure your payout account');
} finally {
btn.textContent = '🏦 Configure payout account →';
btn.textContent = ' Configure payout account →';
btn.disabled = false;
}
}
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Zoomer Onboarding — quiXzoom</title>
<style>
/* ── iOS 26 / Apple Native — Light Mode ──────────────────────────────── */
/* iOS 26 / Apple Native — Light Mode */
:root {
--bg: #f2f2f7;
--bg-card: #ffffff;
@@ -42,7 +42,7 @@
overflow-x: hidden;
}
/* ── Screen wrapper ───────────────────────────────────────────────────── */
/* Screen wrapper */
.screen {
display: none;
min-height: 100dvh;
@@ -62,7 +62,7 @@
.screen.slide-in-right { animation: slideInRight 0.32s cubic-bezier(0.32,0.72,0,1) forwards; }
.screen.slide-in-left { animation: slideInLeft 0.32s cubic-bezier(0.32,0.72,0,1) forwards; }
/* ── Nav Bar ─────────────────────────────────────────────────────────── */
/* Nav Bar */
.nav-bar {
position: sticky;
top: 0;
@@ -101,7 +101,7 @@
}
.nav-spacer { width: 60px; }
/* ── Progress Indicator ──────────────────────────────────────────────── */
/* Progress Indicator */
.progress-steps {
display: flex;
justify-content: center;
@@ -110,7 +110,7 @@
}
.step-dot {
height: 4px;
border-radius: 2px;
border-radius: var(--radius-sm);
background: var(--fill);
transition: all 0.3s ease;
}
@@ -118,16 +118,25 @@
.step-dot.done { background: var(--green); width: 12px; }
.step-dot.pending { width: 8px; }
/* ── Hero Area ────────────────────────────────────────────────────────── */
/* Hero Area */
.hero {
padding: 32px 28px 20px;
text-align: center;
}
.hero-emoji {
font-size: 64px;
line-height: 1;
margin-bottom: 16px;
display: block;
.hero-icon {
width: 64px;
height: 64px;
margin: 0 auto 16px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-lg);
background: linear-gradient(135deg, var(--tint), #5AC8FA);
color: #fff;
}
.hero-icon svg {
width: 32px;
height: 32px;
}
.hero-title {
font-size: 28px;
@@ -143,14 +152,14 @@
line-height: 1.5;
}
/* ── Content Area ────────────────────────────────────────────────────── */
/* Content Area */
.content {
flex: 1;
padding: 0 16px;
overflow-y: auto;
}
/* ── Card ─────────────────────────────────────────────────────────────── */
/* Card */
.card {
background: var(--bg-card);
border-radius: var(--radius-lg);
@@ -159,7 +168,7 @@
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
/* ── Form fields ─────────────────────────────────────────────────────── */
/* Form fields */
.field-row {
display: flex;
flex-direction: column;
@@ -189,7 +198,7 @@
.field-value::placeholder { color: var(--label-tertiary); }
.field-value::-webkit-date-and-time-value { text-align: left; }
/* ── Toggle row ──────────────────────────────────────────────────────── */
/* Toggle row */
.toggle-row {
display: flex;
align-items: center;
@@ -205,7 +214,7 @@
width: 51px;
height: 31px;
background: #e5e5ea;
border-radius: 16px;
border-radius: var(--radius-lg);
position: relative;
cursor: pointer;
transition: background 0.25s;
@@ -221,14 +230,14 @@
position: absolute;
top: 2px; left: 2px;
width: 27px; height: 27px;
border-radius: 50%;
border-radius: var(--radius-full);
background: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.22);
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.ios-toggle.on::after { transform: translateX(20px); }
/* ── Info row ────────────────────────────────────────────────────────── */
/* Info row */
.info-row {
display: flex;
align-items: center;
@@ -239,7 +248,7 @@
.info-row:last-child { border-bottom: none; }
.info-icon {
width: 36px; height: 36px;
border-radius: 10px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 18px;
flex-shrink: 0;
@@ -249,7 +258,7 @@
.info-sub { font-size: 13px; color: var(--label-secondary); margin-top: 2px; }
.info-chevron { color: var(--label-tertiary); font-size: 14px; }
/* ── Upload zone ──────────────────────────────────────────────────────── */
/* Upload zone */
.upload-zone {
border: 2px dashed rgba(0,122,255,0.35);
border-radius: var(--radius);
@@ -268,7 +277,7 @@
.upload-title { font-size: 15px; font-weight: 600; color: var(--tint); }
.upload-sub { font-size: 13px; color: var(--label-secondary); }
/* ── Summary list ────────────────────────────────────────────────────── */
/* Summary list */
.summary-row {
display: flex;
justify-content: space-between;
@@ -280,7 +289,7 @@
.summary-key { font-size: 15px; color: var(--label-secondary); }
.summary-val { font-size: 15px; font-weight: 500; color: var(--label); }
/* ── Mission card ────────────────────────────────────────────────────── */
/* Mission card */
.mission-card {
background: var(--bg-card);
border-radius: var(--radius-lg);
@@ -307,7 +316,7 @@
.mission-detail-key { font-size: 14px; color: var(--label-secondary); }
.mission-detail-val { font-size: 15px; font-weight: 600; color: var(--label); }
/* ── CTA / Primary Button ────────────────────────────────────────────── */
/* CTA / Primary Button */
.cta-area {
padding: 16px 16px calc(var(--safe-bottom) + 24px);
background: transparent;
@@ -348,7 +357,7 @@
}
.btn-secondary:active { opacity: 0.5; }
/* ── BankID specific ─────────────────────────────────────────────────── */
/* BankID specific */
.bankid-frame {
margin: 12px 16px;
background: var(--bg-card);
@@ -361,7 +370,7 @@
width: 72px;
height: 72px;
background: linear-gradient(135deg, #235971, #4E9CC0);
border-radius: 18px;
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
@@ -380,20 +389,20 @@
line-height: 1.5;
}
/* ── Spinner ──────────────────────────────────────────────────────────── */
/* Spinner */
@keyframes spin { to { transform: rotate(360deg); } }
.spinner-inline {
display: inline-block;
width: 20px; height: 20px;
border: 2px solid rgba(255,255,255,0.4);
border-top-color: #fff;
border-radius: 50%;
border-radius: var(--radius-full);
animation: spin 0.7s linear infinite;
vertical-align: middle;
margin-right: 8px;
}
/* ── Success screen ──────────────────────────────────────────────────── */
/* Success screen */
@keyframes successPop {
0% { transform: scale(0.4); opacity: 0; }
70% { transform: scale(1.15); }
@@ -406,7 +415,7 @@
}
.success-circle {
width: 96px; height: 96px;
border-radius: 50%;
border-radius: var(--radius-full);
background: var(--green);
display: flex; align-items: center; justify-content: center;
font-size: 48px;
@@ -414,7 +423,7 @@
animation: successPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
/* ── Equipment chips ─────────────────────────────────────────────────── */
/* Equipment chips */
.chip-grid {
display: flex;
flex-wrap: wrap;
@@ -442,7 +451,7 @@
.equip-chip-icon { font-size: 22px; }
.equip-chip-label { font-size: 14px; font-weight: 500; color: var(--label); }
/* ── Section label ───────────────────────────────────────────────────── */
/* Section label */
.form-section-title {
font-size: 13px;
font-weight: 600;
@@ -495,11 +504,13 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<body>
<h1 style="position:absolute;left:-9999px;">Zoomer Onboarding Flow</h1>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 1: Welcome ────────────────────────────────────────────────── -->
<!-- -->
<!-- SCREEN 1: Welcome -->
<div class="screen active" id="screen-1">
<div class="hero" style="padding-top: calc(var(--safe-top) + 48px)">
<span class="hero-emoji"></span>
<div class="hero-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
</div>
<div class="hero-title">Become a Zoomer</div>
<div class="hero-sub">Earn money with your photography skills. Accept missions, deliver results, get paid fast.</div>
</div>
@@ -507,7 +518,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="content">
<div class="card">
<div class="info-row">
<div class="info-icon" style="background:#007AFF22">📸</div>
<div class="info-icon" style="background:#007AFF22">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#007AFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
</div>
<div class="info-body">
<div class="info-title">Photograph missions</div>
<div class="info-sub">Shops, products, events in your city</div>
@@ -515,7 +528,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="info-chevron"></span>
</div>
<div class="info-row">
<div class="info-icon" style="background:#34C75922">💰</div>
<div class="info-icon" style="background:#34C75922">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#34C759" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
</div>
<div class="info-body">
<div class="info-title">Fast payout</div>
<div class="info-sub">Direct to your account via Stripe Connect</div>
@@ -523,7 +538,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="info-chevron"></span>
</div>
<div class="info-row">
<div class="info-icon" style="background:#FF950022">🗺️</div>
<div class="info-icon" style="background:#FF950022">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#FF9500" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10z"/></svg>
</div>
<div class="info-body">
<div class="info-title">Your choice</div>
<div class="info-sub">Only accept missions that suit you</div>
@@ -539,8 +556,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 2: BankID Verification ────────────────────────────────────── -->
<!-- -->
<!-- SCREEN 2: BankID Verification -->
<div class="screen" id="screen-2">
<div class="nav-bar">
<button class="nav-back" onclick="goBack(1)"><span class="nav-back-arrow"></span> Back</button>
@@ -551,13 +568,17 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="content">
<div class="hero" style="padding-top:20px">
<span class="hero-emoji">🏦</span>
<div class="hero-icon" style="background: linear-gradient(135deg, #235971, #4E9CC0);">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<div class="hero-title">Verify with BankID</div>
<div class="hero-sub">We need to verify your identity so you can receive payments.</div>
</div>
<div class="bankid-frame">
<div class="bankid-logo">🔐</div>
<div class="bankid-logo">
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<div class="bankid-title">Open the BankID app</div>
<div class="bankid-sub">Tap the button below to start BankID sign-in on this device.</div>
</div>
@@ -577,8 +598,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 3: Profile Setup ──────────────────────────────────────────── -->
<!-- -->
<!-- SCREEN 3: Profile Setup -->
<div class="screen" id="screen-3">
<div class="nav-bar">
<button class="nav-back" onclick="goBack(2)"><span class="nav-back-arrow"></span> Back</button>
@@ -589,7 +610,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="content">
<div class="hero" style="padding-top:20px">
<span class="hero-emoji">👤</span>
<div class="hero-icon" style="background: linear-gradient(135deg, #AF52DE, #5856D6);">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</div>
<div class="hero-title">Tell us about yourself</div>
<div class="hero-sub">Clients see your profile when choosing a Zoomer for their missions.</div>
</div>
@@ -631,8 +654,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 4: Equipment Declaration ──────────────────────────────────── -->
<!-- -->
<!-- SCREEN 4: Equipment Declaration -->
<div class="screen" id="screen-4">
<div class="nav-bar">
<button class="nav-back" onclick="goBack(3)"><span class="nav-back-arrow"></span> Back</button>
@@ -643,28 +666,38 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="content">
<div class="hero" style="padding-top:20px">
<span class="hero-emoji">📷</span>
<div class="hero-icon" style="background: linear-gradient(135deg, #FF9500, #FFCC00);">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
</div>
<div class="hero-title">What equipment do you have?</div>
<div class="hero-sub">Select all equipment you can use on missions. More options more missions.</div>
<div class="hero-sub">Select all equipment you can use on missions. More options means more missions.</div>
</div>
<div class="form-section-title">Camera</div>
<div class="card">
<div class="chip-grid">
<div class="equip-chip selected" onclick="toggleChip(this)">
<span class="equip-chip-icon">📱</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
</span>
<span class="equip-chip-label">Smartphone</span>
</div>
<div class="equip-chip" onclick="toggleChip(this)">
<span class="equip-chip-icon">📸</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
</span>
<span class="equip-chip-label">Mirrorless</span>
</div>
<div class="equip-chip" onclick="toggleChip(this)">
<span class="equip-chip-icon">🎥</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
</span>
<span class="equip-chip-label">DSLR</span>
</div>
<div class="equip-chip" onclick="toggleChip(this)">
<span class="equip-chip-icon">🚁</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</span>
<span class="equip-chip-label">Drone</span>
</div>
</div>
@@ -674,15 +707,21 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="card">
<div class="chip-grid">
<div class="equip-chip" onclick="toggleChip(this)">
<span class="equip-chip-icon">🕯️</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</span>
<span class="equip-chip-label">Flash</span>
</div>
<div class="equip-chip" onclick="toggleChip(this)">
<span class="equip-chip-icon">🔦</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2"/><path d="M12 21v2"/><path d="M4.22 4.22l1.42 1.42"/><path d="M18.36 18.36l1.42 1.42"/><path d="M1 12h2"/><path d="M21 12h2"/><path d="M4.22 19.78l1.42-1.42"/><path d="M18.36 5.64l1.42-1.42"/></svg>
</span>
<span class="equip-chip-label">Lighting</span>
</div>
<div class="equip-chip" onclick="toggleChip(this)">
<span class="equip-chip-icon">📐</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</span>
<span class="equip-chip-label">Tripod</span>
</div>
</div>
@@ -712,8 +751,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 5: KYC Document Upload ────────────────────────────────────── -->
<!-- -->
<!-- SCREEN 5: KYC Document Upload -->
<div class="screen" id="screen-5">
<div class="nav-bar">
<button class="nav-back" onclick="goBack(4)"><span class="nav-back-arrow"></span> Back</button>
@@ -724,7 +763,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="content">
<div class="hero" style="padding-top:20px">
<span class="hero-emoji">🪪</span>
<div class="hero-icon" style="background: linear-gradient(135deg, #34C759, #30D158);">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<div class="hero-title">Upload ID document</div>
<div class="hero-sub">Required by law to verify your identity and enable payouts.</div>
</div>
@@ -732,7 +773,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="form-section-title">Accepted documents</div>
<div class="card">
<div class="info-row" onclick="markDoc('passport', this)" style="cursor:pointer">
<div class="info-icon" style="background:#007AFF22" id="doc-passport-icon">📘</div>
<div class="info-icon" style="background:#007AFF22" id="doc-passport-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#007AFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<div class="info-body">
<div class="info-title">Passport</div>
<div class="info-sub">Valid passport with photo page</div>
@@ -740,7 +783,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="info-chevron" id="doc-passport-check"></span>
</div>
<div class="info-row" onclick="markDoc('id', this)" style="cursor:pointer">
<div class="info-icon" style="background:#AF52DE22" id="doc-id-icon">🪪</div>
<div class="info-icon" style="background:#AF52DE22" id="doc-id-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#AF52DE" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<div class="info-body">
<div class="info-title">National ID card</div>
<div class="info-sub">Front and back</div>
@@ -748,7 +793,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="info-chevron" id="doc-id-check"></span>
</div>
<div class="info-row" onclick="markDoc('license', this)" style="cursor:pointer">
<div class="info-icon" style="background:#34C75922" id="doc-license-icon">🚗</div>
<div class="info-icon" style="background:#34C75922" id="doc-license-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#34C759" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<div class="info-body">
<div class="info-title">Driving licence</div>
<div class="info-sub">Valid driving licence</div>
@@ -758,9 +805,11 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="upload-zone" onclick="simulateUpload()">
<span class="upload-icon" id="upload-icon">📎</span>
<span class="upload-icon" id="upload-icon">
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#007AFF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
</span>
<span class="upload-title" id="upload-title">Choose file or take photo</span>
<span class="upload-sub" id="upload-sub">JPG, PNG eller PDF · Max 10 MB</span>
<span class="upload-sub" id="upload-sub">JPG, PNG or PDF · Max 10 MB</span>
</div>
<div class="form-section-title">Bank details</div>
@@ -781,8 +830,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 6: First Mission ──────────────────────────────────────────── -->
<!-- -->
<!-- SCREEN 6: First Mission -->
<div class="screen" id="screen-6">
<div class="nav-bar">
<button class="nav-back" onclick="goBack(5)"><span class="nav-back-arrow"></span> Back</button>
@@ -793,16 +842,21 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="content" style="padding:0">
<div class="hero" style="padding-top:20px">
<span class="hero-emoji">🎯</span>
<div class="hero-icon" style="background: linear-gradient(135deg, #FF3B30, #FF6B6B);">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
</div>
<div class="hero-title">Ready for your first mission?</div>
<div class="hero-sub">We found a mission near you!</div>
</div>
<div class="mission-card">
<div class="mission-header">
<div class="mission-type">📸 Store photography</div>
<div class="mission-type">Store photography</div>
<div class="mission-title">H&M Flagship — Stockholm</div>
<div class="mission-loc">📍 Drottninggatan 55, Stockholm · 1.2 km away</div>
<div class="mission-loc">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:middle;margin-right:4px"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
Drottninggatan 55, Stockholm · 1.2 km away
</div>
</div>
<div class="mission-detail">
<span class="mission-detail-key">Compensation</span>
@@ -834,7 +888,10 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="summary-row">
<span class="summary-key">Rating</span>
<span class="summary-val">⭐ 4.9 (47 missions)</span>
<span class="summary-val">
<svg width="14" height="14" viewBox="0 0 24 24" fill="#FF9500" stroke="#FF9500" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:middle;margin-right:4px"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
4.9 (47 missions)
</span>
</div>
<div class="summary-row">
<span class="summary-key">Payout</span>
@@ -845,21 +902,26 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="cta-area">
<button class="btn-primary green" onclick="goTo(7)">✓ Accept mission</button>
<button class="btn-primary green" onclick="goTo(7)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:middle;margin-right:6px"><polyline points="20 6 9 17 4 12"/></svg>
Accept mission
</button>
<button class="btn-secondary" onclick="goTo(7)">View more missions</button>
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 7: Success ────────────────────────────────────────────────── -->
<!-- -->
<!-- SCREEN 7: Success -->
<div class="screen" id="screen-7">
<div class="nav-bar" style="justify-content:center">
<div class="nav-title-center" style="color:var(--green)">🎉 Welcome!</div>
<div class="nav-title-center" style="color:var(--green)">Welcome!</div>
</div>
<div class="content" style="display:flex; flex-direction:column; align-items:center; text-align:center; padding-top:0">
<div class="success-icon-wrap">
<div class="success-circle"></div>
<div class="success-circle">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
</div>
<div style="padding:0 28px 20px">
@@ -871,15 +933,24 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="card">
<div class="summary-row">
<span class="summary-key">Status</span>
<span class="summary-val" style="color:var(--green)">✓ Active Zoomer</span>
<span class="summary-val" style="color:var(--green);display:flex;align-items:center;gap:6px">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#34C759" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
Active Zoomer
</span>
</div>
<div class="summary-row">
<span class="summary-key">BankID</span>
<span class="summary-val" style="color:var(--green)">✓ Verifierad</span>
<span class="summary-val" style="color:var(--green);display:flex;align-items:center;gap:6px">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#34C759" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
Verified
</span>
</div>
<div class="summary-row">
<span class="summary-key">KYC</span>
<span class="summary-val" style="color:var(--yellow)">⏳ Review in progress</span>
<span class="summary-val" style="color:var(--yellow);display:flex;align-items:center;gap:6px">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#FF9500" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Review in progress
</span>
</div>
<div class="summary-row">
<span class="summary-key">First mission</span>
@@ -893,7 +964,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div style="padding:24px 28px 0; color:var(--label-secondary); font-size:14px; line-height:1.6">
📲 You will receive a confirmation email. Open the quiXzoom app to see details about your mission.
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:middle;margin-right:4px"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
You will receive a confirmation email. Open the quiXzoom app to see details about your mission.
</div>
</div>
@@ -904,12 +976,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<script>
// ── State ──────────────────────────────────────────────────────────────
// State
const TOTAL_STEPS = 6; // screens 2-7
let currentScreen = 1;
let bankIDTimer = null;
// ── Navigation ─────────────────────────────────────────────────────────
// Navigation
function goTo(n, direction = 'right') {
const curr = document.getElementById(`screen-${currentScreen}`);
const next = document.getElementById(`screen-${n}`);
@@ -930,7 +1002,7 @@ function goBack(n) {
goTo(n, 'left');
}
// ── Step dots ──────────────────────────────────────────────────────────
// Step dots
function renderSteps(screenN) {
if (screenN < 2) return;
const step = screenN - 1; // step 1..6
@@ -943,7 +1015,7 @@ function renderSteps(screenN) {
}).join('');
}
// ── BankID simulation ──────────────────────────────────────────────────
// BankID simulation
function startBankID() {
const btn = document.getElementById('bankid-btn');
btn.innerHTML = '<span class="spinner-inline"></span>Waiting for BankID…';
@@ -958,32 +1030,32 @@ function startBankID() {
}, 2500);
}
// ── Equipment chip toggle ──────────────────────────────────────────────
// Equipment chip toggle
function toggleChip(el) {
el.classList.toggle('selected');
}
// ── Document selection ─────────────────────────────────────────────────
// Document selection
let selectedDoc = null;
function markDoc(type, row) {
['passport','id','license'].forEach(t => {
document.getElementById(`doc-${t}-check`).textContent = '';
});
selectedDoc = type;
document.getElementById(`doc-${type}-check`).textContent = '';
document.getElementById(`doc-${type}-check`).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>';
}
// ── Upload simulation ──────────────────────────────────────────────────
// Upload simulation
function simulateUpload() {
const icon = document.getElementById('upload-icon');
const title = document.getElementById('upload-title');
const sub = document.getElementById('upload-sub');
icon.textContent = '';
icon.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
title.textContent = 'File uploaded';
sub.textContent = 'id_dokument.jpg · 2.4 MB';
}
// ── Init ──────────────────────────────────────────────────────────────
// Init
renderSteps(1);
</script>
</body>
+73 -73
View File
@@ -37,7 +37,7 @@
padding: 32px 20px 24px;
}
.avatar {
width: 88px; height: 88px; border-radius: 50%;
width: 88px; height: 88px; border-radius: var(--radius-full);
background: linear-gradient(135deg, var(--ios-blue), var(--ios-indigo));
display: flex; align-items: center; justify-content: center;
font-size: 42px; margin-bottom: 14px;
@@ -45,7 +45,7 @@
}
.avatar-badge {
position: absolute; bottom: 0; right: 0;
width: 26px; height: 26px; border-radius: 50%;
width: 26px; height: 26px; border-radius: var(--radius-full);
border: 2.5px solid var(--grouped-background-primary);
display: flex; align-items: center; justify-content: center;
font-size: 14px;
@@ -76,7 +76,7 @@
display: grid; grid-template-columns: 1fr 1fr 1fr;
margin: 0 16px 20px;
background: var(--background-secondary);
border-radius: 14px;
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
overflow: hidden;
}
@@ -100,7 +100,7 @@
}
.section-card {
background: var(--background-secondary);
border-radius: 14px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
@@ -114,7 +114,7 @@
.list-row:last-child { border-bottom: none; }
.list-row:active { opacity: 0.7; }
.row-icon-wrap {
width: 32px; height: 32px; border-radius: 8px;
width: 32px; height: 32px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 18px; flex-shrink: 0;
}
@@ -130,7 +130,7 @@
/* KYC Card */
.kyc-card {
background: var(--background-secondary);
border-radius: 14px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.kyc-top {
@@ -138,7 +138,7 @@
padding: 16px;
}
.kyc-icon {
width: 52px; height: 52px; border-radius: 12px;
width: 52px; height: 52px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 26px; flex-shrink: 0;
}
@@ -158,7 +158,7 @@
}
.status-chip {
display: inline-flex; align-items: center; gap: 4px;
padding: 4px 10px; border-radius: 20px;
padding: 4px 10px; border-radius: var(--radius-xl);
font-size: 12px; font-weight: 600;
}
.chip-unverified { background: rgba(255,149,0,0.15); color: var(--ios-orange); }
@@ -190,13 +190,13 @@
.type-label { flex: 1; font-size: 14px; }
.type-count { font-size: 14px; font-weight: 600; color: var(--label-secondary); }
.type-bar-wrap { width: 80px; }
.type-bar { height: 4px; border-radius: 2px; background: var(--ios-blue); }
.type-bar { height: 4px; border-radius: var(--radius-sm); background: var(--ios-blue); }
/* Onboarding Progress Bar */
.onboarding-card {
margin: 0 16px 20px;
background: var(--background-secondary);
border-radius: 16px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.onboarding-header {
@@ -207,10 +207,10 @@
.onboarding-fraction { font-size: 13px; color: var(--label-secondary); font-weight: 500; }
.onboarding-track {
height: 5px; background: var(--fill-secondary);
margin: 0 16px 14px; border-radius: 3px; overflow: hidden;
margin: 0 16px 14px; border-radius: var(--radius-sm); overflow: hidden;
}
.onboarding-fill {
height: 100%; border-radius: 3px;
height: 100%; border-radius: var(--radius-sm);
background: linear-gradient(90deg, var(--ios-blue), var(--ios-indigo));
transition: width 0.6s cubic-bezier(0.34,1.56,0.64,1);
}
@@ -232,7 +232,7 @@
margin: 0 16px 16px;
padding: 15px;
background: var(--ios-blue); color: #fff;
border: none; border-radius: 12px;
border: none; border-radius: var(--radius-md);
font-family: var(--font-text); font-size: 16px; font-weight: 700;
cursor: pointer; transition: opacity 0.15s;
letter-spacing: -0.2px; text-align: center;
@@ -243,7 +243,7 @@
/* Earnings Calculator */
.calc-card {
background: var(--background-secondary);
border-radius: 16px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.calc-header { padding: 14px 16px 12px; border-bottom: 0.5px solid var(--separator); }
@@ -259,19 +259,19 @@
.calc-slider {
width: 100%; margin-bottom: 16px;
-webkit-appearance: none; appearance: none;
height: 4px; border-radius: 2px;
height: 4px; border-radius: var(--radius-sm);
background: var(--ios-blue); outline: none;
}
.calc-slider::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 22px; height: 22px; border-radius: 50%;
width: 22px; height: 22px; border-radius: var(--radius-full);
background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.25);
cursor: pointer;
}
.calc-result {
background: rgba(48,209,88,0.08);
border: 0.5px solid rgba(48,209,88,0.2);
border-radius: 12px; padding: 16px; text-align: center;
border-radius: var(--radius-md); padding: 16px; text-align: center;
}
.calc-result-label { font-size: 13px; color: var(--label-secondary); margin-bottom: 6px; }
.calc-result-amount {
@@ -293,13 +293,13 @@
.referral-card {
background: linear-gradient(135deg, rgba(10,132,255,0.08), rgba(94,92,230,0.08));
border: 0.5px solid rgba(10,132,255,0.2);
border-radius: 16px; padding: 20px;
border-radius: var(--radius-lg); padding: 20px;
}
.referral-header {
display: flex; gap: 12px; align-items: center; margin-bottom: 14px;
}
.referral-icon {
width: 48px; height: 48px; border-radius: 12px;
width: 48px; height: 48px; border-radius: var(--radius-md);
background: rgba(10,132,255,0.15);
display: flex; align-items: center; justify-content: center;
font-size: 24px;
@@ -311,14 +311,14 @@
display: flex; gap: 10px; align-items: stretch; margin-bottom: 14px;
}
.referral-code {
flex: 1; background: var(--fill-secondary); border-radius: 10px;
flex: 1; background: var(--fill-secondary); border-radius: var(--radius-md);
padding: 12px 14px;
font-family: var(--font-mono); font-size: 16px; font-weight: 700;
letter-spacing: 2px; color: var(--ios-blue); text-align: center;
}
.referral-copy-btn {
background: var(--ios-blue); color: #fff;
border: none; border-radius: 10px;
border: none; border-radius: var(--radius-md);
padding: 12px 16px; font-size: 14px; font-weight: 700;
cursor: pointer; transition: opacity 0.15s;
}
@@ -326,7 +326,7 @@
.referral-share-btn {
display: block; width: 100%;
padding: 13px; background: rgba(10,132,255,0.12); color: var(--ios-blue);
border: 0.5px solid rgba(10,132,255,0.2); border-radius: 10px;
border: 0.5px solid rgba(10,132,255,0.2); border-radius: var(--radius-md);
font-size: 15px; font-weight: 600;
cursor: pointer; transition: opacity 0.15s; text-align: center;
margin-bottom: 12px;
@@ -336,7 +336,7 @@
display: flex; gap: 10px;
}
.referral-stat {
flex: 1; background: var(--fill-secondary); border-radius: 10px;
flex: 1; background: var(--fill-secondary); border-radius: var(--radius-md);
padding: 10px; text-align: center;
}
.referral-stat-num { font-size: 20px; font-weight: 700; font-family: var(--font-display); }
@@ -345,7 +345,7 @@
/* Loader */
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { width:24px; height:24px; border-radius:50%; border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; display:inline-block; }
.spinner { width:24px; height:24px; border-radius: var(--radius-full); border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; display:inline-block; }
/* Tab Bar */
.tab-bar {
@@ -363,7 +363,7 @@
.tab-item.active .tab-label { color:var(--ios-blue); }
/* Toast */
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius:20px; padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius: var(--radius-xl); padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
@@ -417,13 +417,13 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Profile Hero -->
<div class="profile-hero">
<div class="avatar" id="avatar">
🧑
<div class="avatar-badge unverified" id="avatar-badge">⚠️</div>
<div class="avatar-badge unverified" id="avatar-badge"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></div>
</div>
<div class="profile-name" id="profile-name">Zoomer</div>
<div class="profile-id" id="profile-id">Laddar...</div>
<div class="rating-row">
<span class="stars">★★★★★</span>
<span class="stars"></span>
<span class="rating-num" id="rating-num"></span>
<span class="rating-count" id="rating-count">(0 uppdrag)</span>
</div>
@@ -448,7 +448,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Onboarding Progress -->
<div class="onboarding-card" id="onboarding-section">
<div class="onboarding-header">
<span class="onboarding-title">🚀 Kom igång</span>
<span class="onboarding-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> Kom igång</span>
<span class="onboarding-fraction" id="ob-fraction">0 / 3 klara</span>
</div>
<div class="onboarding-track">
@@ -456,15 +456,15 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="onboarding-steps">
<div class="onboarding-step" id="ob-step-1">
<div class="os-icon">🪪</div>
<div class="os-icon"></div>
<div class="os-label" id="ob-lbl-1">KYC</div>
</div>
<div class="onboarding-step" id="ob-step-2">
<div class="os-icon">🏦</div>
<div class="os-icon"></div>
<div class="os-label" id="ob-lbl-2">Utbetalning</div>
</div>
<div class="onboarding-step" id="ob-step-3">
<div class="os-icon">🎯</div>
<div class="os-icon"></div>
<div class="os-label" id="ob-lbl-3">Första uppdrag</div>
</div>
</div>
@@ -476,18 +476,18 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Identitetsverifiering</div>
<div class="kyc-card">
<div class="kyc-top">
<div class="kyc-icon unverified" id="kyc-icon">🪪</div>
<div class="kyc-icon unverified" id="kyc-icon"></div>
<div class="kyc-info">
<div class="kyc-title">Identitetsverifiering</div>
<div class="kyc-desc">Verifiera din identitet för att ta emot utbetalningar. Krävs enligt lag (AML).</div>
</div>
</div>
<div class="kyc-status-bar" id="kyc-status-bar">
<span class="status-chip chip-unverified" id="kyc-chip">⚠️ Ej verifierad</span>
<span class="status-chip chip-unverified" id="kyc-chip"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Ej verifierad</span>
<span style="color:var(--label-secondary)">Verifiera för att låsa upp alla uppdrag</span>
</div>
<button class="kyc-btn unverified" id="kyc-btn" onclick="startKYC()">
🪪 Verifiera identitet →
Verifiera identitet →
</button>
</div>
</div>
@@ -497,7 +497,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Inkomstpotential</div>
<div class="calc-card">
<div class="calc-header">
<div class="calc-title">💰 Vad kan du tjäna?</div>
<div class="calc-title"> Vad kan du tjäna?</div>
<div class="calc-sub">Estimera din månadsinkomst</div>
</div>
<div class="calc-body">
@@ -510,12 +510,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="calc-label">Uppdragstyp</span>
<span class="calc-value" id="calc-type-lbl">Fasadscanning (800 kr)</span>
</div>
<select id="calc-type" onchange="updateCalc()" style="width:100%;background:var(--fill-secondary);border:0.5px solid var(--separator);border-radius:10px;padding:10px 14px;font-size:14px;color:var(--label-primary);font-family:var(--font-text);margin-bottom:16px;-webkit-appearance:none">
<option value="800">🏢 Fasadscanning — 800 kr</option>
<option value="1200">🏠 Takscanning — 1 200 kr</option>
<option value="600">🌳 Vegetationsinventering — 600 kr</option>
<option value="1500">🛣️ Väginspektionen — 1 500 kr</option>
<option value="2000">🏗️ Byggnadsdokumentation — 2 000 kr</option>
<select id="calc-type" onchange="updateCalc()" style="width:100%;background:var(--fill-secondary);border:0.5px solid var(--separator);border-radius: var(--radius-md);padding:10px 14px;font-size:14px;color:var(--label-primary);font-family:var(--font-text);margin-bottom:16px;-webkit-appearance:none">
<option value="800"><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 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Fasadscanning — 800 kr</option>
<option value="1200"> Takscanning — 1 200 kr</option>
<option value="600"> Vegetationsinventering — 600 kr</option>
<option value="1500"> Väginspektionen — 1 500 kr</option>
<option value="2000"><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> Byggnadsdokumentation — 2 000 kr</option>
</select>
<div class="calc-result">
<div class="calc-result-label">Estimerad månadsinkomst</div>
@@ -545,7 +545,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Referral Program</div>
<div class="referral-card">
<div class="referral-header">
<div class="referral-icon">🤝</div>
<div class="referral-icon"></div>
<div class="referral-info">
<div class="referral-title">Tjäna 200 kr per ny Zoomer</div>
<div class="referral-desc">Dela din kod — du och din vän får 200 kr när de slutför sitt första uppdrag</div>
@@ -553,9 +553,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="referral-code-row">
<div class="referral-code" id="referral-code">ZOOM-——</div>
<button class="referral-copy-btn" onclick="copyReferralCode()">📋 Kopiera</button>
<button class="referral-copy-btn" onclick="copyReferralCode()"><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> Kopiera</button>
</div>
<button class="referral-share-btn" onclick="shareReferral()">📤 Dela via iOS Share Sheet</button>
<button class="referral-share-btn" onclick="shareReferral()"> Dela via iOS Share Sheet</button>
<div class="referral-stats">
<div class="referral-stat">
<div class="referral-stat-num" id="ref-count">0</div>
@@ -586,7 +586,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Inställningar</div>
<div class="section-card">
<div class="list-row" onclick="copyId()">
<div class="row-icon-wrap" style="background:rgba(0,122,255,0.15)">🆔</div>
<div class="row-icon-wrap" style="background:rgba(0,122,255,0.15)"></div>
<div class="row-body">
<div class="row-title">Zoomer-ID</div>
<div class="row-sub" id="full-id" style="font-family:var(--font-mono);font-size:11px"></div>
@@ -594,7 +594,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="row-right">Kopiera <span class="chevron"></span></span>
</div>
<div class="list-row" onclick="openEarnings()">
<div class="row-icon-wrap" style="background:rgba(52,199,89,0.15)">💰</div>
<div class="row-icon-wrap" style="background:rgba(52,199,89,0.15)"></div>
<div class="row-body">
<div class="row-title">Intjäning & utbetalningar</div>
<div class="row-sub">Utbetalningskonto, historik</div>
@@ -602,7 +602,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="chevron"></span>
</div>
<div class="list-row" onclick="openSupport()">
<div class="row-icon-wrap" style="background:rgba(175,82,222,0.15)">💬</div>
<div class="row-icon-wrap" style="background:rgba(175,82,222,0.15)"></div>
<div class="row-body">
<div class="row-title">Support</div>
<div class="row-sub">support@quixzoom.com</div>
@@ -610,7 +610,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="chevron"></span>
</div>
<div class="list-row" onclick="openTerms()">
<div class="row-icon-wrap" style="background:rgba(142,142,147,0.15)">📄</div>
<div class="row-icon-wrap" style="background:rgba(142,142,147,0.15)"></div>
<div class="row-body">
<div class="row-title">Användarvillkor & Integritet</div>
<div class="row-sub">Juridisk information</div>
@@ -631,15 +631,15 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Tab Bar -->
<nav class="tab-bar">
<a class="tab-item" href="zoomer-app.html">
<span class="tab-icon">🎯</span>
<span class="tab-icon"></span>
<span class="tab-label">Uppdrag</span>
</a>
<a class="tab-item" href="zoomer-earnings.html">
<span class="tab-icon">💰</span>
<span class="tab-icon"></span>
<span class="tab-label">Intjäning</span>
</a>
<a class="tab-item active" href="zoomer-profile.html">
<span class="tab-icon">👤</span>
<span class="tab-icon"></span>
<span class="tab-label">Profil</span>
</a>
</nav>
@@ -675,8 +675,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
return m[type] || type;
}
function missionTypeEmoji(type) {
const m = { facade_scan:'🏢', roof_scan:'🏠', road_inspection:'🛣️', interior_scan:'🏚️', vegetation_survey:'🌳', inspection:'🔍' };
return m[type] || '📍';
const m = { facade_scan:'<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 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg>', roof_scan:'', road_inspection:'', interior_scan:'', vegetation_survey:'', inspection:'<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>' };
return m[type] || '';
}
async function loadProfile() {
@@ -743,13 +743,13 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (status === 'verified') {
iconEl.className = 'kyc-icon verified';
iconEl.textContent = '';
iconEl.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
chipEl.className = 'status-chip chip-verified';
chipEl.textContent = ' Verifierad';
chipEl.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Verifierad';
btnEl.className = 'kyc-btn verified';
btnEl.textContent = ' Identitet verifierad';
btnEl.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Identitet verifierad';
badgeEl.className = 'avatar-badge verified';
badgeEl.textContent = '';
badgeEl.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
document.getElementById('kyc-status-bar').querySelector('span:last-child').textContent = 'Konto verifierat — alla uppdrag tillgängliga';
} else if (status === 'pending') {
iconEl.className = 'kyc-icon pending';
@@ -762,11 +762,11 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
} else {
// unverified
iconEl.className = 'kyc-icon unverified';
iconEl.textContent = '🪪';
iconEl.textContent = '';
chipEl.className = 'status-chip chip-unverified';
chipEl.textContent = '⚠️ Ej verifierad';
chipEl.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Ej verifierad';
btnEl.className = 'kyc-btn unverified';
btnEl.textContent = '🪪 Verifiera identitet →';
btnEl.textContent = ' Verifiera identitet →';
}
}
@@ -784,7 +784,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
setKYCStatus('pending');
renderKYCStatus('pending');
toast('🪪 Öppnar verifiering...');
toast(' Öppnar verifiering...');
window.open(kycUrl, '_blank');
// Starta polling för status
@@ -792,8 +792,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
} catch(e) {
console.error('KYC error:', e);
toast(' Ett fel uppstod: ' + e.message);
btn.textContent = '🪪 Verifiera identitet →';
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Ett fel uppstod: ' + e.message);
btn.textContent = ' Verifiera identitet →';
btn.disabled = false;
}
}
@@ -808,12 +808,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (data.assurance_level === 'photo_low' || data.status === 'approved') {
setKYCStatus('verified');
renderKYCStatus('verified');
toast(' Identitet verifierad!');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Identitet verifierad!');
return;
} else if (data.assurance_level === 'rejected') {
setKYCStatus('unverified');
renderKYCStatus('unverified');
toast(' Verifiering nekad: ' + (data.reject_reason || 'Okänt fel'));
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Verifiering nekad: ' + (data.reject_reason || 'Okänt fel'));
return;
}
@@ -827,7 +827,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
function copyId() {
const id = getZoomerId();
navigator.clipboard?.writeText(id).then(() => toast('📋 ID kopierat!')).catch(() => toast('ID: ' + id));
navigator.clipboard?.writeText(id).then(() => toast('<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> ID kopierat!')).catch(() => toast('ID: ' + id));
}
function openEarnings() { window.location.href = 'zoomer-earnings.html'; }
@@ -858,7 +858,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
steps.forEach((s,i) => {
const el = document.getElementById(labels[i]);
el.className = 'os-label ' + (s ? 'done' : (i === done ? 'active' : ''));
if (s) el.textContent = ' ' + texts[i];
if (s) el.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> ' + texts[i];
else if (i === done) el.textContent = ' ' + texts[i];
else el.textContent = texts[i];
});
@@ -867,13 +867,13 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (done >= 2) {
// KYC + bank done — can take first mission
cta.disabled = false;
cta.textContent = steps[2] ? ' Första uppdrag slutfört!' : '🎯 Ta mitt första uppdrag →';
cta.textContent = steps[2] ? '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Första uppdrag slutfört!' : ' Ta mitt första uppdrag →';
if (steps[2]) cta.disabled = true;
} else {
cta.disabled = true;
cta.textContent = done === 0 ? '🔒 Slutför KYC för att börja' :
done === 1 ? '🔒 Länka bankkonto för att fortsätta' :
'🎯 Ta mitt första uppdrag →';
cta.textContent = done === 0 ? ' Slutför KYC för att börja' :
done === 1 ? ' Länka bankkonto för att fortsätta' :
' Ta mitt första uppdrag →';
}
}
updateOnboarding();
@@ -932,7 +932,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
function copyReferralCode() {
const code = getReferralCode();
const msg = 'Gå med i quiXzoom med min kod ' + code + ' och tjäna 200 kr på ditt första uppdrag! https://quixzoom.com/join?ref=' + code;
navigator.clipboard?.writeText(msg).then(() => toast('📋 Referral-kod kopierad!')).catch(() => toast('Kod: ' + code));
navigator.clipboard?.writeText(msg).then(() => toast('<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> Referral-kod kopierad!')).catch(() => toast('Kod: ' + code));
}
function shareReferral() {
+37 -37
View File
@@ -18,7 +18,7 @@
-webkit-font-smoothing: antialiased;
}
/* ── Nav Bar ───────────────────────────────────────────── */
/* Nav Bar */
.nav-bar {
position: sticky;
top: 0;
@@ -45,7 +45,7 @@
background-clip: text;
}
/* ── Hero ──────────────────────────────────────────────── */
/* Hero */
.hero {
padding: 48px 20px 32px;
text-align: center;
@@ -61,7 +61,7 @@
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 5px 14px;
border-radius: 20px;
border-radius: var(--radius-xl);
margin-bottom: 20px;
}
@@ -89,12 +89,12 @@
margin: 0 auto 28px;
}
/* ── Earnings Banner ───────────────────────────────────── */
/* Earnings Banner */
.earnings-card {
margin: 0 20px 32px;
background: linear-gradient(135deg, rgba(52,199,89,0.15) 0%, rgba(0,122,255,0.10) 100%);
border: 0.5px solid rgba(52,199,89,0.3);
border-radius: 18px;
border-radius: var(--radius-lg);
padding: 20px;
text-align: center;
}
@@ -113,7 +113,7 @@
margin-top: 4px;
}
/* ── 3-Step How It Works ───────────────────────────────── */
/* 3-Step How It Works */
.section-title {
font-family: var(--font-display, -apple-system, "SF Pro Display", sans-serif);
font-size: 22px;
@@ -137,7 +137,7 @@
gap: 16px;
background: rgba(255,255,255,0.06);
border: 0.5px solid rgba(255,255,255,0.1);
border-radius: 16px;
border-radius: var(--radius-lg);
padding: 18px 16px;
transition: background 0.2s;
}
@@ -149,7 +149,7 @@
.step-icon {
width: 44px;
height: 44px;
border-radius: 12px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
@@ -183,7 +183,7 @@
line-height: 1.4;
}
/* ── Live Missions ─────────────────────────────────────── */
/* Live Missions */
.missions-section {
margin-bottom: 36px;
}
@@ -208,7 +208,7 @@
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
border-radius: var(--radius-full);
background: #34C759;
animation: pulse 2s infinite;
}
@@ -232,7 +232,7 @@
flex: 0 0 260px;
background: rgba(255,255,255,0.07);
border: 0.5px solid rgba(255,255,255,0.1);
border-radius: 16px;
border-radius: var(--radius-lg);
padding: 16px;
}
@@ -243,7 +243,7 @@
font-size: 12px;
font-weight: 600;
padding: 3px 10px;
border-radius: 10px;
border-radius: var(--radius-md);
margin-bottom: 10px;
}
@@ -279,7 +279,7 @@
color: rgba(52,199,89,0.7);
}
/* ── CTA Buttons ───────────────────────────────────────── */
/* CTA Buttons */
.cta-section {
padding: 0 20px;
margin-bottom: 40px;
@@ -294,7 +294,7 @@
background: #007AFF;
color: #fff;
border: none;
border-radius: 16px;
border-radius: var(--radius-lg);
font-family: var(--font-text, -apple-system, sans-serif);
font-size: 17px;
font-weight: 600;
@@ -316,7 +316,7 @@
background: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.85);
border: 0.5px solid rgba(255,255,255,0.15);
border-radius: 14px;
border-radius: var(--radius-lg);
font-size: 16px;
font-weight: 500;
cursor: pointer;
@@ -326,13 +326,13 @@
.btn-secondary:active { background: rgba(255,255,255,0.14); }
/* ── Stats Strip ───────────────────────────────────────── */
/* Stats Strip */
.stats-strip {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: rgba(255,255,255,0.08);
border-radius: 16px;
border-radius: var(--radius-lg);
margin: 0 20px 36px;
overflow: hidden;
border: 0.5px solid rgba(255,255,255,0.1);
@@ -357,7 +357,7 @@
color: rgba(255,255,255,0.75);
}
/* ── FAQ / Trust ───────────────────────────────────────── */
/* FAQ / Trust */
.trust-section {
padding: 0 20px;
margin-bottom: 48px;
@@ -378,7 +378,7 @@
.trust-icon {
width: 32px;
height: 32px;
border-radius: 8px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
@@ -387,7 +387,7 @@
background: rgba(52,199,89,0.15);
}
/* ── Loading state for missions ────────────────────────── */
/* Loading state for missions */
.missions-loading {
padding: 0 20px;
color: rgba(255,255,255,0.75);
@@ -443,7 +443,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Hero -->
<section class="hero">
<div class="hero-badge">🌍 Launching August 2026</div>
<div class="hero-badge"> Launching August 2026</div>
<h1>Earn money on<br><span class="accent">what you see around you</span></h1>
<p>Take photo missions near you. Get paid fast. No boss, no invoicing.</p>
</section>
@@ -474,7 +474,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<p class="section-title">How it works</p>
<div class="steps">
<div class="step-card">
<div class="step-icon blue">🔍</div>
<div class="step-icon blue"><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="step-content">
<div class="step-num">Step 1</div>
<div class="step-title">Find missions near you</div>
@@ -482,7 +482,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<div class="step-card">
<div class="step-icon green">📸</div>
<div class="step-icon green"></div>
<div class="step-content">
<div class="step-num">Step 2</div>
<div class="step-title">Complete the mission</div>
@@ -490,7 +490,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<div class="step-card">
<div class="step-icon purple">💰</div>
<div class="step-icon purple"></div>
<div class="step-content">
<div class="step-num">Step 3</div>
<div class="step-title">Get paid</div>
@@ -507,7 +507,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="missions-scroll" id="missions-container">
<div class="mission-card" style="opacity:0.5;text-align:center;padding:24px">
<div style="font-size:24px;margin-bottom:8px">📍</div>
<div style="font-size:24px;margin-bottom:8px"></div>
<div style="font-size:14px;color:#aaa">Loading missions near you...</div>
</div>
</div>
@@ -517,23 +517,23 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="trust-section">
<p class="section-title" style="padding:0;margin-bottom:12px">Always included</p>
<div class="trust-item">
<div class="trust-icon"></div>
<div class="trust-icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
Clear brief — you know exactly what to do
</div>
<div class="trust-item">
<div class="trust-icon">🔒</div>
<div class="trust-icon"></div>
Secure payout via Stripe Connect
</div>
<div class="trust-item">
<div class="trust-icon">📱</div>
<div class="trust-icon"></div>
No special gear — a smartphone is enough
</div>
<div class="trust-item">
<div class="trust-icon"></div>
<div class="trust-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg></div>
Approval and payment within 48 hours
</div>
<div class="trust-item">
<div class="trust-icon">🗺️</div>
<div class="trust-icon"></div>
Choose which missions you take
</div>
</div>
@@ -551,21 +551,21 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<script>
const API_BASE = 'https://api.quixzoom.com/api/qz';
function missionEmoji(t){
if(/elledn|kabel|transfor/i.test(t)) return '';
if(/vag|potthal|trottoar/i.test(t)) return '🛣️';
if(/kanal|bro/i.test(t)) return '🚤';
if(/gatuliv|neon/i.test(t)) return '🏙️';
return '📸';
if(/elledn|kabel|transfor/i.test(t)) return '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg>';
if(/vag|potthal|trottoar/i.test(t)) return '';
if(/kanal|bro/i.test(t)) return '';
if(/gatuliv|neon/i.test(t)) return '';
return '';
}
function renderMissions(missions){
const c=document.getElementById('missions-container');
if(!missions.length){c.innerHTML='<div style="text-align:center;padding:24px;color:#aaa;font-size:14px">📍 No missions near you right now</div>';return;}
if(!missions.length){c.innerHTML='<div style="text-align:center;padding:24px;color:#aaa;font-size:14px"> No missions near you right now</div>';return;}
c.innerHTML=missions.slice(0,6).map(m=>{
const d=m.distance_meters<1000?Math.round(m.distance_meters)+'m':(m.distance_meters/1000).toFixed(1)+'km';
return '<div class="mission-card" onclick="location.href=\''/zoomer-onboarding-flow\'" style="cursor:pointer">'
+'<div class="mission-type-badge road">'+missionEmoji(m.title)+' '+(m.area_name||'Mission')+'</div>'
+'<h3>'+m.title+'</h3>'
+'<div class="mission-meta">📍 '+d+' away · '+(m.required_photos||1)+' photo</div>'
+'<div class="mission-meta"> '+d+' away · '+(m.required_photos||1)+' photo</div>'
+'<div class="mission-reward">'+m.reward_credits+' cr<span> on approval</span></div>'
+'</div>';
}).join('');