Subh775 commited on
Commit
091ea0d
·
1 Parent(s): 61bce2d

RELEASE: URBANFLOW: YOUR VISION PARTNER..

Browse files
backend/engine.py CHANGED
@@ -55,9 +55,7 @@ class ThreadedVideoWriter:
55
  def write(self, frame):
56
  if not self.stopped:
57
  try:
58
- # If queue is full, we might want to wait or drop, but here we'll wait
59
- # to ensure the export video is complete and accurate.
60
- self.queue.put(frame.copy())
61
  except Exception as e:
62
  print(f"[BACKEND] Writer queue error: {e}")
63
 
@@ -240,10 +238,11 @@ def run(model, video_path, line, config, on_frame, save_annotated=False, annotat
240
  "total_iters": total_iters,
241
  "total_frames": total,
242
  "active": active,
243
- "congestion": congestion.copy(),
 
244
  "class_in": {str(k): v for k, v in class_in.items()},
245
  "class_out": {str(k): v for k, v in class_out.items()},
246
- "flow_times": flow_times.copy(),
247
  "elapsed": round(elapsed, 2),
248
  "fps": round((frame_idx + 1) / elapsed, 2) if elapsed > 0 else 0,
249
  }
 
55
  def write(self, frame):
56
  if not self.stopped:
57
  try:
58
+ self.queue.put(frame) # no copy frame ownership transfers to queue
 
 
59
  except Exception as e:
60
  print(f"[BACKEND] Writer queue error: {e}")
61
 
 
238
  "total_iters": total_iters,
239
  "total_frames": total,
240
  "active": active,
241
+ "congestion_len": len(congestion), # just the length, not the full list
242
+ "congestion_last": congestion[-1] if congestion else 0, # only latest value
243
  "class_in": {str(k): v for k, v in class_in.items()},
244
  "class_out": {str(k): v for k, v in class_out.items()},
245
+ "flow_count": len(flow_times), # just the count
246
  "elapsed": round(elapsed, 2),
247
  "fps": round((frame_idx + 1) / elapsed, 2) if elapsed > 0 else 0,
248
  }
backend/server.py CHANGED
@@ -397,7 +397,6 @@ async def ws_run(ws: WebSocket):
397
  "actual_fps": result["actual_fps"],
398
  "speed_vs_realtime": result["speed_vs_realtime"],
399
  "pcu": result.get("pcu", {}),
400
- "speed_distribution": result.get("speed", {}).get("distribution", {}),
401
  }))
402
  await ws.close()
403
 
@@ -414,16 +413,19 @@ async def ws_run(ws: WebSocket):
414
 
415
  from fastapi.staticfiles import StaticFiles
416
 
417
- class NoCacheStaticFiles(StaticFiles):
418
  def is_not_modified(self, response_headers, request_headers) -> bool:
419
  return False
420
-
421
  async def get_response(self, path: str, scope):
422
  resp = await super().get_response(path, scope)
423
- resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
 
 
 
424
  return resp
425
 
426
- app.mount("/", NoCacheStaticFiles(directory=str(FRONTEND)), name="frontend")
427
 
428
  if __name__ == "__main__":
429
  import uvicorn
 
397
  "actual_fps": result["actual_fps"],
398
  "speed_vs_realtime": result["speed_vs_realtime"],
399
  "pcu": result.get("pcu", {}),
 
400
  }))
401
  await ws.close()
402
 
 
413
 
414
  from fastapi.staticfiles import StaticFiles
415
 
416
+ class SmartCacheStaticFiles(StaticFiles):
417
  def is_not_modified(self, response_headers, request_headers) -> bool:
418
  return False
419
+
420
  async def get_response(self, path: str, scope):
421
  resp = await super().get_response(path, scope)
422
+ if path.endswith(".html") or path in ("", "/"):
423
+ resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
424
+ else:
425
+ resp.headers["Cache-Control"] = "public, max-age=3600"
426
  return resp
427
 
428
+ app.mount("/", SmartCacheStaticFiles(directory=str(FRONTEND)), name="frontend")
429
 
430
  if __name__ == "__main__":
431
  import uvicorn
frontend/assets/{rf.png → shuriken.png} RENAMED
File without changes
frontend/assets/shurkien_b.png ADDED

Git LFS Details

  • SHA256: 97d886b4ec5ad9a18432f6aaa0d78aa67c2ae2a18fe987438824066364cf7d4a
  • Pointer size: 131 Bytes
  • Size of remote file: 934 kB
frontend/css/initial.css CHANGED
@@ -1,70 +1,476 @@
 
 
 
 
 
1
  :root {
2
- --cocoa: #8b5e3c;
3
- --cocoa-l: #c89a6c;
4
  --cocoa-xl: #d4b08a;
5
- --t1: #f0ece6;
6
- --t2: #a89f97;
7
- --border: #2a2a2a;
8
  }
 
 
 
 
 
 
 
 
 
 
9
  body {
 
 
10
  font-family: 'Montserrat', sans-serif;
11
  background-color: #000000;
12
  color: var(--t1);
 
 
 
13
  }
 
 
 
 
 
14
  .fade-in {
15
  animation: fadeIn 0.4s ease-in-out forwards;
16
  }
17
  @keyframes fadeIn {
18
- from {
19
- opacity: 0;
20
- transform: translateY(10px);
21
- }
22
- to {
23
- opacity: 1;
24
- transform: translateY(0);
25
- }
26
  }
27
- /* Executive Overrides */
 
 
 
 
 
 
 
 
 
 
28
  .traffic-dynamics-card {
29
  background-color: #0a0a0a !important;
30
- border: 2px solid var(--cocoa) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
  .traffic-dynamics-card:hover {
33
- border-color: var(--cocoa-l) !important;
34
  }
 
 
 
35
  #dropzone {
36
  transition: all 0.2s ease;
37
  border-color: #2a2a2a;
 
 
38
  }
39
- #dropzone:hover {
 
 
40
  border-color: var(--cocoa-l) !important;
41
- background-color: #0a0a0a !important;
42
  }
 
 
43
  .core-badge {
44
  background-color: var(--cocoa) !important;
45
  color: var(--t1) !important;
46
  }
47
- /* Onboarding */
 
48
  .onboard-overlay {
49
  position: fixed; inset: 0; z-index: 9999;
50
  background: rgba(0,0,0,0.92);
51
  display: flex; align-items: center; justify-content: center;
 
52
  }
53
  .onboard-card {
54
- background: #0a0a0a; border: 1px solid #2a2a2a;
55
- border-radius: 16px; max-width: 440px; width: 90%;
56
- padding: 40px 32px; text-align: center;
 
 
 
 
57
  }
58
  .onboard-step { display: none; }
59
  .onboard-step.active { display: block; }
60
- .onboard-dots { display: flex; gap: 6px; justify-content: center; margin-top: 20px; }
 
 
 
 
61
  .onboard-dot {
62
- width: 8px; height: 8px; border-radius: 50%;
63
- background: #333; transition: background 0.2s;
 
 
64
  }
65
  .onboard-dot.active { background: var(--cocoa-l); }
66
- /* Mobile responsive */
67
- @media (max-width: 768px) {
68
- main { grid-template-columns: 1fr !important; padding: 16px !important; }
69
- h1 { font-size: 2.2rem !important; }
70
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =============================================
2
+ UrbanFlow — initial.css (Mobile-First)
3
+ Same design system, fully touch-friendly
4
+ ============================================= */
5
+
6
  :root {
7
+ --cocoa: #8b5e3c;
8
+ --cocoa-l: #c89a6c;
9
  --cocoa-xl: #d4b08a;
10
+ --t1: #f0ece6;
11
+ --t2: #a89f97;
12
+ --border: #2a2a2a;
13
  }
14
+
15
+ *, *::before, *::after { box-sizing: border-box; }
16
+
17
+ html {
18
+ scrollbar-width: none;
19
+ }
20
+ html::-webkit-scrollbar {
21
+ display: none;
22
+ }
23
+
24
  body {
25
+ overflow-x: hidden;
26
+ width: 100%;
27
  font-family: 'Montserrat', sans-serif;
28
  background-color: #000000;
29
  color: var(--t1);
30
+ margin: 0;
31
+ -webkit-tap-highlight-color: transparent;
32
+ scrollbar-width: none; /* Firefox */
33
  }
34
+ body::-webkit-scrollbar {
35
+ display: none; /* Chrome, Safari, Edge */
36
+ }
37
+
38
+ /* ---- Fade animation ---- */
39
  .fade-in {
40
  animation: fadeIn 0.4s ease-in-out forwards;
41
  }
42
  @keyframes fadeIn {
43
+ from { opacity: 0; transform: translateY(10px); }
44
+ to { opacity: 1; transform: translateY(0); }
45
+ }
46
+
47
+ /* ---- Feature List Animation ---- */
48
+ @keyframes fadeSlideIn {
49
+ from { opacity: 0; transform: translateX(-8px); }
50
+ to { opacity: 1; transform: translateX(0); }
51
  }
52
+ .hero-text-section ul li {
53
+ opacity: 0;
54
+ animation: fadeSlideIn 0.4s ease-out forwards;
55
+ }
56
+ .hero-text-section ul li:nth-child(1) { animation-delay: 0.1s; }
57
+ .hero-text-section ul li:nth-child(2) { animation-delay: 0.2s; }
58
+ .hero-text-section ul li:nth-child(3) { animation-delay: 0.3s; }
59
+ .hero-text-section ul li:nth-child(4) { animation-delay: 0.4s; }
60
+ .hero-text-section ul li:nth-child(5) { animation-delay: 0.5s; }
61
+
62
+ /* ---- Card ---- */
63
  .traffic-dynamics-card {
64
  background-color: #0a0a0a !important;
65
+ border: none !important;
66
+ position: relative;
67
+ transition: box-shadow 0.3s ease;
68
+ z-index: 0;
69
+ }
70
+
71
+ /* Shimmer border — only on the 2px ring, not inside the card */
72
+ .traffic-dynamics-card::before {
73
+ content: '';
74
+ position: absolute;
75
+ inset: 0;
76
+ border-radius: inherit;
77
+ padding: 2px;
78
+ background: linear-gradient(
79
+ 90deg,
80
+ var(--cocoa) 0%,
81
+ var(--cocoa-l) 30%,
82
+ rgba(255,255,255,0.7) 50%,
83
+ var(--cocoa-l) 70%,
84
+ var(--cocoa) 100%
85
+ );
86
+ background-size: 200% 100%;
87
+ animation: borderShimmer 2.4s linear infinite;
88
+ -webkit-mask:
89
+ linear-gradient(#fff 0 0) content-box,
90
+ linear-gradient(#fff 0 0);
91
+ -webkit-mask-composite: xor;
92
+ mask-composite: exclude;
93
+ pointer-events: none;
94
+ }
95
+ @keyframes borderShimmer {
96
+ 0% { background-position: 200% 0; }
97
+ 100% { background-position: -200% 0; }
98
+ }
99
+ .traffic-dynamics-card:hover::before {
100
+ animation-duration: 1.2s;
101
  }
102
  .traffic-dynamics-card:hover {
103
+ box-shadow: 0 0 32px 0 rgba(200,154,108,0.18);
104
  }
105
+
106
+
107
+ /* ---- Dropzone ---- */
108
  #dropzone {
109
  transition: all 0.2s ease;
110
  border-color: #2a2a2a;
111
+ /* finger-friendly minimum height */
112
+ min-height: 160px;
113
  }
114
+ #dropzone:hover,
115
+ #dropzone:active,
116
+ #dropzone.dz-active {
117
  border-color: var(--cocoa-l) !important;
118
+ background-color: #0d0d0d !important;
119
  }
120
+
121
+ /* ---- Core badge ---- */
122
  .core-badge {
123
  background-color: var(--cocoa) !important;
124
  color: var(--t1) !important;
125
  }
126
+
127
+ /* ---- Onboarding ---- */
128
  .onboard-overlay {
129
  position: fixed; inset: 0; z-index: 9999;
130
  background: rgba(0,0,0,0.92);
131
  display: flex; align-items: center; justify-content: center;
132
+ padding: 16px;
133
  }
134
  .onboard-card {
135
+ background: #0a0a0a;
136
+ border: 1px solid #2a2a2a;
137
+ border-radius: 16px;
138
+ max-width: 440px;
139
+ width: 100%;
140
+ padding: 32px 24px;
141
+ text-align: center;
142
  }
143
  .onboard-step { display: none; }
144
  .onboard-step.active { display: block; }
145
+ .onboard-dots {
146
+ display: flex; gap: 6px;
147
+ justify-content: center;
148
+ margin-top: 20px;
149
+ }
150
  .onboard-dot {
151
+ width: 8px; height: 8px;
152
+ border-radius: 50%;
153
+ background: #333;
154
+ transition: background 0.2s;
155
  }
156
  .onboard-dot.active { background: var(--cocoa-l); }
157
+
158
+ /* Onboarding buttons — large touch targets */
159
+ .onboard-card .flex.gap-3 button {
160
+ min-height: 44px;
161
+ padding: 10px 20px;
162
+ font-size: 11px;
163
+ }
164
+
165
+ /* Onboarding Icon Pulse */
166
+ @keyframes iconPulse {
167
+ 0% { transform: scale(1); }
168
+ 50% { transform: scale(1.12); }
169
+ 100% { transform: scale(1); }
170
+ }
171
+ .pulse-once {
172
+ animation: iconPulse 0.35s ease;
173
+ }
174
+
175
+ /* ---- Canvas container — touch hint ---- */
176
+ #drawing-canvas {
177
+ touch-action: none; /* prevents scroll while drawing */
178
+ }
179
+
180
+ /* ---- Upload progress ---- */
181
+ #upload-progress-container {
182
+ padding: 0 4px;
183
+ }
184
+ #upload-bar {
185
+ transition: width 0.3s ease-out;
186
+ position: relative;
187
+ }
188
+
189
+ /* Shimmer overlay */
190
+ #upload-bar::after {
191
+ content: '';
192
+ position: absolute;
193
+ top: 0; right: 0; bottom: 0; left: 0;
194
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
195
+ transform: translateX(-100%);
196
+ animation: shimmer 1.5s infinite;
197
+ }
198
+ @keyframes shimmer {
199
+ 100% { transform: translateX(100%); }
200
+ }
201
+
202
+ /* =============================================
203
+ DESKTOP (≥1024px) — original layout
204
+ ============================================= */
205
+ @media (min-width: 1024px) {
206
+ header { margin-top: 4rem; }
207
+
208
+ main {
209
+ display: grid !important;
210
+ grid-template-columns: repeat(12, 1fr) !important;
211
+ gap: 5rem !important;
212
+ padding: 2rem 2.5rem !important;
213
+ align-items: center;
214
+ min-height: 70vh !important;
215
+ }
216
+
217
+ /* Stable card on desktop */
218
+ main > div:last-child {
219
+ height: 520px !important;
220
+ overflow: hidden !important;
221
+ margin-bottom: 0 !important;
222
+ display: flex;
223
+ flex-direction: column;
224
+ justify-content: center;
225
+ }
226
+
227
+ /* Steps scroll internally */
228
+ #step-modules, #step-upload, #step-draw {
229
+ overflow-y: auto !important;
230
+ height: auto !important;
231
+ max-height: 100% !important;
232
+ padding-right: 8px;
233
+ }
234
+
235
+ /* Clamped font size to avoid overflow */
236
+ .hero-title { font-size: clamp(3rem, 4.5vw, 4.5rem) !important; line-height: 1.1 !important; }
237
+
238
+ #dropzone { min-height: 200px; }
239
+ }
240
+
241
+ /* =============================================
242
+ TABLET (640px – 1023px)
243
+ ============================================= */
244
+ @media (min-width: 640px) and (max-width: 1023px) {
245
+ header { margin-top: 2rem; }
246
+ header img { height: 8rem !important; }
247
+
248
+ main {
249
+ display: flex !important;
250
+ flex-direction: column !important;
251
+ padding: 1.5rem !important;
252
+ gap: 2rem !important;
253
+ }
254
+
255
+ .hero-title { font-size: 2.8rem !important; }
256
+
257
+ /* Hero text section — centered on tablet */
258
+ main > div:first-child {
259
+ text-align: center;
260
+ padding-bottom: 0 !important;
261
+ }
262
+
263
+ main > div:first-child ul {
264
+ display: inline-block;
265
+ text-align: left;
266
+ }
267
+
268
+ /* Step card — full width */
269
+ main > div:last-child {
270
+ max-width: 100% !important;
271
+ width: 100% !important;
272
+ }
273
+
274
+ #dropzone { min-height: 180px; }
275
+ }
276
+
277
+ /* =============================================
278
+ MOBILE (< 640px) — redesigned mobile layout
279
+ ============================================= */
280
+ @media (max-width: 639px) {
281
+
282
+ /* ---- Body: single-column scroll ---- */
283
+ body {
284
+ overflow-y: auto !important;
285
+ overflow-x: hidden !important;
286
+ }
287
+
288
+ /* ---- Header — compact logo ---- */
289
+ header {
290
+ margin-top: 2rem !important;
291
+ margin-bottom: 0 !important;
292
+ padding: 0 20px;
293
+ }
294
+ header img {
295
+ height: 7rem !important;
296
+ }
297
+
298
+ /* ---- Main — stacked column ---- */
299
+ main {
300
+ display: flex !important;
301
+ flex-direction: column !important;
302
+ padding: 20px 16px 8px !important;
303
+ gap: 20px !important;
304
+ max-width: 100% !important;
305
+ }
306
+
307
+ /* ---- Hero text: SHOW on mobile (compact) ---- */
308
+ .hero-text-section {
309
+ display: flex !important;
310
+ flex-direction: column;
311
+ align-items: center;
312
+ text-align: center;
313
+ padding-bottom: 0 !important;
314
+ order: 1;
315
+ }
316
+
317
+ /* Mobile hero title styling */
318
+ .hero-title {
319
+ font-size: 2.8rem !important;
320
+ line-height: 1.15 !important;
321
+ margin-top: 10px !important;
322
+ text-align: center !important;
323
+ }
324
+
325
+ /* DEMO badge + tagline row */
326
+ .hero-text-section > p:first-of-type {
327
+ font-size: 11.5px !important;
328
+ letter-spacing: 0.18em !important;
329
+ margin-bottom: 14px !important;
330
+ justify-content: center;
331
+ }
332
+
333
+ /* Feature bullets */
334
+ .hero-text-section ul {
335
+ display: flex !important;
336
+ flex-direction: column !important;
337
+ gap: 4px !important;
338
+ width: 100%;
339
+ text-align: left;
340
+ }
341
+
342
+ .hero-text-section ul li {
343
+ display: flex !important;
344
+ align-items: flex-start !important;
345
+ font-size: 14px !important;
346
+ font-weight: 500 !important;
347
+ line-height: 1.6 !important;
348
+ padding: 5px 0;
349
+ color: #a89f97 !important;
350
+ }
351
+
352
+ .hero-text-section ul li i {
353
+ font-size: 14px !important;
354
+ margin-right: 10px !important;
355
+ margin-top: 2px;
356
+ flex-shrink: 0;
357
+ }
358
+
359
+ /* ---- Step card: full width, right below bullets ---- */
360
+ .step-card-section {
361
+ max-width: 100% !important;
362
+ width: 100% !important;
363
+ min-height: unset !important;
364
+ height: auto !important;
365
+ margin: 0 !important;
366
+ order: 2;
367
+ }
368
+
369
+ /* The fixed-height container that holds all steps */
370
+ main > div:last-child {
371
+ height: auto !important;
372
+ min-height: unset !important;
373
+ }
374
+
375
+ /* ---- Step headings ---- */
376
+ #step-modules h2,
377
+ #step-upload h2,
378
+ #step-draw h2 {
379
+ font-size: 1.4rem !important;
380
+ margin-bottom: 6px !important;
381
+ }
382
+
383
+ #step-modules p,
384
+ #step-upload p {
385
+ font-size: 11px !important;
386
+ margin-bottom: 20px !important;
387
+ }
388
+
389
+ /* ---- Traffic Dynamics card ---- */
390
+ .traffic-dynamics-card {
391
+ padding: 24px 20px !important;
392
+ border-radius: 1.5rem !important;
393
+ }
394
+
395
+ /* ---- Dropzone — warm glow tap target ---- */
396
+ #dropzone {
397
+ padding: 2rem 1.25rem !important;
398
+ min-height: 150px;
399
+ border-radius: 1.25rem !important;
400
+ border-color: #2a2a2a !important;
401
+ }
402
+
403
+ #dropzone span:first-of-type {
404
+ font-size: 0.95rem !important;
405
+ }
406
+
407
+ /* ---- Canvas wrapper ---- */
408
+ .relative.w-full.aspect-video {
409
+ aspect-ratio: 4/3 !important;
410
+ border-radius: 1rem !important;
411
+ max-height: 40vh; /* Keeps continue button above fold */
412
+ }
413
+
414
+ /* ---- Proceed button — full width ---- */
415
+ #btn-proceed {
416
+ width: 100% !important;
417
+ padding: 14px !important;
418
+ font-size: 0.9rem !important;
419
+ border-radius: 999px !important;
420
+ }
421
+
422
+ /* ---- Reset / Back buttons ---- */
423
+ button[onclick="resetCanvas()"],
424
+ button[onclick="showStep('modules')"] {
425
+ font-size: 11px !important;
426
+ padding: 10px 0 !important;
427
+ min-height: 40px;
428
+ }
429
+
430
+ /* ---- Footer — padding only on mobile, layout handled by Tailwind ---- */
431
+ footer {
432
+ padding: 14px 20px 24px !important;
433
+ }
434
+
435
+ /* ---- Onboarding card ---- */
436
+ .onboard-card {
437
+ padding: 28px 20px !important;
438
+ border-radius: 18px !important;
439
+ }
440
+ .onboard-card h3 {
441
+ font-size: 1rem !important;
442
+ }
443
+ .onboard-card p {
444
+ font-size: 11px !important;
445
+ }
446
+ }
447
+
448
+
449
+ /* =============================================
450
+ TOUCH: remove drag-related cursor on mobile
451
+ ============================================= */
452
+ @media (hover: none) and (pointer: coarse) {
453
+ #dropzone {
454
+ cursor: pointer;
455
+ }
456
+ #dropzone:hover {
457
+ /* no hover state on touch — only active */
458
+ border-color: #2a2a2a !important;
459
+ background-color: transparent !important;
460
+ }
461
+ #dropzone:active,
462
+ #dropzone.dz-active {
463
+ border-color: var(--cocoa-l) !important;
464
+ background-color: #0d0d0d !important;
465
+ }
466
+
467
+ /* Bigger touch targets for all buttons */
468
+ button {
469
+ min-height: 44px;
470
+ }
471
+
472
+ /* Canvas cursor for drawing */
473
+ #drawing-canvas {
474
+ cursor: crosshair;
475
+ }
476
+ }
frontend/css/vehicles.css CHANGED
@@ -1,450 +1,1389 @@
1
-
2
- :root {
3
- --cocoa: #8b5e3c;
4
- --cocoa-l: #c89a6c;
5
- --cocoa-xl: #d4b08a;
6
- }
7
-
8
- body {
9
- font-family: 'Montserrat', sans-serif;
10
- background-color: #000000;
11
- color: #f0ece6;
12
- }
13
-
14
- .mono-font {
15
- font-family: 'JetBrains Mono', monospace;
16
- }
17
-
18
- ::-webkit-scrollbar {
19
- width: 4px;
20
- height: 4px;
21
- }
22
-
23
- ::-webkit-scrollbar-track {
24
- background: #000000;
25
- }
26
-
27
- ::-webkit-scrollbar-thumb {
28
- background: #222222;
29
- border-radius: 4px;
30
- }
31
-
32
- ::-webkit-scrollbar-thumb:hover {
33
- background: #333333;
34
- }
35
-
36
- .info-wrap {
37
- position: relative;
38
- display: inline-flex;
39
- align-items: center;
40
- margin-left: 6px;
41
- }
42
-
43
- .info-btn {
44
- display: inline-flex;
45
- align-items: center;
46
- justify-content: center;
47
- width: 14px;
48
- height: 14px;
49
- border-radius: 50%;
50
- background: #444444 !important;
51
- color: #ffffff !important;
52
- font-size: 7px;
53
- cursor: pointer;
54
- transition: all 0.2s ease;
55
- }
56
-
57
- .info-btn:hover {
58
- background: #666666 !important;
59
- }
60
-
61
- .info-tip {
62
- display: none;
63
- position: fixed;
64
- z-index: 9999;
65
- background: #0a0a0a;
66
- color: #aaaaaa;
67
- font-size: 10px;
68
- font-weight: 500;
69
- line-height: 1.4;
70
- padding: 8px 12px;
71
- border-radius: 6px;
72
- max-width: 240px;
73
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8);
74
- border: 1px solid #222222;
75
- pointer-events: none;
76
- text-transform: none;
77
- letter-spacing: normal;
78
- }
79
-
80
- /* Nav states */
81
- .nav-item-active {
82
- background-color: #111111 !important;
83
- color: var(--cocoa-xl) !important;
84
- border-left: 2px solid var(--cocoa-l) !important;
85
- }
86
-
87
- .nav-item-inactive {
88
- color: #555555 !important;
89
- }
90
-
91
- .nav-item-inactive:hover {
92
- color: #f0ece6 !important;
93
- background-color: #050505 !important;
94
- }
95
-
96
- /* Card Overrides */
97
- .bg-white {
98
- background-color: #0a0a0a !important;
99
- }
100
-
101
- .border-slate-200,
102
- .border-slate-100,
103
- .border-slate-50,
104
- .border-neutral-800,
105
- .border-neutral-900 {
106
- border-color: #2a2a2a !important;
107
- }
108
-
109
- .bg-slate-50\/50,
110
- .bg-slate-50,
111
- .bg-slate-900,
112
- .bg-neutral-900 {
113
- background-color: #0c0c0c !important;
114
- }
115
-
116
- .text-slate-900,
117
- .text-slate-800,
118
- .text-slate-700,
119
- .text-neutral-900 {
120
- color: #ffffff !important;
121
- }
122
-
123
- .text-slate-600,
124
- .text-slate-500,
125
- .text-slate-400,
126
- .text-neutral-500,
127
- .text-neutral-400 {
128
- color: #888888 !important;
129
- }
130
-
131
- .shadow-sm {
132
- box-shadow: none !important;
133
- }
134
-
135
- /* Controls */
136
- .toggle-track {
137
- width: 32px;
138
- height: 18px;
139
- border-radius: 9px;
140
- background: #222222;
141
- position: relative;
142
- cursor: pointer;
143
- }
144
-
145
- .toggle-track.active {
146
- background: var(--cocoa-l);
147
- }
148
-
149
- .toggle-thumb {
150
- width: 14px;
151
- height: 14px;
152
- border-radius: 50%;
153
- background: #555555;
154
- position: absolute;
155
- top: 2px;
156
- left: 2px;
157
- transition: all 0.2s ease;
158
- }
159
-
160
- .toggle-track.active .toggle-thumb {
161
- transform: translateX(14px);
162
- background: #000000;
163
- }
164
-
165
- .custom-select {
166
- appearance: none;
167
- background-color: #111111;
168
- border: 1px solid #222222;
169
- border-radius: 6px;
170
- padding: 4px 24px 4px 10px;
171
- font-size: 11px;
172
- font-weight: 600;
173
- color: #ffffff;
174
- outline: none;
175
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23666666'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z'/%3E%3C/svg%3E");
176
- background-repeat: no-repeat;
177
- background-position: right 8px center;
178
- background-size: 12px;
179
- }
180
-
181
- .s-stepper {
182
- display: inline-flex;
183
- border: 1px solid #222222;
184
- border-radius: 6px;
185
- background: #111111;
186
- overflow: hidden;
187
- }
188
-
189
- .s-stepper button {
190
- padding: 4px 8px;
191
- color: #666666;
192
- font-size: 12px;
193
- }
194
-
195
- .s-stepper button:hover {
196
- background: #1a1a1a;
197
- color: #ffffff;
198
- }
199
-
200
- .s-stepper .s-val {
201
- min-width: 40px;
202
- text-align: center;
203
- font-family: 'JetBrains Mono', monospace;
204
- font-size: 12px;
205
- font-weight: 700;
206
- color: #ffffff;
207
- padding: 4px 0;
208
- border-left: 1px solid #222222;
209
- border-right: 1px solid #222222;
210
- }
211
-
212
- .s-row {
213
- display: flex;
214
- align-items: center;
215
- justify-content: space-between;
216
- padding: 12px 0;
217
- border-bottom: 1px solid #1a1a1a;
218
- }
219
-
220
- .s-row:last-child {
221
- border-bottom: none;
222
- }
223
-
224
- #proc-bar {
225
- background-color: var(--cocoa-l) !important;
226
- }
227
-
228
- #proc-label {
229
- color: #ffffff !important;
230
- }
231
-
232
- .s-row.disabled {
233
- opacity: 0.65 !important;
234
- }
235
-
236
- .s-row.disabled .s-stepper,
237
- .s-row.disabled .custom-select,
238
- .s-row.disabled .toggle-track,
239
- .s-row.disabled .chip-container {
240
- pointer-events: none !important;
241
- opacity: 0.5 !important;
242
- }
243
-
244
- .s-row.disabled .info-wrap {
245
- pointer-events: auto !important;
246
- opacity: 1 !important;
247
- }
248
-
249
-
250
- #btn-start-processing {
251
- font-family: 'Montserrat', sans-serif !important;
252
- }
253
-
254
- /* Chips */
255
- .chip-container {
256
- display: flex;
257
- flex-wrap: wrap;
258
- gap: 8px;
259
- margin-top: 12px;
260
- padding-top: 12px;
261
- border-top: 1px solid #1a1a1a;
262
- transition: all 0.3s ease;
263
- }
264
-
265
- .chip {
266
- display: inline-flex;
267
- align-items: center;
268
- gap: 6px;
269
- padding: 6px 14px;
270
- border-radius: 9999px;
271
- font-size: 10px;
272
- font-weight: 700;
273
- cursor: pointer;
274
- transition: all 0.2s ease;
275
- user-select: none;
276
- border: 1px solid #333333;
277
- background: rgba(255, 255, 255, 0.03);
278
- color: #888888;
279
- }
280
-
281
- .chip.active {
282
- background: var(--cocoa-l);
283
- color: #000000;
284
- border-color: var(--cocoa-l);
285
- }
286
-
287
- .chip.frozen {
288
- background: rgba(255, 255, 255, 0.4);
289
- color: #000000;
290
- border-color: transparent;
291
- cursor: default !important;
292
- pointer-events: none;
293
- }
294
-
295
- .chip:hover {
296
- border-color: #666666;
297
- }
298
-
299
- .chip.active:hover {
300
- background: var(--cocoa-xl);
301
- }
302
-
303
- .chip i {
304
- font-size: 9px;
305
- }
306
-
307
- .hidden-chip-container {
308
- height: 0;
309
- opacity: 0;
310
- overflow: hidden;
311
- margin-top: 0;
312
- padding-top: 0;
313
- border-top: none;
314
- }
315
-
316
- /* Toast Notifications */
317
- #toast-container {
318
- position: fixed;
319
- bottom: 20px;
320
- right: 20px;
321
- z-index: 10000;
322
- display: flex;
323
- flex-direction: column;
324
- gap: 8px;
325
- pointer-events: none;
326
- }
327
- .toast {
328
- background: #111;
329
- border: 1px solid #2a2a2a;
330
- color: #f0ece6;
331
- font-size: 11px;
332
- font-weight: 600;
333
- padding: 10px 18px;
334
- border-radius: 10px;
335
- display: flex;
336
- align-items: center;
337
- gap: 8px;
338
- pointer-events: auto;
339
- animation: toastIn 0.3s ease-out;
340
- max-width: 320px;
341
- }
342
- .toast.toast-out { animation: toastOut 0.3s ease-in forwards; }
343
- .toast-success { border-color: #166534; }
344
- .toast-success i { color: #22c55e; }
345
- .toast-error { border-color: #7f1d1d; }
346
- .toast-error i { color: #ef4444; }
347
- .toast-info i { color: var(--cocoa-l); }
348
- @keyframes toastIn { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } }
349
- @keyframes toastOut { from { opacity: 1; } to { opacity: 0; transform: translateX(40px); } }
350
-
351
- /* Stats Empty State */
352
- .stats-empty-overlay {
353
- position: absolute;
354
- inset: 0;
355
- z-index: 50;
356
- display: flex;
357
- flex-direction: column;
358
- align-items: center;
359
- justify-content: center;
360
- background: rgba(10, 10, 10, 0.85);
361
- backdrop-filter: blur(8px);
362
- border-radius: 12px;
363
- }
364
-
365
- /* Mobile Responsive */
366
- @media (max-width: 1024px) {
367
- aside.w-60 { display: none; }
368
- .mobile-nav { display: flex !important; }
369
- #tab-overview { grid-template-columns: repeat(1, 1fr) !important; }
370
- #tab-overview > div { grid-column: span 1 !important; }
371
- }
372
- @media (max-width: 768px) {
373
- .grid-cols-3 { grid-template-columns: 1fr !important; }
374
- .grid-cols-2 { grid-template-columns: 1fr !important; }
375
- main { padding: 8px !important; }
376
- }
377
-
378
- /* Feedback form */
379
- .fb-textarea {
380
- background: #111;
381
- border: 1px solid #2a2a2a;
382
- border-radius: 8px;
383
- color: #f0ece6;
384
- font-size: 12px;
385
- padding: 12px;
386
- width: 100%;
387
- min-height: 120px;
388
- resize: vertical;
389
- font-family: 'Inter', sans-serif;
390
- }
391
- .fb-textarea:focus { outline: none; border-color: var(--cocoa-l); }
392
- .fb-select {
393
- background: #111;
394
- border: 1px solid #2a2a2a;
395
- border-radius: 8px;
396
- color: #f0ece6;
397
- font-size: 11px;
398
- padding: 8px 12px;
399
- width: 100%;
400
- font-family: 'Inter', sans-serif;
401
- }
402
- .fb-select:focus { outline: none; border-color: var(--cocoa-l); }
403
- .fb-stars { display: flex; gap: 6px; }
404
- .fb-star {
405
- font-size: 22px;
406
- color: #333;
407
- cursor: pointer;
408
- transition: color 0.15s;
409
- }
410
- .fb-star.active, .fb-star:hover { color: var(--cocoa-l); }
411
- .fb-chip {
412
- background: #050505;
413
- border: 1px solid #222;
414
- border-radius: 8px;
415
- color: #666;
416
- font-size: 10px;
417
- font-weight: 700;
418
- padding: 12px;
419
- cursor: pointer;
420
- transition: all 0.2s ease;
421
- text-align: center;
422
- text-transform: uppercase;
423
- tracking-widest: 0.05em;
424
- }
425
- .fb-chip:hover { border-color: #444; color: #999; }
426
- .fb-chip.active {
427
- border-color: var(--cocoa-l);
428
- background: #111;
429
- color: #fff;
430
- box-shadow: 0 0 15px rgba(200, 154, 108, 0.15);
431
- }
432
- .fb-emoji-btn {
433
- background: #111;
434
- border: 1px solid #2a2a2a;
435
- border-radius: 8px;
436
- color: #555;
437
- flex: 1;
438
- text-align: center;
439
- padding: 10px 4px;
440
- cursor: pointer;
441
- transition: all 0.2s ease;
442
- }
443
- .fb-emoji-btn:hover { border-color: #444; color: #888; }
444
- .fb-emoji-btn.active {
445
- border-color: var(--cocoa-l);
446
- background: #1a1a1a;
447
- color: var(--cocoa-l);
448
- box-shadow: 0 0 15px rgba(200, 154, 108, 0.15);
449
- }
450
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =============================================
2
+ UrbanFlow — vehicles.css (Mobile-First)
3
+ Desktop layout preserved exactly.
4
+ Mobile: bottom nav, touch targets, stacked cards.
5
+ ============================================= */
6
+
7
+ :root {
8
+ --cocoa: #8b5e3c;
9
+ --cocoa-l: #c89a6c;
10
+ --cocoa-xl: #d4b08a;
11
+ --mob-nav-h: 68px;
12
+ /* bottom nav height on mobile */
13
+ }
14
+
15
+ *,
16
+ *::before,
17
+ *::after {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ .hidden {
22
+ display: none !important;
23
+ }
24
+
25
+ html {
26
+ overflow: hidden;
27
+ height: 100%;
28
+ }
29
+
30
+ body {
31
+ font-family: 'Montserrat', sans-serif;
32
+ background-color: #000000;
33
+ color: #f0ece6;
34
+ -webkit-tap-highlight-color: transparent;
35
+ overscroll-behavior: none;
36
+ }
37
+
38
+ .mono-font {
39
+ font-family: 'JetBrains Mono', monospace;
40
+ }
41
+
42
+ /* ---- Scrollbar: hide globally on mobile, keep #class-breakdown visible ---- */
43
+ @media (max-width: 1023px) {
44
+ * {
45
+ scrollbar-width: none;
46
+ -ms-overflow-style: none;
47
+ }
48
+
49
+ *::-webkit-scrollbar {
50
+ display: none;
51
+ }
52
+
53
+ /* Vehicle Classification section keeps its scrollbar on mobile */
54
+ #class-breakdown {
55
+ scrollbar-width: thin !important;
56
+ -ms-overflow-style: auto !important;
57
+ }
58
+
59
+ #class-breakdown::-webkit-scrollbar {
60
+ display: block !important;
61
+ width: 4px !important;
62
+ }
63
+
64
+ #class-breakdown::-webkit-scrollbar-track {
65
+ background: #000000 !important;
66
+ }
67
+
68
+ #class-breakdown::-webkit-scrollbar-thumb {
69
+ background: #222222 !important;
70
+ border-radius: 4px !important;
71
+ }
72
+
73
+ #class-breakdown::-webkit-scrollbar-thumb:hover {
74
+ background: #333333 !important;
75
+ }
76
+ }
77
+
78
+ /* ---- Notification Glow ---- */
79
+ @keyframes glow-green {
80
+ 0% {
81
+ color: #f0ece6;
82
+ filter: drop-shadow(0 0 0px #4ade80);
83
+ }
84
+
85
+ 50% {
86
+ color: #4ade80;
87
+ filter: drop-shadow(0 0 8px #4ade80);
88
+ }
89
+
90
+ 100% {
91
+ color: #f0ece6;
92
+ filter: drop-shadow(0 0 0px #4ade80);
93
+ }
94
+ }
95
+
96
+ .notify-glow i {
97
+ animation: glow-green 1.5s infinite ease-in-out !important;
98
+ }
99
+
100
+ /* ---- Info tooltip ---- */
101
+ .info-wrap {
102
+ position: relative;
103
+ display: inline-flex;
104
+ align-items: center;
105
+ margin-left: 6px;
106
+ }
107
+
108
+ .info-btn {
109
+ display: inline-flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ width: 18px;
113
+ /* slightly larger for touch */
114
+ height: 18px;
115
+ border-radius: 50%;
116
+ background: #444444 !important;
117
+ color: #ffffff !important;
118
+ font-size: 8px;
119
+ cursor: pointer;
120
+ transition: all 0.2s ease;
121
+ }
122
+
123
+ .info-btn:hover,
124
+ .info-btn:active {
125
+ background: #666666 !important;
126
+ }
127
+
128
+ .info-tip {
129
+ display: none;
130
+ position: fixed;
131
+ z-index: 9999;
132
+ background: #0a0a0a;
133
+ color: #aaaaaa;
134
+ font-size: 10px;
135
+ font-weight: 500;
136
+ line-height: 1.4;
137
+ padding: 8px 12px;
138
+ border-radius: 6px;
139
+ max-width: 240px;
140
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8);
141
+ border: 1px solid #222222;
142
+ pointer-events: none;
143
+ text-transform: none;
144
+ letter-spacing: normal;
145
+ }
146
+
147
+ /* ---- Mobile Top Bar ---- */
148
+ .mobile-top-bar {
149
+ display: none;
150
+ }
151
+
152
+ @media (max-width: 1023px) {
153
+ .mobile-top-bar {
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ position: fixed;
158
+ top: 0;
159
+ left: 0;
160
+ right: 0;
161
+ height: 58px;
162
+ background: #000000;
163
+ border-bottom: 1px solid #1a1a1a;
164
+ z-index: 35;
165
+ flex-shrink: 0;
166
+ }
167
+
168
+ #legal-menu {
169
+ animation: menuFadeIn 0.2s ease-out forwards;
170
+ transform-origin: top right;
171
+ }
172
+
173
+ @keyframes menuFadeIn {
174
+ from {
175
+ opacity: 0;
176
+ transform: translateY(-10px) scale(0.95);
177
+ }
178
+
179
+ to {
180
+ opacity: 1;
181
+ transform: translateY(0) scale(1);
182
+ }
183
+ }
184
+ }
185
+
186
+ /* ---- Sidebar nav states ---- */
187
+ .nav-item-active {
188
+ background-color: #111111 !important;
189
+ color: var(--cocoa-xl) !important;
190
+ border-left: 2px solid var(--cocoa-l) !important;
191
+ }
192
+
193
+ .nav-item-inactive {
194
+ color: #555555 !important;
195
+ }
196
+
197
+ .nav-item-inactive:hover {
198
+ color: #f0ece6 !important;
199
+ background-color: #050505 !important;
200
+ }
201
+
202
+ /* ---- Card overrides ---- */
203
+ .bg-white {
204
+ background-color: #0a0a0a !important;
205
+ }
206
+
207
+ .border-slate-200,
208
+ .border-slate-100,
209
+ .border-slate-50,
210
+ .border-neutral-800,
211
+ .border-neutral-900 {
212
+ border-color: #2a2a2a !important;
213
+ }
214
+
215
+ .bg-slate-50\/50,
216
+ .bg-slate-50,
217
+ .bg-slate-900,
218
+ .bg-neutral-900 {
219
+ background-color: #0c0c0c !important;
220
+ }
221
+
222
+ .text-slate-900,
223
+ .text-slate-800,
224
+ .text-slate-700,
225
+ .text-neutral-900 {
226
+ color: #ffffff !important;
227
+ }
228
+
229
+ .text-slate-600,
230
+ .text-slate-500,
231
+ .text-slate-400,
232
+ .text-neutral-500,
233
+ .text-neutral-400 {
234
+ color: #888888 !important;
235
+ }
236
+
237
+ .shadow-sm {
238
+ box-shadow: none !important;
239
+ }
240
+
241
+ /* ---- Toggle control ---- */
242
+ .toggle-track {
243
+ width: 36px;
244
+ /* slightly wider for touch */
245
+ height: 20px;
246
+ border-radius: 999px;
247
+ background: #1a1a1a;
248
+ border: 1px solid #333;
249
+ position: relative;
250
+ cursor: pointer;
251
+ flex-shrink: 0;
252
+ transition: background 0.2s ease;
253
+ }
254
+
255
+ .toggle-track.active {
256
+ background: #c89a6c !important;
257
+ border-color: #c89a6c !important;
258
+ }
259
+
260
+ .toggle-thumb {
261
+ width: 16px;
262
+ height: 16px;
263
+ border-radius: 50%;
264
+ background: #555555;
265
+ position: absolute;
266
+ top: 2px;
267
+ left: 2px;
268
+ transition: all 0.2s ease;
269
+ }
270
+
271
+ .toggle-track.active .toggle-thumb {
272
+ transform: translateX(16px);
273
+ background: #ffffff;
274
+ /* pure white for contrast on gold track */
275
+ }
276
+
277
+ /* ---- Custom select ---- */
278
+ .custom-select {
279
+ appearance: none;
280
+ background-color: #111111;
281
+ border: 1px solid #222222;
282
+ border-radius: 6px;
283
+ padding: 4px 24px 4px 10px;
284
+ font-size: 11px;
285
+ font-weight: 600;
286
+ color: #ffffff;
287
+ outline: none;
288
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23666666'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z'/%3E%3C/svg%3E");
289
+ background-repeat: no-repeat;
290
+ background-position: right 8px center;
291
+ background-size: 12px;
292
+ }
293
+
294
+ /* ---- Stepper ---- */
295
+ .s-stepper {
296
+ display: inline-flex;
297
+ border: 1px solid #222222;
298
+ border-radius: 6px;
299
+ background: #111111;
300
+ overflow: hidden;
301
+ flex-shrink: 0;
302
+ }
303
+
304
+ .s-stepper button {
305
+ padding: 8px 12px;
306
+ /* larger touch target than original 4px 8px */
307
+ color: #666666;
308
+ font-size: 14px;
309
+ min-width: 36px;
310
+ min-height: 36px;
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: center;
314
+ }
315
+
316
+ .s-stepper button:hover,
317
+ .s-stepper button:active {
318
+ background: #1a1a1a;
319
+ color: #ffffff;
320
+ }
321
+
322
+ .s-stepper .s-val {
323
+ min-width: 44px;
324
+ text-align: center;
325
+ font-family: 'JetBrains Mono', monospace;
326
+ font-size: 12px;
327
+ font-weight: 700;
328
+ color: #ffffff;
329
+ padding: 4px 0;
330
+ border-left: 1px solid #222222;
331
+ border-right: 1px solid #222222;
332
+ display: flex;
333
+ align-items: center;
334
+ justify-content: center;
335
+ }
336
+
337
+ /* ---- Settings row ---- */
338
+ .s-row {
339
+ display: flex;
340
+ align-items: center;
341
+ justify-content: space-between;
342
+ padding: 14px 0;
343
+ /* slightly more vertical padding */
344
+ border-bottom: 1px solid #1a1a1a;
345
+ gap: 12px;
346
+ }
347
+
348
+ .s-row:last-child {
349
+ border-bottom: none;
350
+ }
351
+
352
+ .s-row>div:first-child {
353
+ flex: 1;
354
+ min-width: 0;
355
+ }
356
+
357
+ /* ---- Progress bar ---- */
358
+ #proc-bar {
359
+ background-color: var(--cocoa-l) !important;
360
+ }
361
+
362
+ #proc-label {
363
+ color: #ffffff !important;
364
+ }
365
+
366
+ /* ---- Disabled rows ---- */
367
+ .s-row.disabled {
368
+ opacity: 0.65 !important;
369
+ }
370
+
371
+ .s-row.disabled .s-stepper,
372
+ .s-row.disabled .custom-select,
373
+ .s-row.disabled .toggle-track,
374
+ .s-row.disabled .chip-container,
375
+ .s-row.disabled .uf-select-wrap,
376
+ .s-row.disabled .uf-select-trigger {
377
+ pointer-events: none !important;
378
+ opacity: 0.5 !important;
379
+ }
380
+
381
+ /* Force-collapse the dropdown panel when row is locked */
382
+ .s-row.disabled .uf-select-dropdown {
383
+ display: none !important;
384
+ }
385
+
386
+ .s-row.disabled .info-wrap {
387
+ pointer-events: auto !important;
388
+ opacity: 1 !important;
389
+ }
390
+
391
+ #btn-start-processing {
392
+ font-family: 'Montserrat', sans-serif !important;
393
+ }
394
+
395
+ /* ---- Chips ---- */
396
+ .chip-container {
397
+ display: flex;
398
+ flex-wrap: wrap;
399
+ gap: 8px;
400
+ margin-top: 12px;
401
+ padding-top: 12px;
402
+ border-top: 1px solid #1a1a1a;
403
+ transition: all 0.3s ease;
404
+ }
405
+
406
+ .chip {
407
+ display: inline-flex;
408
+ align-items: center;
409
+ gap: 6px;
410
+ padding: 8px 14px;
411
+ /* larger than original 6px 14px */
412
+ border-radius: 9999px;
413
+ font-size: 10px;
414
+ font-weight: 700;
415
+ cursor: pointer;
416
+ transition: all 0.2s ease;
417
+ user-select: none;
418
+ border: 1px solid #333333;
419
+ background: rgba(255, 255, 255, 0.03);
420
+ color: #888888;
421
+ min-height: 36px;
422
+ }
423
+
424
+ .chip.active {
425
+ background: var(--cocoa-l);
426
+ color: #000000;
427
+ border-color: var(--cocoa-l);
428
+ }
429
+
430
+ .chip.frozen {
431
+ background: rgba(255, 255, 255, 0.4);
432
+ color: #000000;
433
+ border-color: transparent;
434
+ cursor: default !important;
435
+ pointer-events: none;
436
+ }
437
+
438
+ .chip:hover {
439
+ border-color: #666666;
440
+ }
441
+
442
+ .chip.active:hover {
443
+ background: var(--cocoa-xl);
444
+ }
445
+
446
+ .chip i {
447
+ font-size: 9px;
448
+ }
449
+
450
+ .hidden-chip-container {
451
+ display: none !important;
452
+ margin: 0 !important;
453
+ padding: 0 !important;
454
+ height: 0 !important;
455
+ }
456
+
457
+ /* ---- Toast ---- */
458
+ #toast-container {
459
+ position: fixed;
460
+ bottom: calc(var(--mob-nav-h) + 12px);
461
+ /* above bottom nav on mobile */
462
+ left: 50%;
463
+ transform: translateX(-50%);
464
+ z-index: 10000;
465
+ display: flex;
466
+ flex-direction: column;
467
+ align-items: center;
468
+ gap: 8px;
469
+ pointer-events: none;
470
+ width: 90%;
471
+ max-width: 360px;
472
+ }
473
+
474
+ .toast {
475
+ background: #111;
476
+ border: 1px solid #2a2a2a;
477
+ color: #f0ece6;
478
+ font-size: 11px;
479
+ font-weight: 600;
480
+ padding: 12px 18px;
481
+ border-radius: 10px;
482
+ display: flex;
483
+ align-items: center;
484
+ gap: 8px;
485
+ pointer-events: auto;
486
+ animation: toastIn 0.3s ease-out;
487
+ width: 100%;
488
+ }
489
+
490
+ .toast.toast-out {
491
+ animation: toastOut 0.3s ease-in forwards;
492
+ }
493
+
494
+ .toast-success {
495
+ border-color: #166534;
496
+ }
497
+
498
+ .toast-success i {
499
+ color: #22c55e;
500
+ }
501
+
502
+ .toast-error {
503
+ border-color: #7f1d1d;
504
+ }
505
+
506
+ .toast-error i {
507
+ color: #ef4444;
508
+ }
509
+
510
+ .toast-info i {
511
+ color: var(--cocoa-l);
512
+ }
513
+
514
+ @keyframes toastIn {
515
+ from {
516
+ opacity: 0;
517
+ transform: translateY(20px);
518
+ }
519
+
520
+ to {
521
+ opacity: 1;
522
+ transform: translateY(0);
523
+ }
524
+ }
525
+
526
+ @keyframes toastOut {
527
+ from {
528
+ opacity: 1;
529
+ }
530
+
531
+ to {
532
+ opacity: 0;
533
+ transform: translateY(20px);
534
+ }
535
+ }
536
+
537
+ /* ---- Stats empty overlay ---- */
538
+ .stats-empty-overlay {
539
+ position: absolute;
540
+ inset: 0;
541
+ z-index: 50;
542
+ display: flex;
543
+ flex-direction: column;
544
+ align-items: center;
545
+ justify-content: center;
546
+ background: rgba(10, 10, 10, 0.85);
547
+ backdrop-filter: blur(8px);
548
+ border-radius: 12px;
549
+ }
550
+
551
+ /* ---- Feedback form ---- */
552
+ .fb-textarea {
553
+ background: #111;
554
+ border: 1px solid #2a2a2a;
555
+ border-radius: 8px;
556
+ color: #f0ece6;
557
+ font-size: 12px;
558
+ padding: 12px;
559
+ width: 100%;
560
+ min-height: 120px;
561
+ resize: vertical;
562
+ font-family: 'Inter', sans-serif;
563
+ }
564
+
565
+ .fb-textarea:focus {
566
+ outline: none;
567
+ border-color: var(--cocoa-l);
568
+ }
569
+
570
+ .fb-select {
571
+ background: #111;
572
+ border: 1px solid #2a2a2a;
573
+ border-radius: 8px;
574
+ color: #f0ece6;
575
+ font-size: 11px;
576
+ padding: 10px 12px;
577
+ /* taller for touch */
578
+ width: 100%;
579
+ font-family: 'Inter', sans-serif;
580
+ min-height: 44px;
581
+ }
582
+
583
+ .fb-select:focus {
584
+ outline: none;
585
+ border-color: var(--cocoa-l);
586
+ }
587
+
588
+ .fb-stars {
589
+ display: flex;
590
+ gap: 8px;
591
+ }
592
+
593
+ .fb-star {
594
+ font-size: 28px;
595
+ /* larger for mobile tapping */
596
+ color: #333;
597
+ cursor: pointer;
598
+ transition: color 0.15s;
599
+ min-width: 36px;
600
+ min-height: 36px;
601
+ display: flex;
602
+ align-items: center;
603
+ justify-content: center;
604
+ }
605
+
606
+ .fb-star.active,
607
+ .fb-star:hover {
608
+ color: var(--cocoa-l);
609
+ }
610
+
611
+ .fb-chip {
612
+ background: #050505;
613
+ border: 1px solid #222;
614
+ border-radius: 8px;
615
+ color: #666;
616
+ font-size: 10px;
617
+ font-weight: 700;
618
+ padding: 14px 12px;
619
+ /* taller for touch */
620
+ cursor: pointer;
621
+ transition: all 0.2s ease;
622
+ text-align: center;
623
+ text-transform: uppercase;
624
+ min-height: 44px;
625
+ display: flex;
626
+ align-items: center;
627
+ justify-content: center;
628
+ }
629
+
630
+ .fb-chip:hover {
631
+ border-color: #444;
632
+ color: #999;
633
+ }
634
+
635
+ .fb-chip.active {
636
+ border-color: var(--cocoa-l);
637
+ background: #111;
638
+ color: #fff;
639
+ box-shadow: 0 0 15px rgba(200, 154, 108, 0.15);
640
+ }
641
+
642
+ .fb-emoji-btn {
643
+ background: #111;
644
+ border: 1px solid #2a2a2a;
645
+ border-radius: 8px;
646
+ color: #555;
647
+ flex: 1;
648
+ text-align: center;
649
+ padding: 12px 4px;
650
+ /* taller */
651
+ cursor: pointer;
652
+ transition: all 0.2s ease;
653
+ min-height: 64px;
654
+ display: flex;
655
+ flex-direction: column;
656
+ align-items: center;
657
+ justify-content: center;
658
+ }
659
+
660
+ .fb-emoji-btn:hover {
661
+ border-color: #444;
662
+ color: #888;
663
+ }
664
+
665
+ .fb-emoji-btn.active {
666
+ border-color: var(--cocoa-l);
667
+ background: #1a1a1a;
668
+ color: var(--cocoa-l);
669
+ box-shadow: 0 0 15px rgba(200, 154, 108, 0.15);
670
+ }
671
+
672
+ /* =============================================
673
+ DESKTOP (≥1024px) — original layout intact
674
+ ============================================= */
675
+ @media (min-width: 1024px) {
676
+
677
+ /* Sidebar visible */
678
+ aside.w-60 {
679
+ display: flex !important;
680
+ }
681
+
682
+ /* Top mobile nav hidden */
683
+ .mobile-nav {
684
+ display: none !important;
685
+ }
686
+
687
+ /* Bottom mobile nav hidden */
688
+ .mobile-bottom-nav {
689
+ display: none !important;
690
+ }
691
+
692
+ /* Main — no bottom padding needed */
693
+ main {
694
+ padding-bottom: 1rem !important;
695
+ }
696
+
697
+ /* Toast — desktop position: bottom-right */
698
+ #toast-container {
699
+ bottom: 20px;
700
+ left: unset;
701
+ right: 20px;
702
+ transform: none;
703
+ width: auto;
704
+ align-items: flex-end;
705
+ }
706
+
707
+ /* Settings — 2 column grid */
708
+ #tab-settings .grid {
709
+ grid-template-columns: repeat(2, 1fr) !important;
710
+ }
711
+
712
+ /* Run details — multi-column grids preserved */
713
+ #run-results-content {
714
+ grid-template-columns: repeat(3, 1fr) !important;
715
+ }
716
+
717
+ .grid-cols-2 {
718
+ grid-template-columns: repeat(2, 1fr) !important;
719
+ }
720
+
721
+ .grid-cols-3 {
722
+ grid-template-columns: repeat(3, 1fr) !important;
723
+ }
724
+
725
+ /* Reports grid */
726
+ #reports-grid,
727
+ #reports-pending {
728
+ grid-template-columns: repeat(2, 1fr) !important;
729
+ }
730
+
731
+ /* About grid */
732
+ #tab-about .grid.grid-cols-3 {
733
+ grid-template-columns: repeat(3, 1fr) !important;
734
+ }
735
+
736
+ /* Post-process cards */
737
+ #post-process-cards {
738
+ grid-template-columns: repeat(2, 1fr) !important;
739
+ }
740
+
741
+ /* Insights panel */
742
+ #insights-panel .grid {
743
+ grid-template-columns: repeat(2, 1fr) !important;
744
+ }
745
+ }
746
+
747
+ /* =============================================
748
+ MOBILE (< 1024px) — full mobile overhaul
749
+ ============================================= */
750
+ @media (max-width: 1023px) {
751
+
752
+ /* --- Hide desktop sidebar --- */
753
+ aside.w-60 {
754
+ display: none !important;
755
+ }
756
+
757
+ /* --- Hide old top mobile nav bar --- */
758
+ .mobile-nav {
759
+ display: none !important;
760
+ }
761
+
762
+ /* --- Body layout --- */
763
+ body {
764
+ height: 100dvh;
765
+ /* dynamic viewport height — accounts for mobile browser chrome */
766
+ overflow: hidden;
767
+ }
768
+
769
+ /* --- Main content — room for top and bottom nav --- */
770
+ main {
771
+ padding: 70px 12px calc(var(--mob-nav-h) + 8px) 12px !important;
772
+ gap: 12px !important;
773
+ display: flex !important;
774
+ flex-direction: column !important;
775
+ height: 100dvh !important;
776
+ }
777
+
778
+ /* --- Tab Scrolling Fixes — force flex-1 to push progress bar down --- */
779
+ #tab-about,
780
+ #tab-overview,
781
+ #tab-run-details,
782
+ #tab-reports,
783
+ #tab-settings,
784
+ #tab-help,
785
+ #tab-feedback {
786
+ flex: 1 !important;
787
+ min-height: 0 !important;
788
+ padding-bottom: 20px !important;
789
+ overscroll-behavior: contain;
790
+ -webkit-overflow-scrolling: touch;
791
+ overflow-y: auto !important;
792
+ }
793
+
794
+ /* --- About tab specific spacing --- */
795
+ #tab-about .space-y-8 {
796
+ gap: 16px !important;
797
+ }
798
+
799
+ #tab-about .pt-8 {
800
+ padding-top: 16px !important;
801
+ }
802
+
803
+ #tab-overview:not(.hidden) {
804
+ display: flex !important;
805
+ flex-direction: column !important;
806
+ overflow-y: auto !important;
807
+ overflow-x: hidden !important;
808
+ -webkit-overflow-scrolling: touch;
809
+ overscroll-behavior: contain;
810
+ padding-bottom: calc(var(--mob-nav-h) + 24px) !important;
811
+ gap: 16px !important;
812
+ }
813
+
814
+ #tab-overview>div:not(#stats-empty-state) {
815
+ grid-column: span 1 !important;
816
+ min-height: 280px;
817
+ flex-shrink: 0;
818
+ }
819
+
820
+ .stats-empty-overlay {
821
+ position: fixed !important;
822
+ top: 58px;
823
+ /* below mobile top bar */
824
+ left: 0;
825
+ right: 0;
826
+ bottom: var(--mob-nav-h);
827
+ height: auto !important;
828
+ z-index: 100;
829
+ background: rgba(0, 0, 0, 0.98);
830
+ display: flex;
831
+ flex-direction: column;
832
+ align-items: center;
833
+ justify-content: center;
834
+ }
835
+
836
+ /* CRITICAL: hide overlay when its parent tab is hidden */
837
+ #tab-overview.hidden .stats-empty-overlay {
838
+ display: none !important;
839
+ }
840
+
841
+ /* Hide charts when curtain is up to prevent scroll jank */
842
+ #tab-overview.curtain-active {
843
+ overflow: hidden !important;
844
+ }
845
+
846
+ /* --- Settings tab — tighter layout --- */
847
+ #tab-settings>div[class*="grid"] {
848
+ display: flex !important;
849
+ flex-direction: column !important;
850
+ gap: 12px !important;
851
+ }
852
+
853
+ #tab-settings {
854
+ overflow-x: hidden !important;
855
+ }
856
+
857
+ /* Collapse chip panel completely when not shown — removes gap */
858
+ #chip-selector.hidden-chip-container {
859
+ display: none !important;
860
+ margin: 0 !important;
861
+ padding: 0 !important;
862
+ height: 0 !important;
863
+ }
864
+
865
+ /* When visible, give it breathing room */
866
+ #chip-selector:not(.hidden-chip-container) {
867
+ display: flex !important;
868
+ flex-wrap: wrap !important;
869
+ gap: 8px !important;
870
+ margin-top: 12px !important;
871
+ padding: 0 !important;
872
+ }
873
+
874
+ /* Ensure all s-row items are uniform flex rows */
875
+ .s-row {
876
+ display: flex !important;
877
+ align-items: center !important;
878
+ justify-content: space-between !important;
879
+ padding: 14px 0 !important;
880
+ border-bottom: 1px solid #1a1a1a !important;
881
+ gap: 12px !important;
882
+ }
883
+
884
+ .s-row:last-child {
885
+ border-bottom: none !important;
886
+ }
887
+
888
+ /* Never let mobile flex override Tailwind .hidden utility */
889
+ .s-row.hidden {
890
+ display: none !important;
891
+ }
892
+
893
+ /* The annotated video row overrides — it is flex-col on desktop; force flex-row on mobile */
894
+ .s-row[data-param="annotated"] {
895
+ flex-direction: column !important;
896
+ align-items: stretch !important;
897
+ }
898
+
899
+ .s-row[data-param="annotated"]>.flex {
900
+ width: 100% !important;
901
+ }
902
+
903
+ .s-stepper {
904
+ width: 140px !important;
905
+ /* Compact fixed width */
906
+ scale: 0.9;
907
+ transform-origin: right;
908
+ display: inline-flex !important;
909
+ }
910
+
911
+ .toggle-track {
912
+ width: 36px !important;
913
+ scale: 0.9;
914
+ transform-origin: right;
915
+ }
916
+
917
+ @media (max-width: 480px) {
918
+ .s-row {
919
+ flex-direction: row !important;
920
+ align-items: center !important;
921
+ justify-content: space-between !important;
922
+ /* align toggles on right for discipline */
923
+ gap: 12px !important;
924
+ padding: 10px 16px !important;
925
+ width: 100% !important;
926
+ box-sizing: border-box !important;
927
+ }
928
+
929
+ .s-row[data-param="annotated"] {
930
+ flex-direction: column !important;
931
+ align-items: stretch !important;
932
+ padding: 12px 16px !important;
933
+ /* matches normal s-row padding */
934
+ }
935
+
936
+ .s-row:not([data-param="annotated"]) {
937
+ padding: 12px 16px !important;
938
+ }
939
+
940
+ #tab-run-details .p-8 {
941
+ padding: 20px !important;
942
+ }
943
+
944
+ #run-results-content {
945
+ grid-template-columns: 1fr !important;
946
+ gap: 16px !important;
947
+ }
948
+
949
+ #panel-video .flex,
950
+ #panel-perf .flex,
951
+ #panel-model .flex,
952
+ #panel-infer .flex {
953
+ padding-bottom: 8px !important;
954
+ }
955
+
956
+ .s-row .info-wrap {
957
+ display: inline-flex !important;
958
+ vertical-align: middle;
959
+ }
960
+
961
+ .s-row>div:first-child {
962
+ width: auto !important;
963
+ max-width: 75% !important;
964
+ flex: 1 !important;
965
+ }
966
+
967
+ .toggle-track {
968
+ width: 36px !important;
969
+ min-width: 36px !important;
970
+ height: 20px !important;
971
+ flex-shrink: 0 !important;
972
+ display: block !important;
973
+ position: relative !important;
974
+ }
975
+
976
+ #run-results-card .text-[10px] {
977
+ font-size: 9px !important;
978
+ letter-spacing: 0.05em !important;
979
+ }
980
+
981
+ .s-row>.s-stepper {
982
+ width: 130px !important;
983
+ flex-shrink: 0 !important;
984
+ display: inline-flex !important;
985
+ flex-direction: row !important;
986
+ }
987
+
988
+ .chip-container {
989
+ display: grid !important;
990
+ grid-template-columns: 1fr 1fr !important;
991
+ gap: 6px !important;
992
+ margin-top: 12px !important;
993
+ padding: 10px !important;
994
+ background: rgba(255, 255, 255, 0.03);
995
+ border-radius: 8px;
996
+ border: 1px solid #1a1a1a;
997
+ width: 100% !important;
998
+ box-sizing: border-box !important;
999
+ }
1000
+
1001
+ .chip {
1002
+ padding: 6px !important;
1003
+ font-size: 9px !important;
1004
+ min-height: 32px !important;
1005
+ border-radius: 6px !important;
1006
+ justify-content: center !important;
1007
+ width: 100% !important;
1008
+ white-space: nowrap !important;
1009
+ }
1010
+
1011
+ .s-stepper {
1012
+ width: 130px !important;
1013
+ min-width: 130px !important;
1014
+ display: inline-flex !important;
1015
+ flex-direction: row !important;
1016
+ align-items: center !important;
1017
+ justify-content: space-between !important;
1018
+ transform-origin: right !important;
1019
+ }
1020
+
1021
+ .toggle-track {
1022
+ transform-origin: right !important;
1023
+ }
1024
+ }
1025
+
1026
+ /* --- Progress bar wrapper — remove extra margin to fix huge gap --- */
1027
+ #progress-bar-wrapper {
1028
+ width: 100% !important;
1029
+ max-width: 100% !important;
1030
+ box-sizing: border-box !important;
1031
+ margin-top: auto !important;
1032
+ margin-bottom: 4px !important;
1033
+ padding: 8px 12px !important;
1034
+ flex-direction: column !important;
1035
+ align-items: flex-start !important;
1036
+ gap: 6px !important;
1037
+ position: relative;
1038
+ z-index: 10;
1039
+ }
1040
+
1041
+ #progress-bar-wrapper>div:first-child {
1042
+ width: 100% !important;
1043
+ flex: 1 !important;
1044
+ min-width: 0 !important;
1045
+ margin-right: 0 !important;
1046
+ }
1047
+
1048
+ #progress-bar-wrapper>div:last-child {
1049
+ width: 100% !important;
1050
+ justify-content: space-between !important;
1051
+ font-size: 10px !important;
1052
+ }
1053
+
1054
+ /* --- All other grids collapse to single column --- */
1055
+ .grid-cols-3,
1056
+ .grid-cols-2,
1057
+ .lg\:grid-cols-2,
1058
+ .xl\:grid-cols-3 {
1059
+ grid-template-columns: 1fr !important;
1060
+ }
1061
+
1062
+ /* --- Run details tab --- */
1063
+ #run-results-content {
1064
+ grid-template-columns: 1fr !important;
1065
+ }
1066
+
1067
+ #tab-run-details .grid-cols-2,
1068
+ #tab-run-details .grid-cols-3 {
1069
+ grid-template-columns: 1fr !important;
1070
+ }
1071
+
1072
+ /* --- Reports grid --- */
1073
+ #reports-grid,
1074
+ #reports-pending {
1075
+ grid-template-columns: 1fr !important;
1076
+ }
1077
+
1078
+ /* --- About tab grid --- */
1079
+ #tab-about .grid.grid-cols-3 {
1080
+ grid-template-columns: 1fr !important;
1081
+ }
1082
+
1083
+ /* --- Post-process cards --- */
1084
+ #post-process-cards {
1085
+ grid-template-columns: 1fr !important;
1086
+ }
1087
+
1088
+ /* --- Insights panel --- */
1089
+ #insights-panel .grid {
1090
+ grid-template-columns: 1fr !important;
1091
+ }
1092
+
1093
+ /* --- Feedback tab --- */
1094
+ #tab-feedback .grid {
1095
+ grid-template-columns: 1fr !important;
1096
+ }
1097
+
1098
+ /* --- About tab cards --- */
1099
+ #tab-about .bg-black.border.rounded-xl {
1100
+ padding: 20px !important;
1101
+ }
1102
+
1103
+ /* --- Stepper — ensure full tap area --- */
1104
+ .s-stepper button {
1105
+ padding: 10px 14px;
1106
+ min-width: 40px;
1107
+ min-height: 40px;
1108
+ }
1109
+
1110
+ /* --- s-row label text — allow wrap --- */
1111
+ .s-row>div:first-child .text-xs {
1112
+ font-size: 11px;
1113
+ }
1114
+
1115
+ /* --- Help accordion buttons --- */
1116
+ #tab-help button.w-full {
1117
+ min-height: 52px;
1118
+ padding: 14px 16px !important;
1119
+ }
1120
+
1121
+ /* --- Feedback priority chips grid --- */
1122
+ #fb-priorities {
1123
+ grid-template-columns: 1fr !important;
1124
+ }
1125
+
1126
+ /* --- Keyboard shortcut modal --- */
1127
+ #appModal-shortcutsModal>div {
1128
+ max-width: 95% !important;
1129
+ padding: 20px !important;
1130
+ }
1131
+
1132
+ /* --- Privacy / Terms modals --- */
1133
+ [id^="appModal-"]>div {
1134
+ max-width: 95% !important;
1135
+ max-height: 80dvh !important;
1136
+ overflow-y: auto !important;
1137
+ }
1138
+
1139
+ #tab-overview>div:last-child {
1140
+ min-height: 300px !important;
1141
+ padding-bottom: 4px !important;
1142
+ margin-bottom: 0 !important;
1143
+ }
1144
+
1145
+ /* --- Vehicle Classification Internal Scroll --- */
1146
+ #tab-overview>div:nth-child(4) {
1147
+ max-height: 380px !important;
1148
+ display: flex !important;
1149
+ flex-direction: column !important;
1150
+ }
1151
+
1152
+ #tab-overview>div:nth-child(4) #class-breakdown {
1153
+ flex: 1 !important;
1154
+ overflow-y: auto !important;
1155
+ min-height: 0 !important;
1156
+ }
1157
+ }
1158
+
1159
+ /* =============================================
1160
+ BOTTOM NAVIGATION BAR — mobile only
1161
+ ============================================= */
1162
+ .mobile-bottom-nav {
1163
+ display: none;
1164
+ /* hidden by default, shown on mobile */
1165
+ position: fixed;
1166
+ bottom: 0;
1167
+ left: 0;
1168
+ right: 0;
1169
+ height: 68px;
1170
+ background: #000000;
1171
+ border-top: 1px solid #1a1a1a;
1172
+ z-index: 40;
1173
+ align-items: stretch;
1174
+ }
1175
+
1176
+ .mob-nav-item {
1177
+ flex: 1;
1178
+ display: flex;
1179
+ flex-direction: column;
1180
+ align-items: center;
1181
+ justify-content: center;
1182
+ gap: 3px;
1183
+ cursor: pointer;
1184
+ color: #444444;
1185
+ font-size: 0;
1186
+ font-weight: 700;
1187
+ text-transform: uppercase;
1188
+ letter-spacing: 0.05em;
1189
+ transition: color 0.15s ease;
1190
+ border: none;
1191
+ background: none;
1192
+ padding: 8px 2px;
1193
+ -webkit-tap-highlight-color: transparent;
1194
+ }
1195
+
1196
+ .mob-nav-item i {
1197
+ font-size: 22px;
1198
+ transition: color 0.15s ease;
1199
+ }
1200
+
1201
+ .mob-nav-item.active {
1202
+ color: var(--cocoa-l);
1203
+ }
1204
+
1205
+ .mob-nav-item.active i {
1206
+ color: var(--cocoa-l);
1207
+ }
1208
+
1209
+ .mob-nav-item:active {
1210
+ color: var(--cocoa-xl);
1211
+ }
1212
+
1213
+ /* Show bottom nav only on mobile */
1214
+ @media (max-width: 1023px) {
1215
+ .mobile-bottom-nav {
1216
+ display: flex !important;
1217
+ }
1218
+ }
1219
+
1220
+ /* =============================================
1221
+ MEDIUM TABLET (640px–1023px) adjustments
1222
+ ============================================= */
1223
+ @media (min-width: 640px) and (max-width: 1023px) {
1224
+
1225
+ /* 2-column grids on tablet where it fits */
1226
+ #tab-overview>div {
1227
+ min-height: 280px;
1228
+ }
1229
+
1230
+ #reports-grid,
1231
+ #reports-pending {
1232
+ grid-template-columns: repeat(2, 1fr) !important;
1233
+ }
1234
+
1235
+ #fb-priorities {
1236
+ grid-template-columns: repeat(2, 1fr) !important;
1237
+ }
1238
+
1239
+ #tab-about .grid.grid-cols-3 {
1240
+ grid-template-columns: repeat(2, 1fr) !important;
1241
+ }
1242
+ }
1243
+
1244
+ /* =============================================
1245
+ TOUCH DEVICES — remove hover jank
1246
+ ============================================= */
1247
+ @media (hover: none) and (pointer: coarse) {
1248
+ .nav-item-inactive:hover {
1249
+ color: #555555 !important;
1250
+ background-color: transparent !important;
1251
+ }
1252
+
1253
+ .chip:hover {
1254
+ border-color: #333333;
1255
+ }
1256
+
1257
+ .chip.active:hover {
1258
+ background: var(--cocoa-l);
1259
+ }
1260
+
1261
+ .s-stepper button:hover {
1262
+ background: transparent;
1263
+ color: #666666;
1264
+ }
1265
+
1266
+ /* Make all interactive elements minimum 44px tall */
1267
+ button,
1268
+ .fb-emoji-btn,
1269
+ .mob-nav-item {
1270
+ min-height: 44px;
1271
+ }
1272
+ }
1273
+
1274
+ /* ============================================================
1275
+ Custom Select Dropdown (uf-select)
1276
+ Replaces native <select> to prevent OS picker sheet on mobile
1277
+ ============================================================ */
1278
+ .uf-select-wrap {
1279
+ position: relative;
1280
+ display: inline-block;
1281
+ min-width: 110px;
1282
+ }
1283
+
1284
+ .uf-select-wrap.w-full {
1285
+ display: block;
1286
+ width: 100%;
1287
+ }
1288
+
1289
+ .uf-select-trigger {
1290
+ display: flex;
1291
+ align-items: center;
1292
+ justify-content: space-between;
1293
+ gap: 6px;
1294
+ padding: 5px 10px;
1295
+ background: #111111;
1296
+ border: 1px solid #222222;
1297
+ border-radius: 6px;
1298
+ font-size: 11px;
1299
+ font-weight: 600;
1300
+ color: #ffffff;
1301
+ cursor: pointer;
1302
+ user-select: none;
1303
+ -webkit-tap-highlight-color: transparent;
1304
+ transition: border-color 0.15s;
1305
+ white-space: nowrap;
1306
+ }
1307
+
1308
+ .uf-select-trigger:hover,
1309
+ .uf-select-trigger:active {
1310
+ border-color: #444444;
1311
+ }
1312
+
1313
+ .uf-select-arrow {
1314
+ font-size: 9px;
1315
+ color: #666666;
1316
+ transition: transform 0.2s ease;
1317
+ flex-shrink: 0;
1318
+ }
1319
+
1320
+ .uf-select-arrow-open {
1321
+ transform: rotate(180deg);
1322
+ }
1323
+
1324
+ /* Dropdown panel — opens downward by default */
1325
+ .uf-select-dropdown {
1326
+ position: absolute;
1327
+ top: calc(100% + 4px);
1328
+ left: 0;
1329
+ min-width: 100%;
1330
+ background: #111111;
1331
+ border: 1px solid #2a2a2a;
1332
+ border-radius: 8px;
1333
+ z-index: 9999;
1334
+ box-shadow: 0 8px 32px rgba(0,0,0,0.8);
1335
+ overflow: hidden;
1336
+ max-height: 240px;
1337
+ overflow-y: auto;
1338
+ }
1339
+
1340
+ /* Upward variant — anchors above trigger, for bottom-of-screen selects */
1341
+ .uf-select-dropdown-up {
1342
+ top: auto;
1343
+ bottom: calc(100% + 4px);
1344
+ }
1345
+
1346
+ .uf-select-option {
1347
+ padding: 10px 14px;
1348
+ font-size: 11px;
1349
+ font-weight: 600;
1350
+ color: #aaaaaa;
1351
+ cursor: pointer;
1352
+ transition: background 0.1s, color 0.1s;
1353
+ -webkit-tap-highlight-color: transparent;
1354
+ }
1355
+
1356
+ .uf-select-option:hover,
1357
+ .uf-select-option:active {
1358
+ background: #1a1a1a;
1359
+ color: #ffffff;
1360
+ }
1361
+
1362
+ .uf-select-option-active {
1363
+ color: var(--cocoa-l);
1364
+ background: #0a0a0a;
1365
+ }
1366
+
1367
+ /* Hide scrollbar inside dropdown — options fit within max-height */
1368
+ .uf-select-dropdown::-webkit-scrollbar {
1369
+ width: 0;
1370
+ height: 0;
1371
+ }
1372
+
1373
+ /* Desktop: Vehicle Classification thin grey scrollbar (matches reference) */
1374
+ @media (min-width: 1024px) {
1375
+ #class-breakdown::-webkit-scrollbar {
1376
+ width: 4px;
1377
+ }
1378
+ #class-breakdown::-webkit-scrollbar-track {
1379
+ background: #000000;
1380
+ }
1381
+ #class-breakdown::-webkit-scrollbar-thumb {
1382
+ background: #333333;
1383
+ border-radius: 4px;
1384
+ }
1385
+ #class-breakdown::-webkit-scrollbar-thumb:hover {
1386
+ background: #444444;
1387
+ }
1388
+ }
1389
+
frontend/initial.html CHANGED
@@ -16,16 +16,22 @@
16
  }
17
  </script>
18
  <meta charset="UTF-8">
19
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
20
  <title>UrbanFlow</title>
21
- <link rel="icon" type="image/png" href="assets/rf.png">
 
 
 
 
 
 
22
  <script src="https://cdn.tailwindcss.com"></script>
23
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
24
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
25
  <link rel="stylesheet" href="css/initial.css">
26
  </head>
27
 
28
- <body class="bg-black text-white min-h-screen w-full flex flex-col items-center selection:bg-white selection:text-black">
29
 
30
  <header class="mt-16 flex flex-col items-center flex-shrink-0 w-full z-10">
31
  <img src="assets/uf_rf.png" alt="UrbanFlow Logo" class="h-44 md:h-52 w-auto object-contain mb-3">
@@ -33,28 +39,31 @@
33
 
34
  <main class="flex-1 w-full max-w-[90rem] mx-auto grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-20 px-10 py-6 items-center z-10">
35
 
36
- <div class="lg:col-span-7 flex flex-col justify-center xl:pl-10 pb-10 lg:pb-0">
37
- <h1 class="text-5xl xl:text-[4.5rem] font-extrabold mb-4 leading-[1.1] tracking-tight"
38
- style="background:linear-gradient(110deg,#f0ece6 0%,#f0ece6 35%,#c89a6c 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">
39
- Automated <br>Vision Intelligence
 
 
 
40
  </h1>
41
- <p class="font-bold mb-8 text-sm uppercase tracking-[0.2em] flex items-center" style="color:#a89f97">
42
  <span class="core-badge px-3 py-1 rounded-full text-[10px] mr-3">DEMO</span>
43
  Cloud-Native Traffic Intelligence
44
  </p>
45
  <ul class="space-y-4 xl:space-y-5 text-base xl:text-lg font-medium" style="color:#a89f97">
46
- <li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> No new hardware &mdash; works with your existing cameras</li>
47
- <li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Granular vehicle counts across 14 classes</li>
48
- <li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Directional flow &amp; congestion insights in minutes</li>
49
- <li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Downloadable reports ready for planning &amp; compliance</li>
50
- <li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Built for Indian roads &mdash; tested on real field conditions</li>
51
  </ul>
52
  </div>
53
 
54
- <div class="lg:col-span-5 flex flex-col justify-center w-full max-w-[32rem] mx-auto min-h-[450px] mb-12 lg:mb-0">
55
 
56
  <!-- STEP: Modules -->
57
- <div id="step-modules" class="w-full flex flex-col fade-in">
58
  <h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">UrbanFlow</h2>
59
  <p class="text-[13px] font-medium mb-8 text-center" style="color:#a89f97">Select an analytical module to continue.</p>
60
  <div class="flex justify-center w-full">
@@ -70,16 +79,16 @@
70
  </div>
71
 
72
  <!-- STEP: Upload -->
73
- <div id="step-upload" class="hidden w-full flex flex-col fade-in">
74
  <button onclick="showStep('modules')"
75
- class="text-neutral-500 hover:text-white transition flex items-center text-xs font-bold uppercase tracking-widest mb-6 w-fit">
76
  <i class="fa-solid fa-arrow-left mr-2"></i> Back
77
  </button>
78
  <h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">Source Media</h2>
79
  <p class="text-[13px] font-medium mb-8 text-center" style="color:#a89f97">Submit camera footage to begin traffic analysis.</p>
80
  <input id="file-input" type="file" accept="video/*" class="hidden">
81
  <div id="dropzone" onclick="document.getElementById('file-input').click()"
82
- class="border border-dashed border-neutral-700 rounded-[2rem] p-12 flex flex-col items-center justify-center cursor-pointer transition-all duration-300 group">
83
  <i class="fa-solid fa-arrow-up-from-bracket text-4xl mb-5 block mx-auto transition" style="color:#a89f97"></i>
84
  <span class="font-semibold text-lg mb-2 text-center block" style="color:#f0ece6">Drop or select a video file</span>
85
  <span class="text-[10px] font-bold uppercase tracking-widest text-center block" style="color:#a89f97">Any standard video format accepted</span>
@@ -93,10 +102,10 @@
93
  <div id="upload-bar" class="h-full w-0 transition-all duration-75 ease-linear rounded-full" style="background:#c89a6c"></div>
94
  </div>
95
  </div>
96
- </div>
97
 
98
  <!-- STEP: Draw -->
99
- <div id="step-draw" class="hidden w-full flex flex-col fade-in">
100
  <h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">Spatial Boundary</h2>
101
  <p class="text-[11px] font-bold uppercase tracking-widest mb-6 text-center" style="color:#a89f97">Mark two points to define the vehicle counting threshold</p>
102
  <div class="relative w-full aspect-video bg-neutral-950 rounded-3xl overflow-hidden cursor-crosshair mb-6">
@@ -115,7 +124,7 @@
115
  Continue &nbsp;&rarr;
116
  </button>
117
  <button onclick="resetCanvas()"
118
- class="text-[10px] font-bold uppercase tracking-widest text-slate-500 hover:text-white transition"
119
  style="background:none;border:none;">Reset Boundary</button>
120
  </div>
121
  </div>
@@ -123,16 +132,32 @@
123
  </div>
124
  </main>
125
 
126
- <footer class="w-full max-w-[90rem] mx-auto px-10 py-8 mt-auto grid grid-cols-1 md:grid-cols-3 gap-4 items-center text-[11px] font-bold uppercase tracking-[0.2em] z-10" style="color:#777">
127
- <div class="text-center md:text-left">
128
- <button onclick="openAppModal('privacyModal')" class="hover:text-white transition">Privacy Policy</button>
129
- </div>
130
- <div class="text-center">
131
- <button onclick="openAppModal('termsModal')" class="hover:text-white transition">Terms & Conditions</button>
 
 
 
 
 
 
 
132
  </div>
133
- <div class="text-center md:text-right">
134
- &copy; 2026 UrbanFlow. All rights reserved.
 
 
 
 
 
 
 
 
135
  </div>
 
136
  </footer>
137
 
138
  <script src="js/initial.js"></script>
@@ -161,6 +186,7 @@
161
  <p style="color:#a89f97;font-size:11px;margin-bottom:20px">We keep this simple and honest.</p>
162
  <ul style="color:#a89f97;font-size:11px;line-height:1.9;padding-left:16px;list-style:disc;text-align:left">
163
  <li>This is a <strong style="color:#f0ece6">public demo</strong> hosted on Hugging Face Spaces. It is not a production service.</li>
 
164
  <li>Footage you submit is processed in real time and <strong style="color:#f0ece6">discarded immediately</strong> after the session ends. Nothing is stored on our servers.</li>
165
  <li>We do not use your footage to train models, sell it, or share it with any third party.</li>
166
  <li>Reports and annotated videos are generated temporarily and delivered to your device. We do not retain copies.</li>
@@ -199,7 +225,8 @@
199
  <li>Misrepresent demo outputs as certified or regulatory-grade traffic data.</li>
200
  </ul>
201
  <p style="color:#a89f97;font-size:11px;text-align:left">This platform is provided as-is for <strong style="color:#f0ece6">demonstration and evaluation purposes only</strong>.
202
- Outputs are not intended for operational, regulatory, or safety-critical use. This is an early-stage research project, not a commercial product.</p>
 
203
  <p style="color:#555;font-size:10px;margin-top:16px;text-align:left">Questions: <strong
204
  style="color:#c89a6c">support.urbanflow365@gmail.com</strong></p>
205
  </div>
@@ -239,5 +266,12 @@
239
  </div>
240
  </div>
241
 
 
 
 
 
 
 
 
242
  </body>
243
  </html>
 
16
  }
17
  </script>
18
  <meta charset="UTF-8">
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
20
  <title>UrbanFlow</title>
21
+ <link rel="icon" type="image/png" sizes="512x512" href="assets/shuriken.png">
22
+ <link rel="manifest" href="manifest.json">
23
+ <meta name="theme-color" content="#000000">
24
+ <meta name="apple-mobile-web-app-capable" content="yes">
25
+ <meta name="apple-mobile-web-app-status-bar-style" content="black">
26
+ <meta name="apple-mobile-web-app-title" content="UrbanFlow">
27
+ <link rel="apple-touch-icon" sizes="512x512" href="assets/shurkien_b.png">
28
  <script src="https://cdn.tailwindcss.com"></script>
29
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
30
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
31
  <link rel="stylesheet" href="css/initial.css">
32
  </head>
33
 
34
+ <body class="bg-black text-white min-h-screen w-full flex flex-col items-center selection:bg-white selection:text-black overflow-x-hidden">
35
 
36
  <header class="mt-16 flex flex-col items-center flex-shrink-0 w-full z-10">
37
  <img src="assets/uf_rf.png" alt="UrbanFlow Logo" class="h-44 md:h-52 w-auto object-contain mb-3">
 
39
 
40
  <main class="flex-1 w-full max-w-[90rem] mx-auto grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-20 px-10 py-6 items-center z-10">
41
 
42
+ <div class="hero-text-section lg:col-span-7 flex flex-col justify-center pb-6 lg:pb-0">
43
+ <h1 class="hero-title text-center sm:text-left font-extrabold mb-6 sm:mb-4 tracking-tight"
44
+ style="background:linear-gradient(135deg,#f0ece6 0%,#f0ece6 35%,#d4b08a 60%,#c89a6c 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">
45
+ <span class="block sm:inline">Automated</span>
46
+ <br class="hidden sm:block">
47
+ <span class="block sm:inline">Vision</span>
48
+ <span class="block sm:inline">Intelligence</span>
49
  </h1>
50
+ <p class="font-bold mb-8 text-sm uppercase tracking-[0.2em] flex items-center justify-center sm:justify-start" style="color:#a89f97">
51
  <span class="core-badge px-3 py-1 rounded-full text-[10px] mr-3">DEMO</span>
52
  Cloud-Native Traffic Intelligence
53
  </p>
54
  <ul class="space-y-4 xl:space-y-5 text-base xl:text-lg font-medium" style="color:#a89f97">
55
+ <li class="flex items-center"><i class="fa-solid fa-check mr-3 md:mr-5 text-xl" style="color:#c89a6c"></i> No new hardware &mdash; works with your existing cameras</li>
56
+ <li class="flex items-center"><i class="fa-solid fa-check mr-3 md:mr-5 text-xl" style="color:#c89a6c"></i> Granular vehicle counts across 14 classes</li>
57
+ <li class="flex items-center"><i class="fa-solid fa-check mr-3 md:mr-5 text-xl" style="color:#c89a6c"></i> Directional flow &amp; congestion insights in minutes</li>
58
+ <li class="flex items-center"><i class="fa-solid fa-check mr-3 md:mr-5 text-xl" style="color:#c89a6c"></i> Downloadable reports ready for planning &amp; compliance</li>
59
+ <li class="flex items-center"><i class="fa-solid fa-check mr-3 md:mr-5 text-xl" style="color:#c89a6c"></i> Built for Indian roads &mdash; tested on real field conditions</li>
60
  </ul>
61
  </div>
62
 
63
+ <div class="step-card-section lg:col-span-5 flex flex-col justify-center w-full max-w-[32rem] mx-auto h-auto md:h-[480px] lg:h-[520px] mb-12 lg:mb-0">
64
 
65
  <!-- STEP: Modules -->
66
+ <div id="step-modules" class="w-full flex flex-col fade-in overflow-y-auto">
67
  <h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">UrbanFlow</h2>
68
  <p class="text-[13px] font-medium mb-8 text-center" style="color:#a89f97">Select an analytical module to continue.</p>
69
  <div class="flex justify-center w-full">
 
79
  </div>
80
 
81
  <!-- STEP: Upload -->
82
+ <div id="step-upload" class="hidden w-full flex flex-col fade-in overflow-y-auto">
83
  <button onclick="showStep('modules')"
84
+ class="text-[#a89f97] hover:text-white hover:underline transition flex items-center text-xs font-bold uppercase tracking-widest mb-6 w-fit">
85
  <i class="fa-solid fa-arrow-left mr-2"></i> Back
86
  </button>
87
  <h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">Source Media</h2>
88
  <p class="text-[13px] font-medium mb-8 text-center" style="color:#a89f97">Submit camera footage to begin traffic analysis.</p>
89
  <input id="file-input" type="file" accept="video/*" class="hidden">
90
  <div id="dropzone" onclick="document.getElementById('file-input').click()"
91
+ class="border border-dashed border-neutral-700 rounded-[2rem] p-8 md:p-12 flex flex-col items-center justify-center cursor-pointer transition-all duration-300 group">
92
  <i class="fa-solid fa-arrow-up-from-bracket text-4xl mb-5 block mx-auto transition" style="color:#a89f97"></i>
93
  <span class="font-semibold text-lg mb-2 text-center block" style="color:#f0ece6">Drop or select a video file</span>
94
  <span class="text-[10px] font-bold uppercase tracking-widest text-center block" style="color:#a89f97">Any standard video format accepted</span>
 
102
  <div id="upload-bar" class="h-full w-0 transition-all duration-75 ease-linear rounded-full" style="background:#c89a6c"></div>
103
  </div>
104
  </div>
105
+ </div>
106
 
107
  <!-- STEP: Draw -->
108
+ <div id="step-draw" class="hidden w-full flex flex-col fade-in overflow-y-auto">
109
  <h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">Spatial Boundary</h2>
110
  <p class="text-[11px] font-bold uppercase tracking-widest mb-6 text-center" style="color:#a89f97">Mark two points to define the vehicle counting threshold</p>
111
  <div class="relative w-full aspect-video bg-neutral-950 rounded-3xl overflow-hidden cursor-crosshair mb-6">
 
124
  Continue &nbsp;&rarr;
125
  </button>
126
  <button onclick="resetCanvas()"
127
+ class="text-[10px] font-bold uppercase tracking-widest text-slate-500 hover:text-white transition px-4 py-2 mt-2"
128
  style="background:none;border:none;">Reset Boundary</button>
129
  </div>
130
  </div>
 
132
  </div>
133
  </main>
134
 
135
+ <footer class="w-full max-w-[90rem] mx-auto px-10 mt-auto z-10 text-[11px] font-bold uppercase tracking-[0.2em]" style="color:#777">
136
+
137
+ <!-- Desktop: Privacy Policy left | T&C center | © right — single row -->
138
+ <div class="hidden md:grid md:grid-cols-3 items-center py-6">
139
+ <div class="text-left">
140
+ <button onclick="openAppModal('privacyModal')" class="hover:text-white transition">Privacy Policy</button>
141
+ </div>
142
+ <div class="text-center">
143
+ <button onclick="openAppModal('termsModal')" class="hover:text-white transition">Terms &amp; Conditions</button>
144
+ </div>
145
+ <div class="text-right">
146
+ &copy; 2026 UrbanFlow. All rights reserved.
147
+ </div>
148
  </div>
149
+
150
+ <!-- Mobile: Privacy Policy left | T&C right, then © centered below -->
151
+ <div class="md:hidden py-4">
152
+ <div class="flex items-center justify-between mb-2">
153
+ <button onclick="openAppModal('privacyModal')" class="hover:text-white transition">Privacy Policy</button>
154
+ <button onclick="openAppModal('termsModal')" class="hover:text-white transition">Terms &amp; Conditions</button>
155
+ </div>
156
+ <div class="text-center">
157
+ &copy; 2026 UrbanFlow. All rights reserved.
158
+ </div>
159
  </div>
160
+
161
  </footer>
162
 
163
  <script src="js/initial.js"></script>
 
186
  <p style="color:#a89f97;font-size:11px;margin-bottom:20px">We keep this simple and honest.</p>
187
  <ul style="color:#a89f97;font-size:11px;line-height:1.9;padding-left:16px;list-style:disc;text-align:left">
188
  <li>This is a <strong style="color:#f0ece6">public demo</strong> hosted on Hugging Face Spaces. It is not a production service.</li>
189
+ <li>UrbanFlow provides an estimated accuracy of ±5–8% on dense mixed-traffic footage. Results may vary slightly across runs due to the nature of real-time frame-by-frame inference.</li>
190
  <li>Footage you submit is processed in real time and <strong style="color:#f0ece6">discarded immediately</strong> after the session ends. Nothing is stored on our servers.</li>
191
  <li>We do not use your footage to train models, sell it, or share it with any third party.</li>
192
  <li>Reports and annotated videos are generated temporarily and delivered to your device. We do not retain copies.</li>
 
225
  <li>Misrepresent demo outputs as certified or regulatory-grade traffic data.</li>
226
  </ul>
227
  <p style="color:#a89f97;font-size:11px;text-align:left">This platform is provided as-is for <strong style="color:#f0ece6">demonstration and evaluation purposes only</strong>.
228
+ UrbanFlow provides an estimated accuracy of ±5–8% on dense mixed-traffic footage. For research or planning use, we recommend processing the same video 2–3 times and taking the average count.</p>
229
+ <p style="color:#a89f97;font-size:11px;text-align:left">Outputs are not intended for operational, regulatory, or safety-critical use. This is an early-stage research project, not a commercial product.</p>
230
  <p style="color:#555;font-size:10px;margin-top:16px;text-align:left">Questions: <strong
231
  style="color:#c89a6c">support.urbanflow365@gmail.com</strong></p>
232
  </div>
 
266
  </div>
267
  </div>
268
 
269
+ <script>
270
+ if ('serviceWorker' in navigator) {
271
+ window.addEventListener('load', () => {
272
+ navigator.serviceWorker.register('./sw.js');
273
+ });
274
+ }
275
+ </script>
276
  </body>
277
  </html>
frontend/js/initial.js CHANGED
@@ -1,6 +1,12 @@
1
- let videoId = null;
 
 
 
 
 
2
  let runConfig = {};
3
 
 
4
  function showStep(name) {
5
  ['modules', 'upload', 'draw'].forEach(s => {
6
  const el = document.getElementById('step-' + s);
@@ -20,7 +26,8 @@ function showStep(name) {
20
  if (name === 'draw') loadFirstFrame();
21
  }
22
 
23
- const dropzone = document.getElementById('dropzone');
 
24
  const fileInput = document.getElementById('file-input');
25
 
26
  if (fileInput) {
@@ -30,34 +37,54 @@ if (fileInput) {
30
  }
31
 
32
  if (dropzone) {
 
33
  dropzone.addEventListener('dragover', e => {
34
  e.preventDefault();
35
- dropzone.classList.add('border-white', 'bg-neutral-950');
36
  });
37
  dropzone.addEventListener('dragleave', () => {
38
- dropzone.classList.remove('border-white', 'bg-neutral-950');
39
  });
40
  dropzone.addEventListener('drop', e => {
41
  e.preventDefault();
42
- dropzone.classList.remove('border-white', 'bg-neutral-950');
43
  if (e.dataTransfer.files.length) uploadFile(e.dataTransfer.files[0]);
44
  });
45
  }
46
 
 
47
  let currentXHR = null;
48
 
49
  function uploadFile(file) {
50
  if (currentXHR) currentXHR.abort();
51
 
52
  const dropzoneEl = document.getElementById('dropzone');
53
- const prog = document.getElementById('upload-progress-container');
54
- const bar = document.getElementById('upload-bar');
55
- const pct = document.getElementById('upload-percentage');
56
- const txt = document.getElementById('upload-text');
57
 
58
  if (dropzoneEl) dropzoneEl.classList.add('hidden');
59
  if (prog) prog.classList.remove('hidden');
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  const form = new FormData();
62
  form.append('file', file);
63
 
@@ -65,54 +92,68 @@ function uploadFile(file) {
65
  currentXHR = xhr;
66
  xhr.open('POST', 'upload');
67
 
 
68
  xhr.upload.onprogress = e => {
69
  if (e.lengthComputable) {
70
- const p = Math.round(e.loaded / e.total * 100);
71
- bar.style.width = p + '%';
72
- pct.innerText = p + '%';
 
 
 
 
73
  }
74
  };
75
 
76
  xhr.onerror = () => {
 
77
  txt.innerText = 'Error: Network failure';
78
  txt.classList.add('text-red-500');
79
- fileInput.value = '';
80
  };
81
 
82
  xhr.onload = () => {
 
 
83
  if (xhr.status !== 200) {
84
  txt.innerText = 'Error: ' + xhr.status;
85
  txt.classList.add('text-red-500');
86
- fileInput.value = '';
87
  return;
88
  }
 
 
 
 
 
89
  const res = JSON.parse(xhr.responseText);
90
  videoId = res.video_id;
91
  txt.innerText = 'Extracting Metadata...';
92
- bar.style.width = '100%';
93
- pct.innerText = '100%';
94
 
95
  fetch('config/' + videoId)
96
  .then(r => r.json())
97
  .then(cfg => {
98
- runConfig = cfg;
99
  runConfig.conf = 0.12;
100
  runConfig.iou = 0.60;
101
- txt.innerText = 'Initialization Complete';
102
- fileInput.value = '';
103
  setTimeout(() => showStep('draw'), 800);
104
  })
105
  .catch(() => {
106
  txt.innerText = 'Metadata Failed';
107
  txt.classList.add('text-red-500');
108
- fileInput.value = '';
109
  });
110
  };
111
 
112
  xhr.send(form);
113
  }
114
 
115
- // Draw Canvas
 
 
 
116
  const canvas = document.getElementById('drawing-canvas');
117
  const ctx = canvas ? canvas.getContext('2d') : null;
118
  let points = [];
@@ -125,7 +166,9 @@ function loadFirstFrame() {
125
  img.onerror = () => {
126
  console.error('Failed to load first frame');
127
  if (placeholder) {
128
- placeholder.innerHTML = '<i class="fa-solid fa-circle-exclamation text-4xl mb-3 opacity-50" style="color:#c89a6c"></i><span class="font-bold text-[10px] uppercase tracking-widest opacity-50 block mt-2">Frame Load Error</span>';
 
 
129
  }
130
  };
131
 
@@ -140,31 +183,69 @@ function loadFirstFrame() {
140
  }
141
 
142
  function initCanvas() {
143
- if (canvas) {
144
- canvas.width = canvas.offsetWidth;
145
- canvas.height = canvas.offsetHeight;
146
- }
 
147
  }
148
 
149
  window.addEventListener('resize', initCanvas);
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  if (canvas) {
152
  canvas.addEventListener('mousedown', e => {
153
- if (points.length >= 2) return;
154
- const rect = canvas.getBoundingClientRect();
155
- const cx = e.clientX - rect.left;
156
- const cy = e.clientY - rect.top;
157
- const rx = (cx / canvas.width) * imgNatW;
158
- const ry = (cy / canvas.height) * imgNatH;
159
- points.push({ cx, cy, rx: Math.round(rx), ry: Math.round(ry) });
160
- drawDot(cx, cy);
161
- if (points.length === 2) drawLine();
162
  });
163
  }
164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  function drawDot(x, y) {
 
166
  ctx.beginPath();
167
- ctx.arc(x, y, 5, 0, Math.PI * 2);
168
  ctx.fillStyle = '#c89a6c';
169
  ctx.fill();
170
  ctx.strokeStyle = '#f0ece6';
@@ -173,6 +254,7 @@ function drawDot(x, y) {
173
  }
174
 
175
  function drawLine() {
 
176
  ctx.beginPath();
177
  ctx.moveTo(points[0].cx, points[0].cy);
178
  ctx.lineTo(points[1].cx, points[1].cy);
@@ -184,6 +266,7 @@ function drawLine() {
184
  function resetCanvas() {
185
  points = [];
186
  if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
 
187
  }
188
 
189
  function startRun() {
@@ -194,26 +277,41 @@ function startRun() {
194
  line: line,
195
  config: runConfig
196
  }));
 
197
  window.location.href = './';
198
  }
199
 
 
200
  // Onboarding
 
201
  let _obStep = 0;
202
 
 
 
 
 
 
 
 
 
203
  function nextOnboardStep() {
204
  _obStep++;
205
  if (_obStep >= 3) { closeOnboarding(); return; }
206
  document.querySelectorAll('.onboard-step').forEach((s, i) => s.classList.toggle('active', i === _obStep));
207
  document.querySelectorAll('.onboard-dot').forEach((d, i) => d.classList.toggle('active', i === _obStep));
208
  if (_obStep === 2) document.getElementById('onboard-next').innerText = 'Get Started';
 
209
  }
210
 
211
  function closeOnboarding() {
212
- document.getElementById('onboard-overlay').style.display = 'none';
 
213
  }
214
 
215
- // Show onboarding on every page load
216
  document.addEventListener('DOMContentLoaded', () => {
217
  const onboard = document.getElementById('onboard-overlay');
218
- if (onboard) onboard.style.display = 'flex';
219
- });
 
 
 
 
1
+ // =============================================
2
+ // UrbanFlow — initial.js (Mobile-First)
3
+ // Added: touch events, mobile upload UX
4
+ // =============================================
5
+
6
+ let videoId = null;
7
  let runConfig = {};
8
 
9
+ // ---- Step navigation ----
10
  function showStep(name) {
11
  ['modules', 'upload', 'draw'].forEach(s => {
12
  const el = document.getElementById('step-' + s);
 
26
  if (name === 'draw') loadFirstFrame();
27
  }
28
 
29
+ // ---- File input / dropzone ----
30
+ const dropzone = document.getElementById('dropzone');
31
  const fileInput = document.getElementById('file-input');
32
 
33
  if (fileInput) {
 
37
  }
38
 
39
  if (dropzone) {
40
+ // Desktop drag-and-drop
41
  dropzone.addEventListener('dragover', e => {
42
  e.preventDefault();
43
+ dropzone.classList.add('dz-active');
44
  });
45
  dropzone.addEventListener('dragleave', () => {
46
+ dropzone.classList.remove('dz-active');
47
  });
48
  dropzone.addEventListener('drop', e => {
49
  e.preventDefault();
50
+ dropzone.classList.remove('dz-active');
51
  if (e.dataTransfer.files.length) uploadFile(e.dataTransfer.files[0]);
52
  });
53
  }
54
 
55
+ // ---- Upload ----
56
  let currentXHR = null;
57
 
58
  function uploadFile(file) {
59
  if (currentXHR) currentXHR.abort();
60
 
61
  const dropzoneEl = document.getElementById('dropzone');
62
+ const prog = document.getElementById('upload-progress-container');
63
+ const bar = document.getElementById('upload-bar');
64
+ const pct = document.getElementById('upload-percentage');
65
+ const txt = document.getElementById('upload-text');
66
 
67
  if (dropzoneEl) dropzoneEl.classList.add('hidden');
68
  if (prog) prog.classList.remove('hidden');
69
 
70
+ // ---- Simulated progress (proxy-buffer-safe) ----
71
+ // Estimate upload duration: ~1 MB/s conservative, capped between 3s and 60s
72
+ const fileMB = file.size / (1024 * 1024);
73
+ const estDurationMs = Math.min(Math.max(fileMB * 1000, 3000), 60000);
74
+ const targetPct = 100; // stop simulation at 100%, snap to 100% on load
75
+ const tickMs = 200; // update every 200ms
76
+ const totalTicks = estDurationMs / tickMs;
77
+ const stepPerTick = targetPct / totalTicks;
78
+
79
+ let simPct = 0;
80
+ let simInterval = setInterval(() => {
81
+ if (simPct < targetPct) {
82
+ simPct = Math.min(simPct + stepPerTick, targetPct);
83
+ bar.style.width = simPct.toFixed(1) + '%';
84
+ pct.innerText = Math.floor(simPct) + '%';
85
+ }
86
+ }, tickMs);
87
+
88
  const form = new FormData();
89
  form.append('file', file);
90
 
 
92
  currentXHR = xhr;
93
  xhr.open('POST', 'upload');
94
 
95
+ // Real progress override — fires if proxy reports actual bytes (rare but handle it)
96
  xhr.upload.onprogress = e => {
97
  if (e.lengthComputable) {
98
+ const realPct = Math.round(e.loaded / e.total * 100);
99
+ // Only override simulation if real progress is AHEAD of it
100
+ if (realPct > simPct) {
101
+ simPct = realPct;
102
+ bar.style.width = simPct + '%';
103
+ pct.innerText = simPct + '%';
104
+ }
105
  }
106
  };
107
 
108
  xhr.onerror = () => {
109
+ clearInterval(simInterval);
110
  txt.innerText = 'Error: Network failure';
111
  txt.classList.add('text-red-500');
112
+ if (fileInput) fileInput.value = '';
113
  };
114
 
115
  xhr.onload = () => {
116
+ clearInterval(simInterval);
117
+
118
  if (xhr.status !== 200) {
119
  txt.innerText = 'Error: ' + xhr.status;
120
  txt.classList.add('text-red-500');
121
+ if (fileInput) fileInput.value = '';
122
  return;
123
  }
124
+
125
+ // Snap to 100% on successful upload response
126
+ bar.style.width = '100%';
127
+ pct.innerText = '100%';
128
+
129
  const res = JSON.parse(xhr.responseText);
130
  videoId = res.video_id;
131
  txt.innerText = 'Extracting Metadata...';
 
 
132
 
133
  fetch('config/' + videoId)
134
  .then(r => r.json())
135
  .then(cfg => {
136
+ runConfig = cfg;
137
  runConfig.conf = 0.12;
138
  runConfig.iou = 0.60;
139
+ txt.innerText = 'Initialization Complete';
140
+ if (fileInput) fileInput.value = '';
141
  setTimeout(() => showStep('draw'), 800);
142
  })
143
  .catch(() => {
144
  txt.innerText = 'Metadata Failed';
145
  txt.classList.add('text-red-500');
146
+ if (fileInput) fileInput.value = '';
147
  });
148
  };
149
 
150
  xhr.send(form);
151
  }
152
 
153
+ // =============================================
154
+ // CANVAS — Spatial Boundary Drawing
155
+ // Supports both mouse (desktop) and touch (mobile)
156
+ // =============================================
157
  const canvas = document.getElementById('drawing-canvas');
158
  const ctx = canvas ? canvas.getContext('2d') : null;
159
  let points = [];
 
166
  img.onerror = () => {
167
  console.error('Failed to load first frame');
168
  if (placeholder) {
169
+ placeholder.innerHTML =
170
+ '<i class="fa-solid fa-circle-exclamation text-4xl mb-3 opacity-50" style="color:#c89a6c"></i>' +
171
+ '<span class="font-bold text-[10px] uppercase tracking-widest opacity-50 block mt-2">Frame Load Error</span>';
172
  }
173
  };
174
 
 
183
  }
184
 
185
  function initCanvas() {
186
+ if (!canvas) return;
187
+ canvas.width = canvas.offsetWidth;
188
+ canvas.height = canvas.offsetHeight;
189
+ // Redraw existing points after resize
190
+ redrawCanvas();
191
  }
192
 
193
  window.addEventListener('resize', initCanvas);
194
 
195
+ // ---- Coordinate helpers ----
196
+ function getCanvasCoords(clientX, clientY) {
197
+ const rect = canvas.getBoundingClientRect();
198
+ const cx = clientX - rect.left;
199
+ const cy = clientY - rect.top;
200
+ const rx = (cx / canvas.width) * imgNatW;
201
+ const ry = (cy / canvas.height) * imgNatH;
202
+ return { cx, cy, rx: Math.round(rx), ry: Math.round(ry) };
203
+ }
204
+
205
+ function addPoint(coords) {
206
+ if (points.length >= 2) return;
207
+ points.push(coords);
208
+ redrawCanvas();
209
+ if (points.length === 2 && canvas) {
210
+ canvas.style.cursor = 'default';
211
+ }
212
+ }
213
+
214
+ function redrawCanvas() {
215
+ if (!ctx) return;
216
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
217
+ points.forEach(p => drawDot(p.cx, p.cy));
218
+ if (points.length === 2) drawLine();
219
+ }
220
+
221
+ // ---- Mouse events (desktop) ----
222
  if (canvas) {
223
  canvas.addEventListener('mousedown', e => {
224
+ e.preventDefault();
225
+ addPoint(getCanvasCoords(e.clientX, e.clientY));
 
 
 
 
 
 
 
226
  });
227
  }
228
 
229
+ // ---- Touch events (mobile) ----
230
+ if (canvas) {
231
+ canvas.addEventListener('touchstart', e => {
232
+ e.preventDefault(); // prevent scroll while drawing
233
+ if (e.touches.length === 0) return;
234
+ const touch = e.touches[0];
235
+ addPoint(getCanvasCoords(touch.clientX, touch.clientY));
236
+ }, { passive: false });
237
+
238
+ // touchmove: prevent page scroll when finger is on canvas
239
+ canvas.addEventListener('touchmove', e => {
240
+ e.preventDefault();
241
+ }, { passive: false });
242
+ }
243
+
244
+ // ---- Drawing helpers ----
245
  function drawDot(x, y) {
246
+ if (!ctx) return;
247
  ctx.beginPath();
248
+ ctx.arc(x, y, 6, 0, Math.PI * 2); // slightly larger dot for mobile visibility
249
  ctx.fillStyle = '#c89a6c';
250
  ctx.fill();
251
  ctx.strokeStyle = '#f0ece6';
 
254
  }
255
 
256
  function drawLine() {
257
+ if (!ctx || points.length < 2) return;
258
  ctx.beginPath();
259
  ctx.moveTo(points[0].cx, points[0].cy);
260
  ctx.lineTo(points[1].cx, points[1].cy);
 
266
  function resetCanvas() {
267
  points = [];
268
  if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
269
+ if (canvas) canvas.style.cursor = 'crosshair';
270
  }
271
 
272
  function startRun() {
 
277
  line: line,
278
  config: runConfig
279
  }));
280
+ sessionStorage.setItem('uf_active_tab', 'settings');
281
  window.location.href = './';
282
  }
283
 
284
+ // =============================================
285
  // Onboarding
286
+ // =============================================
287
  let _obStep = 0;
288
 
289
+ function triggerIconPulse() {
290
+ const activeStep = document.querySelector('.onboard-step.active i');
291
+ if (activeStep) {
292
+ activeStep.classList.add('pulse-once');
293
+ setTimeout(() => activeStep.classList.remove('pulse-once'), 400);
294
+ }
295
+ }
296
+
297
  function nextOnboardStep() {
298
  _obStep++;
299
  if (_obStep >= 3) { closeOnboarding(); return; }
300
  document.querySelectorAll('.onboard-step').forEach((s, i) => s.classList.toggle('active', i === _obStep));
301
  document.querySelectorAll('.onboard-dot').forEach((d, i) => d.classList.toggle('active', i === _obStep));
302
  if (_obStep === 2) document.getElementById('onboard-next').innerText = 'Get Started';
303
+ triggerIconPulse();
304
  }
305
 
306
  function closeOnboarding() {
307
+ const overlay = document.getElementById('onboard-overlay');
308
+ if (overlay) overlay.style.display = 'none';
309
  }
310
 
 
311
  document.addEventListener('DOMContentLoaded', () => {
312
  const onboard = document.getElementById('onboard-overlay');
313
+ if (onboard) {
314
+ onboard.style.display = 'flex';
315
+ triggerIconPulse();
316
+ }
317
+ });
frontend/js/vehicles.js CHANGED
@@ -1,5 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
- // =========== Rolling Counter ===========
3
  function animateValue(obj, start, end, duration) {
4
  let startTimestamp = null;
5
  const isPct = typeof end === 'string' && end.includes('%');
@@ -27,72 +92,97 @@
27
  window.requestAnimationFrame(step);
28
  }
29
 
30
- // =========== Tooltip ===========
31
- // Position and toggle tooltip visibility
32
- document.addEventListener('mouseover', e => {
33
- const wrap = e.target.closest('.info-wrap');
34
- if (!wrap) return;
35
- const tip = wrap.querySelector('.info-tip');
36
- if (!tip) return;
37
-
38
- tip.style.display = 'block';
39
- const rect = wrap.getBoundingClientRect();
40
- const tipH = tip.offsetHeight || 60;
41
- if (rect.bottom + tipH + 10 > window.innerHeight) {
42
- tip.style.top = (rect.top - tipH - 6) + 'px';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  } else {
44
- tip.style.top = (rect.bottom + 6) + 'px';
 
45
  }
46
- tip.style.left = Math.min(rect.left, window.innerWidth - 300) + 'px';
47
- });
48
-
49
- document.addEventListener('mouseout', e => {
50
- const wrap = e.target.closest('.info-wrap');
51
- if (!wrap) return;
52
- const tip = wrap.querySelector('.info-tip');
53
- if (tip) tip.style.display = 'none';
54
- });
55
 
56
- document.addEventListener('click', e => {
57
- const btn = e.target.closest('.info-btn');
58
- if (!btn) return;
59
- const wrap = btn.closest('.info-wrap');
60
- if (!wrap) return;
61
- const tip = wrap.querySelector('.info-tip');
62
- if (!tip) return;
63
-
64
- const isVisible = tip.style.display === 'block';
65
- tip.style.display = isVisible ? 'none' : 'block';
66
-
67
- if (!isVisible) {
68
- const rect = wrap.getBoundingClientRect();
69
- const tipH = tip.offsetHeight || 60;
70
- if (rect.bottom + tipH + 10 > window.innerHeight) {
71
- tip.style.top = (rect.top - tipH - 6) + 'px';
72
- } else {
73
- tip.style.top = (rect.bottom + 6) + 'px';
74
- }
75
- tip.style.left = Math.min(rect.left, window.innerWidth - 300) + 'px';
76
  }
77
- });
78
-
79
- // =========== Tab switching ===========
80
- function switchTab(tab) {
81
- ['about', 'overview', 'run-details', 'reports', 'settings', 'help', 'feedback'].forEach(t => {
82
- const el = document.getElementById('tab-' + t);
83
- const nav = document.getElementById('nav-' + t);
84
- if (el) el.classList.toggle('hidden', tab !== t);
85
- if (nav) {
86
- if (tab === t) {
87
- nav.classList.add('nav-item-active');
88
- nav.classList.remove('nav-item-inactive');
89
- } else {
90
- nav.classList.remove('nav-item-active');
91
- nav.classList.add('nav-item-inactive');
92
- }
93
- }
94
- });
95
  }
 
 
96
 
97
  // =========== Toast System ===========
98
  function showToast(message, type) {
@@ -244,66 +334,72 @@
244
  const panel = document.getElementById('insights-panel');
245
  panel.classList.remove('hidden');
246
 
247
- // Speed distribution bars
248
- const dist = d.speed_distribution || {};
249
- const bars = document.getElementById('speed-bars');
250
- const colors = { slow: '#ef4444', normal: '#eab308', fast: '#22c55e' };
251
- const labels = { slow: 'Slow', normal: 'Normal', fast: 'Fast' };
252
- bars.innerHTML = ['slow', 'normal', 'fast'].map(cat => {
253
- const pct = dist[cat] || 0;
254
- const h = Math.max(8, pct * 1.2);
255
- return `<div class="flex flex-col items-center gap-1">
256
- <span class="text-[10px] font-bold" style="color:${colors[cat]}">${pct}%</span>
257
- <div style="width:36px;height:${h}px;background:${colors[cat]};border-radius:6px;transition:height 0.5s"></div>
258
- <span class="text-[9px] font-bold text-slate-500 uppercase">${labels[cat]}</span>
259
- </div>`;
260
- }).join('');
261
-
262
  // Congestion insights
263
  const ci = document.getElementById('congestion-insights');
264
  const pcu = d.pcu || {};
265
  ci.innerHTML = [
266
  infoRow('Total PCU', pcu.total_pcu || 0, 'Passenger Car Units (IRC:106-1990). Normalizes mixed traffic.'),
267
  infoRow('PCU In / Out', `${pcu.pcu_in || 0} / ${pcu.pcu_out || 0}`, 'Directional PCU split.'),
268
- infoRow('Speed Profile', `${dist.slow || 0}% slow · ${dist.normal || 0}% normal · ${dist.fast || 0}% fast`, 'Relative speed categories within this video.'),
269
  ].join('');
270
 
271
- // ---- Also populate Stats tab cards ----
272
- // Speed card in Stats
273
- const speedCard = document.getElementById('speed-stats-card');
274
- if (speedCard) {
275
- speedCard.innerHTML = `<div class="flex gap-5 items-end justify-center w-full">
276
- ${['slow', 'normal', 'fast'].map(cat => {
277
- const pct = dist[cat] || 0;
278
- const h = Math.max(12, pct * 1.0);
279
- return `<div class="flex flex-col items-center gap-1.5 flex-1">
280
- <span class="text-lg font-black" style="color:${colors[cat]}">${pct}%</span>
281
- <div style="width:100%;max-width:48px;height:${h}px;background:${colors[cat]};border-radius:8px;transition:height 0.5s"></div>
282
- <span class="text-[10px] font-bold text-slate-500 uppercase">${labels[cat]}</span>
283
- </div>`;
284
- }).join('')}
285
- </div>`;
286
- }
287
-
288
  // PCU card in Stats
289
  const pcuCard = document.getElementById('pcu-stats-card');
290
  if (pcuCard) {
291
- pcuCard.innerHTML = `<div class="space-y-3 w-full">
292
- <div class="flex justify-between items-center">
293
- <span class="text-xs font-medium text-slate-500">Total PCU</span>
294
- <span class="text-2xl font-black" style="color:#8b5e3c">${pcu.total_pcu || 0}</span>
295
- </div>
296
- <div class="flex gap-3">
297
- <div class="flex-1 bg-green-50 rounded-lg p-2.5 text-center border border-green-100">
298
- <div class="text-lg font-bold text-green-700">${pcu.pcu_in || 0}</div>
299
- <div class="text-[9px] font-bold text-green-500 uppercase">PCU In</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  </div>
301
- <div class="flex-1 bg-red-50 rounded-lg p-2.5 text-center border border-red-100">
302
- <div class="text-lg font-bold text-red-700">${pcu.pcu_out || 0}</div>
303
- <div class="text-[9px] font-bold text-red-500 uppercase">PCU Out</div>
304
  </div>
305
- </div>
306
- </div>`;
 
 
 
 
 
 
 
 
 
307
  }
308
  }
309
 
@@ -336,9 +432,9 @@
336
  const res = c.resolution || [0, 0];
337
 
338
  document.getElementById('panel-video').innerHTML =
339
- detailRow('video_fps', c.video_fps) +
340
  detailRow('frames', c.frames) +
341
- detailRow('duration', c.duration + ' sec') +
342
  detailRow('resolution', res[0] + ' <span class="text-slate-400 text-xs">x</span> ' + res[1]) +
343
  detailRow('pixels', (c.pixels || 0).toLocaleString());
344
 
@@ -394,101 +490,127 @@
394
  gold: { congestion: '#fbbf24', congestionBg: 'rgba(251,191,36,0.08)', dominance: '#a8a29e', flow: '#f5f5f4', doughIn: '#f5f5f4', doughOut: '#fbbf24' }
395
  };
396
 
397
- // Read settings from sessionStorage (set by settings.html)
398
- const rawRun = sessionStorage.getItem('funky_run');
399
- const runSettings = rawRun ? (JSON.parse(rawRun).settings || {}) : {};
400
- let currentPalette = runSettings.palette || 'default';
401
- let activePalette = PALETTES[currentPalette];
402
-
403
- // =========== Charts ===========
404
- Chart.defaults.font.family = "'Montserrat', sans-serif";
405
- Chart.defaults.color = '#888888';
406
- Chart.defaults.borderColor = '#222222';
407
- Chart.defaults.plugins.tooltip.backgroundColor = '#0a0a0a';
408
- Chart.defaults.plugins.tooltip.titleColor = '#ffffff';
409
- Chart.defaults.plugins.tooltip.bodyColor = '#aaaaaa';
410
- Chart.defaults.plugins.tooltip.borderColor = '#222222';
411
- Chart.defaults.plugins.tooltip.borderWidth = 1;
412
-
413
  let MODEL_CLASSES = {};
414
  let BUSINESS_MAP = {};
415
-
416
- const congChart = new Chart(document.getElementById('congestionChart').getContext('2d'), {
417
- type: 'line',
418
- data: {
419
- labels: [], datasets: [{
420
- data: [],
421
- borderColor: activePalette.congestion,
422
- backgroundColor: activePalette.congestionBg,
423
- fill: true,
424
- tension: 0.2,
425
- borderWidth: 1.5,
426
- pointRadius: 0
427
- }]
428
- },
429
- options: {
430
- responsive: true, maintainAspectRatio: false,
431
- plugins: { legend: { display: false } },
432
- scales: {
433
- x: { grid: { display: false }, ticks: { maxTicksLimit: 10, font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Frame Index', font: { size: 10, weight: '700' }, color: '#888888' } },
434
- y: { grid: { color: '#1a1a1a' }, beginAtZero: true, ticks: { font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Active Vehicles', font: { size: 10, weight: '700' }, color: '#888888' } }
 
 
 
 
 
 
 
 
 
 
 
435
  },
436
- animation: { duration: 0 }
437
- },
438
- plugins: []
439
- });
 
 
 
 
 
 
 
440
 
441
- const doughChart = new Chart(document.getElementById('doughnutChart').getContext('2d'), {
442
- type: 'doughnut',
443
- data: {
444
- labels: ['Incoming', 'Outgoing'], datasets: [{
445
- data: [0, 0],
446
- backgroundColor: [activePalette.doughIn, activePalette.doughOut],
447
- borderColor: '#0a0a0a',
448
- borderWidth: 3,
449
- hoverOffset: 6
450
- }]
451
- },
452
- options: {
453
- responsive: true, maintainAspectRatio: false,
454
- cutout: '68%',
455
- plugins: {
456
- legend: { display: true, position: 'bottom', labels: { padding: 12, usePointStyle: true, pointStyle: 'circle', font: { size: 10, weight: '600' } } }
457
  },
458
- animation: { duration: 0 }
459
- },
460
- plugins: []
461
- });
 
 
 
 
 
 
462
 
463
- const domChart = new Chart(document.getElementById('dominanceChart').getContext('2d'), {
464
- type: 'bar',
465
- data: { labels: [], datasets: [{ data: [], backgroundColor: activePalette.dominance, borderRadius: 2 }] },
466
- options: {
467
- responsive: true, maintainAspectRatio: false,
468
- plugins: { legend: { display: false } },
469
- scales: {
470
- x: { grid: { display: false }, ticks: { font: { size: 10, weight: '500' }, color: '#666666' } },
471
- y: { grid: { color: '#1a1a1a' }, beginAtZero: true, ticks: { font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Total Vehicle Count', font: { size: 10, weight: '700' }, color: '#888888' } }
 
 
 
472
  },
473
- animation: { duration: 0 }
474
- },
475
- plugins: []
476
- });
477
 
478
- const flowChart = new Chart(document.getElementById('flowChart').getContext('2d'), {
479
- type: 'bar',
480
- data: { labels: [], datasets: [{ data: [], backgroundColor: activePalette.flow, borderColor: '#0a0a0a', borderWidth: 1.5, barPercentage: 1.0, categoryPercentage: 1.0 }] },
481
- options: {
482
- responsive: true, maintainAspectRatio: false,
483
- plugins: { legend: { display: false } },
484
- scales: {
485
- x: { grid: { display: false }, ticks: { maxTicksLimit: 10, font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Time (seconds)', font: { size: 10, weight: '700' }, color: '#888888' } },
486
- y: { grid: { color: '#1a1a1a' }, beginAtZero: true, ticks: { font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Vehicles Crossed', font: { size: 10, weight: '700' }, color: '#888888' } }
 
 
487
  },
488
- animation: { duration: 0 }
489
- },
490
- plugins: []
491
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
 
493
  // =========== Update functions ===========
494
  function sumValues(obj) { return Object.values(obj).reduce((a, b) => a + b, 0); }
@@ -587,6 +709,14 @@
587
  });
588
  const reportRow = document.getElementById('sv-report');
589
  if (reportRow) reportRow.closest('.s-row').classList.add('disabled');
 
 
 
 
 
 
 
 
590
  const annotatedRow = document.getElementById('sv-annotated');
591
  if (annotatedRow) annotatedRow.closest('.s-row').classList.add('disabled');
592
  const wrap = document.getElementById('settings-start-wrap');
@@ -694,28 +824,6 @@
694
  // =========== Main ===========
695
  let _params = null;
696
 
697
- async function init() {
698
- const raw = sessionStorage.getItem('funky_run');
699
- if (!raw) { window.location.href = './'; return; }
700
-
701
- _params = JSON.parse(raw);
702
-
703
- // SECURITY: Clear session storage so refresh always redirects home
704
- sessionStorage.removeItem('funky_run');
705
-
706
- const cRes = await fetch('constants');
707
- const cData = await cRes.json();
708
- MODEL_CLASSES = cData.classes;
709
- BUSINESS_MAP = cData.business_map;
710
-
711
- populateAndInit(_params);
712
-
713
- // SECURITY: Clear session storage after populate so refresh triggers redirect
714
- sessionStorage.removeItem('funky_run');
715
-
716
- // Show Settings tab first, but also initialized with About implicitly in sidebar hierarchy
717
- switchTab('settings');
718
- }
719
 
720
  function populateAndInit(params) {
721
  populateRunDetails(params.config);
@@ -763,16 +871,7 @@
763
  document.getElementById('proc-label').innerText = 'Processing';
764
 
765
  // Reset Run Tab Results to Awaiting
766
- const analyzeAgainBtn = document.getElementById('run-analyze-again-btn');
767
- if (analyzeAgainBtn) {
768
- analyzeAgainBtn.classList.add('hidden');
769
- }
770
 
771
- const badge = document.getElementById('results-status-badge');
772
- if (badge) {
773
- badge.innerText = 'Processing';
774
- badge.className = 'px-2.5 py-1 bg-slate-800 text-white text-[10px] font-bold rounded-full uppercase tracking-tighter animate-pulse';
775
- }
776
  document.getElementById('run-results-content').innerHTML = `
777
  <div class="flex flex-col items-center justify-center p-8 bg-black/40 border border-slate-800 rounded-2xl col-span-3 text-slate-500">
778
  <i class="fa-solid fa-spinner fa-spin text-2xl mb-3 text-white"></i>
@@ -783,15 +882,16 @@
783
  const repIcon = document.getElementById('reports-pending-icon');
784
  if (repIcon) repIcon.className = 'fa-solid fa-circle-notch fa-spin text-[#c89a6c]';
785
  const repText = document.getElementById('reports-pending-text');
786
- if (repText) repText.innerText = 'Generating artifacts & rendering analytics... Please wait';
787
 
788
 
789
  // Start WebSocket
790
  const videoDuration = _params.config.duration || 10;
791
 
792
  const proto = location.protocol === 'https:' ? 'wss' : 'ws';
793
- const wsPath = location.pathname.endsWith('/') ? location.pathname : location.pathname + '/';
794
- const ws = new WebSocket(`${proto}://${location.host}${wsPath}ws/run`);
 
795
 
796
  ws.onopen = () => {
797
  ws.send(JSON.stringify({
@@ -810,10 +910,6 @@
810
  console.error('WS Error:', e);
811
  document.getElementById('proc-label').innerText = 'Connection Error';
812
  showToast('Connection error — server may be busy', 'error');
813
- if (badge) {
814
- badge.innerText = 'Pipeline Failed';
815
- badge.className = 'px-2.5 py-1 bg-red-900/40 text-red-100 text-[10px] font-bold rounded-full uppercase tracking-tighter';
816
- }
817
  };
818
 
819
  let processingDone = false;
@@ -823,11 +919,6 @@
823
  if (!processingDone) {
824
  // Closed before done=True received — show error state
825
  document.getElementById('proc-label').innerText = 'Disconnected';
826
- const badge = document.getElementById('results-status-badge');
827
- if (badge) {
828
- badge.innerText = 'Connection Lost';
829
- badge.className = 'px-2.5 py-1 bg-red-900/40 text-red-300 text-[10px] font-bold rounded-full uppercase tracking-tighter';
830
- }
831
  document.getElementById('run-results-content').innerHTML = `
832
  <div class="flex flex-col items-center justify-center p-8 border-2 border-dashed border-red-900/40 rounded-2xl col-span-3 text-slate-400">
833
  <i class="fa-solid fa-triangle-exclamation text-2xl mb-3 text-red-400"></i>
@@ -841,6 +932,8 @@
841
  };
842
 
843
  let lastUIUpdate = 0;
 
 
844
 
845
  ws.onmessage = e => {
846
  const d = JSON.parse(e.data);
@@ -851,11 +944,6 @@
851
  if (d.error) {
852
  processingDone = true;
853
  document.getElementById('proc-label').innerText = 'Engine Error';
854
- const badge = document.getElementById('results-status-badge');
855
- if (badge) {
856
- badge.innerText = 'Failed';
857
- badge.className = 'px-2.5 py-1 bg-red-900/40 text-red-300 text-[10px] font-bold rounded-full uppercase tracking-tighter';
858
- }
859
  console.error('[UrbanFlow] Engine error:', d.detail || d.error);
860
  document.getElementById('run-results-content').innerHTML = `
861
  <div class="flex flex-col items-center justify-center p-8 border-2 border-dashed border-red-900/40 rounded-2xl col-span-3 text-slate-400">
@@ -884,22 +972,18 @@
884
  }
885
  }
886
 
887
- // Update Run Tab Badge
888
- const badge = document.getElementById('results-status-badge');
889
- if (badge) {
890
- badge.innerText = 'Completed';
891
- badge.className = 'px-2.5 py-1 bg-white text-black text-[10px] font-bold rounded-full uppercase tracking-tighter';
892
- }
893
 
894
- const analyzeAgainBtn = document.getElementById('run-analyze-again-btn');
895
- if (analyzeAgainBtn) {
896
- analyzeAgainBtn.classList.remove('hidden');
 
897
  }
898
 
 
899
  document.getElementById('run-results-content').innerHTML =
900
- detailRow('Inference Time', d.processing_time + ' sec') +
901
- infoRow('Throughput (FPS)', d.actual_fps, 'Measured frame throughput during processing.', ' <span class="text-xs text-slate-400 font-normal">fps</span>') +
902
- infoRow('Real-time Ratio', d.speed_vs_realtime + 'x', 'Processing speed relative to video playback rate.');
903
 
904
  if (d.video_id) {
905
  loadReports(d.video_id).then(data => {
@@ -925,6 +1009,12 @@
925
  if (jsonToggle) {
926
  jsonToggle.closest('.s-row').classList.add('disabled');
927
  }
 
 
 
 
 
 
928
  const csvToggle = document.getElementById('sv-export-csv');
929
  if (csvToggle) {
930
  csvToggle.closest('.s-row').classList.add('disabled');
@@ -937,6 +1027,7 @@
937
  // Toast + Insights
938
  showToast('Processing complete — artifacts ready', 'success');
939
  renderInsights(d);
 
940
 
941
  // Store video_id for keyboard shortcut download
942
  document.body.setAttribute('data-last-video-id', d.video_id);
@@ -969,14 +1060,26 @@
969
  doughChart.data.datasets[0].data = [totalIn, totalOut];
970
  doughChart.update();
971
 
 
 
 
 
 
 
 
 
 
 
 
 
972
  const now = performance.now();
973
  if (now - lastUIUpdate < 300) return;
974
  lastUIUpdate = now;
975
 
976
- updateCongestion(d.congestion, stride);
977
  updateBreakdown(d.class_in, d.class_out);
978
  updateDominance(d.class_in, d.class_out);
979
- buildFlowHistogram(d.flow_times, videoDuration);
980
  };
981
  }
982
 
@@ -1003,8 +1106,10 @@
1003
  const data = await res.json();
1004
  if (!data.files || !data.files.length) return null;
1005
 
1006
- document.getElementById('reports-pending').classList.add('hidden');
1007
- document.getElementById('reports-pending-message').classList.add('hidden');
 
 
1008
  document.getElementById('post-process-cards').classList.remove('hidden');
1009
  const grid = document.getElementById('reports-grid');
1010
  grid.classList.remove('hidden');
@@ -1105,5 +1210,121 @@
1105
  }
1106
  }
1107
 
1108
- init();
1109
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // =========== Custom Select Dropdowns ===========
2
+ function ufSelectToggle(id) {
3
+ const wrap = document.getElementById(id + '-wrap') || document.getElementById(id + '-trigger')?.closest('.uf-select-wrap');
4
+ const trigger = document.getElementById(id + '-trigger');
5
+ const dropdown = document.getElementById(id + '-dropdown');
6
+ const arrow = document.getElementById(id + '-arrow');
7
+ if (!dropdown) return;
8
+
9
+ const isOpen = !dropdown.classList.contains('hidden');
10
+
11
+ // Close all open dropdowns first
12
+ document.querySelectorAll('.uf-select-dropdown').forEach(d => {
13
+ d.classList.add('hidden');
14
+ d.classList.remove('uf-select-dropdown-up');
15
+ });
16
+ document.querySelectorAll('.uf-select-arrow').forEach(a => a.classList.remove('uf-select-arrow-open'));
17
+
18
+ if (!isOpen) {
19
+ // Detect direction: open upward only if there's not enough space below
20
+ if (trigger) {
21
+ const rect = trigger.getBoundingClientRect();
22
+ const spaceBelow = window.innerHeight - rect.bottom;
23
+ const spaceAbove = rect.top;
24
+ const dropH = 240; // max-height of dropdown
25
+ if (spaceBelow < dropH && spaceAbove > spaceBelow) {
26
+ dropdown.classList.add('uf-select-dropdown-up');
27
+ }
28
+ }
29
+ dropdown.classList.remove('hidden');
30
+ if (arrow) arrow.classList.add('uf-select-arrow-open');
31
+ }
32
+ }
33
+
34
+ function ufSelectPick(id, value, label) {
35
+ const hidden = document.getElementById(id);
36
+ const labelEl = document.getElementById(id + '-label');
37
+ const dropdown = document.getElementById(id + '-dropdown');
38
+ const arrow = document.getElementById(id + '-arrow');
39
+
40
+ if (hidden) hidden.value = value;
41
+ if (labelEl) {
42
+ labelEl.textContent = label;
43
+ labelEl.style.color = '#ffffff'; // selected = white, placeholder was grey
44
+ }
45
+ if (dropdown) dropdown.classList.add('hidden');
46
+ if (arrow) arrow.classList.remove('uf-select-arrow-open');
47
+
48
+ // Mark active option
49
+ if (dropdown) {
50
+ dropdown.querySelectorAll('.uf-select-option').forEach(opt => {
51
+ opt.classList.toggle('uf-select-option-active', opt.dataset.value === value);
52
+ });
53
+ }
54
+
55
+ // Fire side-effects by id
56
+ if (id === 'live-palette') applyPalette(value);
57
+ }
58
+
59
+ // Close on outside click
60
+ document.addEventListener('click', function(e) {
61
+ if (!e.target.closest('.uf-select-wrap')) {
62
+ document.querySelectorAll('.uf-select-dropdown').forEach(d => d.classList.add('hidden'));
63
+ document.querySelectorAll('.uf-select-arrow').forEach(a => a.classList.remove('uf-select-arrow-open'));
64
+ }
65
+ });
66
 
67
+ // =========== Rolling Counter ===========
68
  function animateValue(obj, start, end, duration) {
69
  let startTimestamp = null;
70
  const isPct = typeof end === 'string' && end.includes('%');
 
92
  window.requestAnimationFrame(step);
93
  }
94
 
95
+ // Close all open tooltips
96
+ function closeAllTips() {
97
+ document.querySelectorAll('.info-tip').forEach(tip => {
98
+ tip.style.display = 'none';
99
+ });
100
+ }
101
+
102
+ function positionTip(tip, wrap) {
103
+ const rect = wrap.getBoundingClientRect();
104
+ const tipH = tip.offsetHeight || 60;
105
+ if (rect.bottom + tipH + 10 > window.innerHeight) {
106
+ tip.style.top = (rect.top - tipH - 6) + 'px';
107
+ } else {
108
+ tip.style.top = (rect.bottom + 6) + 'px';
109
+ }
110
+ tip.style.left = Math.min(rect.left, window.innerWidth - 260) + 'px';
111
+ }
112
+
113
+ // Desktop hover only — never fires on touch
114
+ document.addEventListener('mouseover', e => {
115
+ if (window.matchMedia('(hover: none)').matches) return;
116
+ const wrap = e.target.closest('.info-wrap');
117
+ if (!wrap) return;
118
+ const tip = wrap.querySelector('.info-tip');
119
+ if (!tip) return;
120
+ tip.style.display = 'block';
121
+ positionTip(tip, wrap);
122
+ });
123
+
124
+ document.addEventListener('mouseout', e => {
125
+ if (window.matchMedia('(hover: none)').matches) return;
126
+ const wrap = e.target.closest('.info-wrap');
127
+ if (!wrap) return;
128
+ const tip = wrap.querySelector('.info-tip');
129
+ if (tip) tip.style.display = 'none';
130
+ });
131
+
132
+ // Tap/click toggle — works on both desktop and mobile
133
+ document.addEventListener('click', e => {
134
+ const btn = e.target.closest('.info-btn');
135
+ if (!btn) {
136
+ if (!e.target.closest('.info-tip') && !e.target.closest('.info-wrap')) {
137
+ closeAllTips();
138
+ }
139
+ return;
140
+ }
141
+ e.stopPropagation();
142
+ const wrap = btn.closest('.info-wrap');
143
+ if (!wrap) return;
144
+ const tip = wrap.querySelector('.info-tip');
145
+ if (!tip) return;
146
+
147
+ const isVisible = tip.style.display === 'block';
148
+ closeAllTips();
149
+ if (!isVisible) {
150
+ tip.style.display = 'block';
151
+ positionTip(tip, wrap);
152
+ }
153
+ });
154
+
155
+ // ---- Tab switching — updates both sidebar + mobile bottom nav ----
156
+ function switchTab(tab) {
157
+ const allTabs = ['about', 'overview', 'run-details', 'reports', 'settings', 'help', 'feedback'];
158
+
159
+ allTabs.forEach(t => {
160
+ // Content panels
161
+ const el = document.getElementById('tab-' + t);
162
+ if (el) el.classList.toggle('hidden', tab !== t);
163
+
164
+ // Desktop sidebar nav items
165
+ const nav = document.getElementById('nav-' + t);
166
+ if (nav) {
167
+ if (tab === t) {
168
+ nav.classList.add('nav-item-active');
169
+ nav.classList.remove('nav-item-inactive');
170
  } else {
171
+ nav.classList.remove('nav-item-active');
172
+ nav.classList.add('nav-item-inactive');
173
  }
174
+ }
 
 
 
 
 
 
 
 
175
 
176
+ // Mobile bottom nav items
177
+ const mobNav = document.getElementById('mob-nav-' + t);
178
+ if (mobNav) {
179
+ mobNav.classList.toggle('active', tab === t);
180
+ if (tab === 'reports' && t === 'reports') {
181
+ mobNav.classList.remove('notify-glow');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  }
184
+ });
185
+ }
186
 
187
  // =========== Toast System ===========
188
  function showToast(message, type) {
 
334
  const panel = document.getElementById('insights-panel');
335
  panel.classList.remove('hidden');
336
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  // Congestion insights
338
  const ci = document.getElementById('congestion-insights');
339
  const pcu = d.pcu || {};
340
  ci.innerHTML = [
341
  infoRow('Total PCU', pcu.total_pcu || 0, 'Passenger Car Units (IRC:106-1990). Normalizes mixed traffic.'),
342
  infoRow('PCU In / Out', `${pcu.pcu_in || 0} / ${pcu.pcu_out || 0}`, 'Directional PCU split.'),
 
343
  ].join('');
344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  // PCU card in Stats
346
  const pcuCard = document.getElementById('pcu-stats-card');
347
  if (pcuCard) {
348
+ const totalPcu = pcu.total_pcu || 0;
349
+
350
+ // Capacity label based on total PCU volume
351
+ let capLabel, capColor, capNote;
352
+ if (totalPcu < 50) {
353
+ capLabel = 'Very Low Volume'; capColor = '#22c55e';
354
+ capNote = 'Minimal traffic on this segment';
355
+ } else if (totalPcu < 200) {
356
+ capLabel = 'Low Volume'; capColor = '#22c55e';
357
+ capNote = 'Below typical urban arterial demand';
358
+ } else if (totalPcu < 500) {
359
+ capLabel = 'Moderate Volume'; capColor = '#c89a6c';
360
+ capNote = 'Typical mixed urban traffic';
361
+ } else if (totalPcu < 900) {
362
+ capLabel = 'High Volume'; capColor = '#f97316';
363
+ capNote = 'Approaching single-lane capacity (IRC:106)';
364
+ } else {
365
+ capLabel = 'Near/Over Capacity'; capColor = '#ef4444';
366
+ capNote = 'Exceeds IRC single-lane urban reference (1000 PCU)';
367
+ }
368
+
369
+ // Dominant class from per_class breakdown
370
+ let domClass = '—', domFactor = '—';
371
+ if (pcu.per_class && Object.keys(pcu.per_class).length > 0) {
372
+ const sorted = Object.entries(pcu.per_class)
373
+ .sort((a, b) => b[1].pcu - a[1].pcu);
374
+ domClass = sorted[0][0];
375
+ domFactor = sorted[0][1].factor;
376
+ }
377
+
378
+ pcuCard.innerHTML = `
379
+ <div class="w-full space-y-3">
380
+ <div>
381
+ <span class="text-base font-black uppercase tracking-wide"
382
+ style="color:${capColor}">${capLabel}</span>
383
+ <p class="text-[10px] mt-0.5" style="color:#a89f97">${capNote}</p>
384
+ <p class="text-[10px] mt-1" style="color:#666">
385
+ Dominant: ${domClass} · PCU factor ${domFactor}
386
+ </p>
387
  </div>
388
+ <div class="flex justify-between items-center pt-1">
389
+ <span class="text-xs font-medium text-slate-500">Total PCU</span>
390
+ <span class="text-2xl font-black" style="color:#8b5e3c">${totalPcu}</span>
391
  </div>
392
+ <div class="flex gap-3">
393
+ <div class="flex-1 bg-green-950 rounded-lg p-2.5 text-center border border-green-900">
394
+ <div class="text-lg font-bold text-green-400">${pcu.pcu_in || 0}</div>
395
+ <div class="text-[9px] font-bold text-green-600 uppercase">PCU In</div>
396
+ </div>
397
+ <div class="flex-1 bg-red-950 rounded-lg p-2.5 text-center border border-red-900">
398
+ <div class="text-lg font-bold text-red-400">${pcu.pcu_out || 0}</div>
399
+ <div class="text-[9px] font-bold text-red-600 uppercase">PCU Out</div>
400
+ </div>
401
+ </div>
402
+ </div>`;
403
  }
404
  }
405
 
 
432
  const res = c.resolution || [0, 0];
433
 
434
  document.getElementById('panel-video').innerHTML =
435
+ detailRow('video_fps', (c.video_fps || 0).toFixed(2)) +
436
  detailRow('frames', c.frames) +
437
+ detailRow('duration', (c.duration || 0).toFixed(2) + ' sec') +
438
  detailRow('resolution', res[0] + ' <span class="text-slate-400 text-xs">x</span> ' + res[1]) +
439
  detailRow('pixels', (c.pixels || 0).toLocaleString());
440
 
 
490
  gold: { congestion: '#fbbf24', congestionBg: 'rgba(251,191,36,0.08)', dominance: '#a8a29e', flow: '#f5f5f4', doughIn: '#f5f5f4', doughOut: '#fbbf24' }
491
  };
492
 
493
+ // =========== Global State (Initialized in initApp) ===========
494
+ let currentPalette = 'default';
495
+ let activePalette = PALETTES.default;
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  let MODEL_CLASSES = {};
497
  let BUSINESS_MAP = {};
498
+ let congChart, doughChart, domChart, flowChart;
499
+
500
+ async function initApp() {
501
+ // Read settings from sessionStorage
502
+ const rawRun = sessionStorage.getItem('funky_run');
503
+ const runSettings = rawRun ? (JSON.parse(rawRun).settings || {}) : {};
504
+ currentPalette = runSettings.palette || 'default';
505
+ activePalette = PALETTES[currentPalette];
506
+
507
+ // =========== Charts ===========
508
+ Chart.defaults.font.family = "'Montserrat', sans-serif";
509
+ Chart.defaults.color = '#888888';
510
+ Chart.defaults.borderColor = '#222222';
511
+ Chart.defaults.plugins.tooltip.backgroundColor = '#0a0a0a';
512
+ Chart.defaults.plugins.tooltip.titleColor = '#ffffff';
513
+ Chart.defaults.plugins.tooltip.bodyColor = '#aaaaaa';
514
+ Chart.defaults.plugins.tooltip.borderColor = '#222222';
515
+ Chart.defaults.plugins.tooltip.borderWidth = 1;
516
+
517
+ congChart = new Chart(document.getElementById('congestionChart').getContext('2d'), {
518
+ type: 'line',
519
+ data: {
520
+ labels: [], datasets: [{
521
+ data: [],
522
+ borderColor: activePalette.congestion,
523
+ backgroundColor: activePalette.congestionBg,
524
+ fill: true,
525
+ tension: 0.2,
526
+ borderWidth: 1.5,
527
+ pointRadius: 0
528
+ }]
529
  },
530
+ options: {
531
+ responsive: true, maintainAspectRatio: false,
532
+ plugins: { legend: { display: false } },
533
+ scales: {
534
+ x: { grid: { display: false }, ticks: { maxTicksLimit: 10, font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Frame Index', font: { size: 10, weight: '700' }, color: '#888888' } },
535
+ y: { grid: { color: '#1a1a1a' }, beginAtZero: true, ticks: { font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Active Vehicles', font: { size: 10, weight: '700' }, color: '#888888' } }
536
+ },
537
+ animation: { duration: 0 }
538
+ },
539
+ plugins: []
540
+ });
541
 
542
+ doughChart = new Chart(document.getElementById('doughnutChart').getContext('2d'), {
543
+ type: 'doughnut',
544
+ data: {
545
+ labels: ['Incoming', 'Outgoing'], datasets: [{
546
+ data: [0, 0],
547
+ backgroundColor: [activePalette.doughIn, activePalette.doughOut],
548
+ borderColor: '#0a0a0a',
549
+ borderWidth: 3,
550
+ hoverOffset: 6
551
+ }]
 
 
 
 
 
 
552
  },
553
+ options: {
554
+ responsive: true, maintainAspectRatio: false,
555
+ cutout: '68%',
556
+ plugins: {
557
+ legend: { display: true, position: 'bottom', labels: { padding: 12, usePointStyle: true, pointStyle: 'circle', font: { size: 10, weight: '600' } } }
558
+ },
559
+ animation: { duration: 0 }
560
+ },
561
+ plugins: []
562
+ });
563
 
564
+ domChart = new Chart(document.getElementById('dominanceChart').getContext('2d'), {
565
+ type: 'bar',
566
+ data: { labels: [], datasets: [{ data: [], backgroundColor: activePalette.dominance, borderRadius: 2 }] },
567
+ options: {
568
+ responsive: true, maintainAspectRatio: false,
569
+ layout: { padding: { bottom: 12 } },
570
+ plugins: { legend: { display: false } },
571
+ scales: {
572
+ x: { grid: { display: false }, ticks: { font: { size: 9, weight: '500' }, color: '#666666', maxRotation: 45, minRotation: 30, autoSkip: false } },
573
+ y: { grid: { color: '#1a1a1a' }, beginAtZero: true, ticks: { font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Total Vehicle Count', font: { size: 10, weight: '700' }, color: '#888888' } }
574
+ },
575
+ animation: { duration: 0 }
576
  },
577
+ plugins: []
578
+ });
 
 
579
 
580
+ flowChart = new Chart(document.getElementById('flowChart').getContext('2d'), {
581
+ type: 'bar',
582
+ data: { labels: [], datasets: [{ data: [], backgroundColor: activePalette.flow, borderColor: '#0a0a0a', borderWidth: 1.5, barPercentage: 1.0, categoryPercentage: 1.0 }] },
583
+ options: {
584
+ responsive: true, maintainAspectRatio: false,
585
+ plugins: { legend: { display: false } },
586
+ scales: {
587
+ x: { grid: { display: false }, ticks: { maxTicksLimit: 10, font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Time (seconds)', font: { size: 10, weight: '700' }, color: '#888888' } },
588
+ y: { grid: { color: '#1a1a1a' }, beginAtZero: true, ticks: { font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Vehicles Crossed', font: { size: 10, weight: '700' }, color: '#888888' } }
589
+ },
590
+ animation: { duration: 0 }
591
  },
592
+ plugins: []
593
+ });
594
+
595
+ // Original init() logic
596
+ const raw = sessionStorage.getItem('funky_run');
597
+ if (!raw) { window.location.href = './'; return; }
598
+
599
+ _params = JSON.parse(raw);
600
+ sessionStorage.removeItem('funky_run');
601
+
602
+ const cRes = await fetch('constants');
603
+ const cData = await cRes.json();
604
+ MODEL_CLASSES = cData.classes;
605
+ BUSINESS_MAP = cData.business_map;
606
+
607
+ populateAndInit(_params);
608
+ sessionStorage.removeItem('funky_run');
609
+
610
+ // Sync current active tab from session if set
611
+ const activeTab = sessionStorage.getItem('uf_active_tab') || 'settings';
612
+ switchTab(activeTab);
613
+ }
614
 
615
  // =========== Update functions ===========
616
  function sumValues(obj) { return Object.values(obj).reduce((a, b) => a + b, 0); }
 
709
  });
710
  const reportRow = document.getElementById('sv-report');
711
  if (reportRow) reportRow.closest('.s-row').classList.add('disabled');
712
+ // Also directly hide the wrap and close dropdown — CSS may be cached
713
+ const svWrap = document.getElementById('sv-report-wrap');
714
+ if (svWrap) {
715
+ svWrap.style.pointerEvents = 'none';
716
+ svWrap.style.opacity = '0.4';
717
+ const dd = document.getElementById('sv-report-dropdown');
718
+ if (dd) dd.classList.add('hidden');
719
+ }
720
  const annotatedRow = document.getElementById('sv-annotated');
721
  if (annotatedRow) annotatedRow.closest('.s-row').classList.add('disabled');
722
  const wrap = document.getElementById('settings-start-wrap');
 
824
  // =========== Main ===========
825
  let _params = null;
826
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
827
 
828
  function populateAndInit(params) {
829
  populateRunDetails(params.config);
 
871
  document.getElementById('proc-label').innerText = 'Processing';
872
 
873
  // Reset Run Tab Results to Awaiting
 
 
 
 
874
 
 
 
 
 
 
875
  document.getElementById('run-results-content').innerHTML = `
876
  <div class="flex flex-col items-center justify-center p-8 bg-black/40 border border-slate-800 rounded-2xl col-span-3 text-slate-500">
877
  <i class="fa-solid fa-spinner fa-spin text-2xl mb-3 text-white"></i>
 
882
  const repIcon = document.getElementById('reports-pending-icon');
883
  if (repIcon) repIcon.className = 'fa-solid fa-circle-notch fa-spin text-[#c89a6c]';
884
  const repText = document.getElementById('reports-pending-text');
885
+ if (repText) repText.innerText = 'Artifacts will be available after processing completes... Please wait';
886
 
887
 
888
  // Start WebSocket
889
  const videoDuration = _params.config.duration || 10;
890
 
891
  const proto = location.protocol === 'https:' ? 'wss' : 'ws';
892
+ // Get the directory path (e.g., /app/ or /) rather than the full filename
893
+ const dirPath = location.pathname.substring(0, location.pathname.lastIndexOf('/') + 1);
894
+ const ws = new WebSocket(`${proto}://${location.host}${dirPath}ws/run`);
895
 
896
  ws.onopen = () => {
897
  ws.send(JSON.stringify({
 
910
  console.error('WS Error:', e);
911
  document.getElementById('proc-label').innerText = 'Connection Error';
912
  showToast('Connection error — server may be busy', 'error');
 
 
 
 
913
  };
914
 
915
  let processingDone = false;
 
919
  if (!processingDone) {
920
  // Closed before done=True received — show error state
921
  document.getElementById('proc-label').innerText = 'Disconnected';
 
 
 
 
 
922
  document.getElementById('run-results-content').innerHTML = `
923
  <div class="flex flex-col items-center justify-center p-8 border-2 border-dashed border-red-900/40 rounded-2xl col-span-3 text-slate-400">
924
  <i class="fa-solid fa-triangle-exclamation text-2xl mb-3 text-red-400"></i>
 
932
  };
933
 
934
  let lastUIUpdate = 0;
935
+ let liveCongestion = [];
936
+ let liveFlowTimes = [];
937
 
938
  ws.onmessage = e => {
939
  const d = JSON.parse(e.data);
 
944
  if (d.error) {
945
  processingDone = true;
946
  document.getElementById('proc-label').innerText = 'Engine Error';
 
 
 
 
 
947
  console.error('[UrbanFlow] Engine error:', d.detail || d.error);
948
  document.getElementById('run-results-content').innerHTML = `
949
  <div class="flex flex-col items-center justify-center p-8 border-2 border-dashed border-red-900/40 rounded-2xl col-span-3 text-slate-400">
 
972
  }
973
  }
974
 
 
 
 
 
 
 
975
 
976
+ // GLOW NOTIFICATION: Let the user know artifacts are ready
977
+ const reportsMob = document.getElementById('mob-nav-reports');
978
+ if (reportsMob) {
979
+ reportsMob.classList.add('notify-glow');
980
  }
981
 
982
+
983
  document.getElementById('run-results-content').innerHTML =
984
+ detailRow('Inference Time', (d.processing_time || 0).toFixed(2) + ' sec') +
985
+ infoRow('Throughput (FPS)', (d.actual_fps || 0).toFixed(2), 'Measured frame throughput during processing.', ' <span class="text-xs text-slate-400 font-normal">fps</span>') +
986
+ infoRow('Real-time Ratio', (d.speed_vs_realtime || 0).toFixed(2) + 'x', 'Processing speed relative to video playback rate.');
987
 
988
  if (d.video_id) {
989
  loadReports(d.video_id).then(data => {
 
1009
  if (jsonToggle) {
1010
  jsonToggle.closest('.s-row').classList.add('disabled');
1011
  }
1012
+
1013
+ // NOTIFY USER: Glow the artifacts (reports) icon in mobile nav
1014
+ const reportsNav = document.getElementById('mob-nav-reports');
1015
+ if (reportsNav) {
1016
+ reportsNav.classList.add('notify-glow');
1017
+ }
1018
  const csvToggle = document.getElementById('sv-export-csv');
1019
  if (csvToggle) {
1020
  csvToggle.closest('.s-row').classList.add('disabled');
 
1027
  // Toast + Insights
1028
  showToast('Processing complete — artifacts ready', 'success');
1029
  renderInsights(d);
1030
+ showRetryBubble();
1031
 
1032
  // Store video_id for keyboard shortcut download
1033
  document.body.setAttribute('data-last-video-id', d.video_id);
 
1060
  doughChart.data.datasets[0].data = [totalIn, totalOut];
1061
  doughChart.update();
1062
 
1063
+ // Live history management (Backend sends incremental updates now)
1064
+ if (d.congestion_last !== undefined) {
1065
+ liveCongestion.push(d.congestion_last);
1066
+ }
1067
+ if (d.flow_count !== undefined) {
1068
+ // Approximate timestamps for new crossings based on current frame
1069
+ const videoFps = _params.config.video_fps || 30;
1070
+ while (liveFlowTimes.length < d.flow_count) {
1071
+ liveFlowTimes.push(d.frame_index * stride / videoFps);
1072
+ }
1073
+ }
1074
+
1075
  const now = performance.now();
1076
  if (now - lastUIUpdate < 300) return;
1077
  lastUIUpdate = now;
1078
 
1079
+ updateCongestion(liveCongestion, stride);
1080
  updateBreakdown(d.class_in, d.class_out);
1081
  updateDominance(d.class_in, d.class_out);
1082
+ buildFlowHistogram(liveFlowTimes, videoDuration);
1083
  };
1084
  }
1085
 
 
1106
  const data = await res.json();
1107
  if (!data.files || !data.files.length) return null;
1108
 
1109
+ const rPending = document.getElementById('reports-pending');
1110
+ if (rPending) rPending.classList.add('hidden');
1111
+ const rMsg = document.getElementById('reports-pending-message');
1112
+ if (rMsg) rMsg.classList.add('hidden');
1113
  document.getElementById('post-process-cards').classList.remove('hidden');
1114
  const grid = document.getElementById('reports-grid');
1115
  grid.classList.remove('hidden');
 
1210
  }
1211
  }
1212
 
1213
+ function showRetryBubble() {
1214
+ if (sessionStorage.getItem('uf_retry_shown')) return;
1215
+ sessionStorage.setItem('uf_retry_shown', '1');
1216
+
1217
+ const existing = document.getElementById('retry-bubble');
1218
+ if (existing) existing.remove();
1219
+
1220
+ const isMobile = window.innerWidth < 1024;
1221
+ const bubble = document.createElement('div');
1222
+ bubble.id = 'retry-bubble';
1223
+
1224
+ Object.assign(bubble.style, {
1225
+ position: 'fixed',
1226
+ background: '#111111',
1227
+ border: '1px solid #c89a6c',
1228
+ borderRadius: '12px',
1229
+ padding: '12px 16px',
1230
+ fontFamily: 'Montserrat, sans-serif',
1231
+ color: '#f0ece6',
1232
+ zIndex: '9000',
1233
+ boxShadow: '0 8px 32px rgba(0,0,0,0.8)',
1234
+ maxWidth: '220px',
1235
+ textAlign: 'center',
1236
+ lineHeight: '1.4',
1237
+ });
1238
+
1239
+ bubble.innerHTML = `
1240
+ <p style="color:#c89a6c;font-weight:800;font-size:13px;margin:0 0 5px 0">
1241
+ Want to try another video?
1242
+ </p>
1243
+ <p style="color:#a89f97;font-size:11px;margin:0">
1244
+ Tap <b style="color:#f0ece6">&#9881; Settings</b> to reset &amp; re-run
1245
+ </p>
1246
+ <div id="retry-bubble-arrow"></div>
1247
+ `;
1248
+
1249
+ // Append to fixed portal so overflow:hidden on body/main never clips it
1250
+ const portal = document.getElementById('fixed-portal') || document.body;
1251
+ portal.style.pointerEvents = 'none';
1252
+ bubble.style.pointerEvents = 'auto';
1253
+ portal.appendChild(bubble);
1254
+
1255
+ if (isMobile) {
1256
+ // 7 icons in bottom nav. Settings = 5th (index 4).
1257
+ // Center of 5th icon = (4 + 0.5) / 7 = 64.28% of viewport width.
1258
+ const navH = parseInt(
1259
+ getComputedStyle(document.documentElement)
1260
+ .getPropertyValue('--mob-nav-h') || '68', 10
1261
+ );
1262
+ const vpW = window.innerWidth;
1263
+ const settingsCenterX = (4.5 / 7) * vpW;
1264
+ const bubbleW = bubble.offsetWidth || 220;
1265
+ const leftPx = Math.max(8, Math.min(settingsCenterX - bubbleW / 2, vpW - bubbleW - 8));
1266
+
1267
+ Object.assign(bubble.style, {
1268
+ bottom: (navH + 10) + 'px',
1269
+ left: leftPx + 'px',
1270
+ top: 'auto',
1271
+ right: 'auto',
1272
+ transform: 'none',
1273
+ });
1274
+
1275
+ const arrowLeft = settingsCenterX - leftPx;
1276
+ const arrow = bubble.querySelector('#retry-bubble-arrow');
1277
+ Object.assign(arrow.style, {
1278
+ position: 'absolute',
1279
+ bottom: '-8px',
1280
+ left: arrowLeft + 'px',
1281
+ transform: 'translateX(-50%)',
1282
+ width: '0', height: '0',
1283
+ borderLeft: '8px solid transparent',
1284
+ borderRight: '8px solid transparent',
1285
+ borderTop: '8px solid #c89a6c',
1286
+ });
1287
+
1288
+ } else {
1289
+ // Desktop sidebar <aside class="w-60"> = 240px.
1290
+ // Settings is 5th nav link: logo(112px) + 4 items × 44px + 22px = 310px from top.
1291
+ const settingsY = 310;
1292
+ Object.assign(bubble.style, {
1293
+ left: '256px',
1294
+ top: (settingsY - 40) + 'px',
1295
+ bottom: 'auto',
1296
+ right: 'auto',
1297
+ transform: 'none',
1298
+ textAlign: 'left',
1299
+ });
1300
+
1301
+ const arrow = bubble.querySelector('#retry-bubble-arrow');
1302
+ Object.assign(arrow.style, {
1303
+ position: 'absolute',
1304
+ left: '-8px',
1305
+ top: '50%',
1306
+ transform: 'translateY(-50%)',
1307
+ width: '0', height: '0',
1308
+ borderTop: '8px solid transparent',
1309
+ borderBottom: '8px solid transparent',
1310
+ borderRight: '8px solid #c89a6c',
1311
+ });
1312
+ }
1313
+
1314
+ bubble.addEventListener('click', () => bubble.remove());
1315
+ setTimeout(() => {
1316
+ const el = document.getElementById('retry-bubble');
1317
+ if (el) el.remove();
1318
+ }, 6000);
1319
+ }
1320
+
1321
+ document.addEventListener('DOMContentLoaded', () => {
1322
+ // Phase 1: instant visual — show the shell immediately on first paint
1323
+ const activeTab = sessionStorage.getItem('uf_active_tab') || 'settings';
1324
+ switchTab(activeTab);
1325
+
1326
+ // Phase 2: defer all heavy init until after browser completes first paint
1327
+ requestAnimationFrame(() => {
1328
+ setTimeout(initApp, 0);
1329
+ });
1330
+ });
frontend/manifest.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "UrbanFlow: Your Vision Partner",
3
+ "short_name": "UrbanFlow",
4
+ "description": "Premium Traffic Intelligence Application",
5
+ "start_url": "/?pwa=true",
6
+ "display": "standalone",
7
+ "background_color": "#000000",
8
+ "theme_color": "#000000",
9
+ "orientation": "portrait",
10
+ "icons": [
11
+ {
12
+ "src": "assets/shuriken.png",
13
+ "sizes": "512x512",
14
+ "type": "image/png",
15
+ "purpose": "any"
16
+ },
17
+ {
18
+ "src": "assets/shurkien_b.png",
19
+ "sizes": "512x512",
20
+ "type": "image/png",
21
+ "purpose": "maskable"
22
+ }
23
+ ]
24
+ }
frontend/sw.js ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const CACHE_NAME = 'urbanflow-v4';
2
+ const ASSETS = [
3
+ './css/initial.css',
4
+ './css/vehicles.css',
5
+ './assets/shuriken.png',
6
+ './assets/shurkien_b.png',
7
+ './assets/uf_rf.png'
8
+ ];
9
+
10
+ self.addEventListener('install', (e) => {
11
+ e.waitUntil(caches.open(CACHE_NAME).then(c => c.addAll(ASSETS)));
12
+ });
13
+
14
+ self.addEventListener('fetch', (e) => {
15
+ const url = new URL(e.request.url);
16
+
17
+ // NEVER cache WebSockets or API calls
18
+ if (url.pathname.includes('/ws/') || url.pathname.includes('/reports/') || url.pathname.includes('/bundle/')) {
19
+ return;
20
+ }
21
+
22
+ e.respondWith(
23
+ fetch(e.request).catch(() => caches.match(e.request))
24
+ );
25
+ });
frontend/vehicles.html CHANGED
@@ -3,9 +3,15 @@
3
 
4
  <head>
5
  <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <title>UrbanFlow</title>
8
  <link rel="icon" type="image/png" href="assets/rf.png">
 
 
 
 
 
 
9
  <script src="https://cdn.tailwindcss.com"></script>
10
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
11
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
@@ -16,7 +22,27 @@
16
  <link rel="stylesheet" href="css/vehicles.css">
17
  </head>
18
 
19
- <body class="bg-black text-white h-screen w-screen overflow-hidden flex">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  <!-- Sidebar -->
22
  <aside class="w-60 bg-white shadow-xl flex flex-col z-20 flex-shrink-0 border-r border-slate-200 relative">
@@ -96,7 +122,8 @@
96
 
97
  <!-- TAB: About -->
98
  <div id="tab-about" class="hidden flex-1 min-h-0 overflow-y-auto">
99
- <div class="bg-black border rounded-xl p-12 shadow-2xl space-y-8 flex flex-col justify-center" style="border-color:#2a2a2a">
 
100
  <div class="text-center">
101
  <p class="text-sm italic font-bold leading-relaxed max-w-3xl mx-auto"
102
  style="color:#c89a6c;font-family:'Montserrat',sans-serif">
@@ -131,7 +158,8 @@
131
  </p>
132
  </div>
133
 
134
- <div class="grid grid-cols-3 gap-8 pt-8 text-left border-t max-w-5xl mx-auto" style="border-color:#1a1a1a">
 
135
  <div class="space-y-4">
136
  <h4 class="font-bold text-[13px] uppercase tracking-wider" style="color:#f0ece6">What You Get
137
  </h4>
@@ -155,52 +183,17 @@
155
  </ul>
156
  </div>
157
  <div class="space-y-4">
158
- <h4 class="font-bold text-[13px] uppercase tracking-wider" style="color:#f0ece6">How It Works
159
- </h4>
160
- <ul class="text-xs space-y-3 pl-1">
161
- <li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
162
- style="color:#c89a6c"></i>
163
- <span>Upload footage from any existing camera &mdash; phone, CCTV, dashcam</span>
164
- </li>
165
- <li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
166
- style="color:#c89a6c"></i>
167
- <span>Draw a counting boundary on the first frame</span>
168
- </li>
169
- <li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
170
- style="color:#c89a6c"></i>
171
- <span>Inference runs in the cloud &mdash; no local GPU or setup needed</span>
172
- </li>
173
- <li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
174
- style="color:#c89a6c"></i>
175
- <span>Review live KPIs and download artifacts when complete</span>
176
- </li>
177
- </ul>
178
- </div>
179
- <div class="space-y-4">
180
- <h4 class="font-bold text-[13px] uppercase tracking-wider" style="color:#f0ece6">What's Coming
181
  </h4>
182
- <ul class="text-xs space-y-3 pl-1" style="color:#a89f97">
183
- <li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
184
- style="color:#8b5e3c"></i>
185
- <span>RTSP live-stream support &mdash; connect directly to IP cameras</span>
186
- </li>
187
- <li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
188
- style="color:#8b5e3c"></i>
189
- <span>ANPR &mdash; automatic number plate recognition and logging</span>
190
- </li>
191
- <li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
192
- style="color:#8b5e3c"></i>
193
- <span>Helmet compliance detection &mdash; rider safety enforcement</span>
194
- </li>
195
- <li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
196
- style="color:#8b5e3c"></i>
197
- <span>Traffic rule violation detection &mdash; signal jumping, wrong-way driving</span>
198
- </li>
199
- <li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
200
- style="color:#8b5e3c"></i>
201
- <span>Enterprise deployment with dedicated SLAs and onboarding</span>
202
- </li>
203
- </ul>
204
  </div>
205
  </div>
206
 
@@ -221,8 +214,8 @@
221
 
222
 
223
  <!-- Progress Bar (shared) -->
224
- <div
225
- class="bg-white rounded-xl px-6 py-4 border border-slate-200 shadow-sm flex items-center justify-between flex-shrink-0">
226
  <div class="flex items-center space-x-4 flex-1 mr-6">
227
  <span class="text-[11px] font-black text-white uppercase tracking-wider whitespace-nowrap"
228
  id="proc-label">Waiting</span>
@@ -234,18 +227,24 @@
234
  <div class="flex items-center space-x-6 text-xs font-bold text-white whitespace-nowrap">
235
  <span id="proc-frames">0 / 0 Frames</span>
236
  <span id="proc-pct">0%</span>
237
- <select class="custom-select" id="live-palette-select" onchange="applyPalette(this.value)"
238
- style="font-size:10px;padding:3px 20px 3px 8px">
239
- <option value="default" selected>Default</option>
240
- <option value="vibrant">Vibrant</option>
241
- <option value="corporate">Corporate</option>
242
- <option value="neon">Neon Night</option>
243
- <option value="earth">Earth Tones</option>
244
- <option value="ocean">Ocean Breeze</option>
245
- <option value="sunset">Sunset Glow</option>
246
- <option value="midnight">Midnight Deep</option>
247
- <option value="gold">Monochrome Gold</option>
248
- </select>
 
 
 
 
 
 
249
  </div>
250
  </div>
251
 
@@ -320,7 +319,8 @@
320
  <span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
321
  <span class="info-tip">Aggregated vehicle count grouped by dominant category.</span></span>
322
  </h3>
323
- <span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Dominant
 
324
  Intelligence</span>
325
  </div>
326
  <div class="flex-1 w-full relative min-h-[220px]">
@@ -353,31 +353,24 @@
353
  <div class="space-y-6 w-full max-w-[1400px] mx-auto">
354
 
355
  <!-- HERO: Process Analytics -->
356
- <div id="run-results-card"
357
- class="bg-white rounded-xl border shadow-sm overflow-hidden flex flex-col" style="border-color:#2a2a2a">
358
- <div class="px-6 py-4 border-b flex justify-between items-center" style="border-color:#1a1a1a;background:#050505">
359
- <div>
 
360
  <h3 class="font-bold text-sm" style="color:#f0ece6">Process Analytics</h3>
361
- <p
362
- class="text-[10px] mt-0.5 uppercase tracking-widest font-medium text-center" style="color:#a89f97">
363
  Execution Telemetry</p>
364
  </div>
365
  <div class="flex items-center gap-3">
366
- <button id="run-analyze-again-btn" onclick="switchTab('settings')"
367
- class="hidden px-4 py-1.5 bg-[#c89a6c] text-white text-[10px] font-bold rounded-full shadow-sm hover:bg-[#8b5e3c] transition-colors"><i
368
- class="fa-solid fa-gear mr-1"></i> Configure & Analyze Again</button>
369
- <div id="results-status-badge"
370
- class="px-3 py-1.5 bg-amber-900/30 border border-amber-800/50 text-amber-500 text-[10px] font-bold rounded-full uppercase tracking-widest shadow-sm flex items-center gap-2">
371
- <div class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></div>System Standby
372
- </div>
373
  </div>
374
  </div>
375
  <div class="p-8">
376
  <div id="run-results-content" class="grid grid-cols-3 gap-12">
377
- <div
378
- class="flex flex-col items-center justify-center p-8 border-2 border-dashed rounded-2xl col-span-3" style="border-color:#2a2a2a;color:#777">
379
- <i class="fa-solid fa-chart-line text-4xl mb-4"></i>
380
- <span class="text-xs font-semibold">Initiate a run to view performance insights</span>
381
  </div>
382
  </div>
383
  </div>
@@ -385,25 +378,29 @@
385
 
386
  <!-- Technical Context Row -->
387
  <div class="grid grid-cols-2 gap-6">
388
- <div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden" style="border-color:#2a2a2a">
 
389
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
390
  <h3 class="font-bold text-sm" style="color:#f0ece6">Stream Source Profile</h3>
391
  </div>
392
  <div class="p-6 space-y-4" id="panel-video"></div>
393
  </div>
394
- <div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden" style="border-color:#2a2a2a">
 
395
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
396
  <h3 class="font-bold text-sm" style="color:#f0ece6">System Resource Utilization</h3>
397
  </div>
398
  <div class="p-6 space-y-4" id="panel-perf"></div>
399
  </div>
400
- <div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden" style="border-color:#2a2a2a">
 
401
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
402
  <h3 class="font-bold text-sm" style="color:#f0ece6">Model Architecture &amp; Logic</h3>
403
  </div>
404
  <div class="p-6 space-y-4" id="panel-model"></div>
405
  </div>
406
- <div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden" style="border-color:#2a2a2a">
 
407
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
408
  <h3 class="font-bold text-sm" style="color:#f0ece6">Inference Parameters</h3>
409
  </div>
@@ -413,25 +410,12 @@
413
 
414
  <!-- Insights Panel (xAI) -->
415
  <div id="insights-panel" class="hidden">
416
- <div class="grid grid-cols-2 gap-6 mt-6">
417
- <div
418
- class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden" style="border-color:#2a2a2a">
419
- <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
420
- <h3 class="font-bold text-sm flex items-center" style="color:#f0ece6">Speed Distribution
421
- <span class="info-wrap"><span class="info-btn"><i
422
- class="fa-solid fa-info"></i></span>
423
- <span class="info-tip">Relative speed categories based on pixel displacement.
424
- Slow/Normal/Fast are percentile-based within this video.</span></span>
425
- </h3>
426
- </div>
427
- <div class="p-6" id="speed-dist-content">
428
- <div class="flex gap-4 items-end justify-center h-32" id="speed-bars"></div>
429
- </div>
430
- </div>
431
- <div
432
- class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden" style="border-color:#2a2a2a">
433
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
434
- <h3 class="font-bold text-sm flex items-center" style="color:#f0ece6">Congestion Insights
 
435
  <span class="info-wrap"><span class="info-btn"><i
436
  class="fa-solid fa-info"></i></span>
437
  <span class="info-tip">Automated peak detection and congestion
@@ -451,70 +435,16 @@
451
  <!-- TAB: Reports -->
452
  <div id="tab-reports" class="hidden flex-1 min-h-0 overflow-y-auto">
453
  <div id="reports-pending-message"
454
- class="mb-4 text-center p-4 border border-[#222] bg-[#0a0a0a] rounded-xl flex items-center justify-center gap-3 shadow-sm">
455
- <i id="reports-pending-icon" class="fa-solid fa-layer-group text-[#c89a6c]"></i>
456
  <span id="reports-pending-text"
457
- class="text-xs text-[#a89f97] font-medium tracking-wide uppercase">Trigger process from settings.
458
- Artifacts will be available here once processing completes.</span>
459
- </div>
460
- <div id="reports-pending" class="grid grid-cols-2 xl:grid-cols-3 gap-4 w-full">
461
- <!-- Skeleton Loader 1 -->
462
- <div
463
- class="bg-[#0a0a0a] rounded-xl border border-[#222] shadow-sm flex flex-col overflow-hidden animate-pulse">
464
- <div class="flex flex-col items-center justify-center py-12">
465
- <div class="w-16 h-16 bg-[#1a1a1a] rounded mb-4"></div>
466
- <div class="w-24 h-2 bg-[#1a1a1a] rounded"></div>
467
- </div>
468
- <div class="px-5 py-4 border-t border-[#1a1a1a] bg-[#050505]">
469
- <div class="w-3/4 h-3 bg-[#1a1a1a] rounded mb-2"></div>
470
- <div class="w-1/2 h-2 bg-[#111] rounded"></div>
471
- </div>
472
- </div>
473
- <!-- Skeleton Loader 2 -->
474
- <div class="bg-[#0a0a0a] rounded-xl border border-[#222] shadow-sm flex flex-col overflow-hidden animate-pulse"
475
- style="animation-delay: 150ms;">
476
- <div class="flex flex-col items-center justify-center py-12">
477
- <div class="w-16 h-16 bg-[#1a1a1a] rounded mb-4"></div>
478
- <div class="w-24 h-2 bg-[#1a1a1a] rounded"></div>
479
- </div>
480
- <div class="px-5 py-4 border-t border-[#1a1a1a] bg-[#050505]">
481
- <div class="w-2/3 h-3 bg-[#1a1a1a] rounded mb-2"></div>
482
- <div class="w-1/3 h-2 bg-[#111] rounded"></div>
483
- </div>
484
- </div>
485
- <!-- Skeleton Loader 3 -->
486
- <div class="bg-[#0a0a0a] rounded-xl border border-[#222] shadow-sm flex flex-col overflow-hidden animate-pulse"
487
- style="animation-delay: 300ms;">
488
- <div class="flex flex-col items-center justify-center py-12">
489
- <div class="w-16 h-16 bg-[#1a1a1a] rounded mb-4"></div>
490
- <div class="w-24 h-2 bg-[#1a1a1a] rounded"></div>
491
- </div>
492
- <div class="px-5 py-4 border-t border-[#1a1a1a] bg-[#050505]">
493
- <div class="w-4/5 h-3 bg-[#1a1a1a] rounded mb-2"></div>
494
- <div class="w-1/2 h-2 bg-[#111] rounded"></div>
495
- </div>
496
- </div>
497
  </div>
498
 
499
  <!-- Post-Processing Insights (Populated after completion) -->
500
- <div id="post-process-cards" class="hidden grid grid-cols-2 gap-4 mb-4">
501
- <!-- Speed Distribution -->
502
- <div class="bg-black rounded-xl p-6 border border-slate-800 shadow-sm flex flex-col min-h-[140px]">
503
- <div class="flex justify-between items-center mb-4 relative">
504
- <h3 class="font-bold text-white text-sm flex items-center">Speed Profile
505
- <span class="info-wrap"><span class="info-btn" style="background:#222;color:#888"><i
506
- class="fa-solid fa-info"></i></span>
507
- <span class="info-tip">Relative speed categories based on pixel displacement between
508
- frames. Slow/Normal/Fast are percentile-based within this video — not
509
- km/h.</span></span>
510
- </h3>
511
- </div>
512
- <div class="flex-1 flex items-center justify-center" id="speed-stats-card">
513
- <div class="text-center text-slate-600 text-xs"><i
514
- class="fa-solid fa-gauge-high text-2xl mb-2"></i><br>Available after processing</div>
515
- </div>
516
- </div>
517
-
518
  <!-- PCU Summary -->
519
  <div class="bg-black rounded-xl p-6 border border-slate-800 shadow-sm flex flex-col min-h-[140px]">
520
  <div class="flex justify-between items-center mb-4 relative">
@@ -539,104 +469,138 @@
539
  <div id="tab-settings" class="hidden flex-1 min-h-0 overflow-y-auto">
540
  <div class="grid grid-cols-2 gap-6 w-full">
541
  <!-- Processing Parameters -->
542
- <div class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col" style="border-color:#2a2a2a">
 
543
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
544
  <h3 class="font-bold text-white text-sm flex items-center">Inference Configuration Profile
545
  <span class="info-wrap ml-2">
546
- <span class="info-btn" style="background:#1a1a1a;color:#888"><i class="fa-solid fa-info"></i></span>
547
- <span class="info-tip">Figures and numbers below are auto-calculated for best performance.</span>
 
 
548
  </span>
549
  </h3>
550
- <p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">Auto-configured pipeline parameters</p>
 
551
  </div>
552
  <div class="px-6 py-2 flex-1" id="settings-params">
553
  <div class="s-row" data-param="imgsz">
554
  <div>
555
  <div class="text-xs font-semibold text-slate-300 flex items-center">Image Size
556
  <span class="info-wrap ml-1">
557
- <span class="info-btn" style="background:#1a1a1a;color:#888"><i class="fa-solid fa-info"></i></span>
558
- <span class="info-tip">Input resolution fed to the detection model. Locked to the value compiled into the OpenVINO graph.</span>
 
 
559
  </span>
560
  </div>
561
  <div class="text-[10px] text-slate-500">Model input resolution</div>
562
  </div>
563
- <div class="s-stepper"><button onclick="stepParam('imgsz',-32)">&#8249;</button><span class="s-val" id="sv-imgsz">640</span><button onclick="stepParam('imgsz',32)">&#8250;</button></div>
 
 
564
  </div>
565
  <div class="s-row" data-param="conf">
566
  <div>
567
  <div class="text-xs font-semibold text-slate-300 flex items-center">Confidence
568
  <span class="info-wrap ml-1">
569
- <span class="info-btn" style="background:#1a1a1a;color:#888"><i class="fa-solid fa-info"></i></span>
570
- <span class="info-tip">Minimum score a detection must reach to be counted. Lower values detect more but may include false positives.</span>
 
 
571
  </span>
572
  </div>
573
  <div class="text-[10px] text-slate-500">Minimum detection threshold</div>
574
  </div>
575
- <div class="s-stepper"><button onclick="stepParam('conf',-0.01)">&#8249;</button><span class="s-val" id="sv-conf">0.12</span><button onclick="stepParam('conf',0.01)">&#8250;</button></div>
 
 
576
  </div>
577
  <div class="s-row" data-param="iou">
578
  <div>
579
  <div class="text-xs font-semibold text-slate-300 flex items-center">IoU Threshold
580
  <span class="info-wrap ml-1">
581
- <span class="info-btn" style="background:#1a1a1a;color:#888"><i class="fa-solid fa-info"></i></span>
582
- <span class="info-tip">Controls how aggressively overlapping detections are merged. Higher values allow more overlap before suppression.</span>
 
 
583
  </span>
584
  </div>
585
  <div class="text-[10px] text-slate-500">Non-max suppression overlap</div>
586
  </div>
587
- <div class="s-stepper"><button onclick="stepParam('iou',-0.05)">&#8249;</button><span class="s-val" id="sv-iou">0.60</span><button onclick="stepParam('iou',0.05)">&#8250;</button></div>
 
 
588
  </div>
589
  <div class="s-row" data-param="stride">
590
  <div>
591
  <div class="text-xs font-semibold text-slate-300 flex items-center">Frame Stride
592
  <span class="info-wrap ml-1">
593
- <span class="info-btn" style="background:#1a1a1a;color:#888"><i class="fa-solid fa-info"></i></span>
594
- <span class="info-tip">Number of frames skipped between each detection pass. Higher values trade accuracy for speed.</span>
 
 
595
  </span>
596
  </div>
597
  <div class="text-[10px] text-slate-500">Frames skipped between detections</div>
598
  </div>
599
- <div class="s-stepper"><button onclick="stepParam('stride',-1)">&#8249;</button><span class="s-val" id="sv-stride">2</span><button onclick="stepParam('stride',1)">&#8250;</button></div>
 
 
600
  </div>
601
 
602
  <div class="s-row" data-param="smoothing">
603
  <div>
604
- <div class="text-xs font-semibold text-slate-300 flex items-center">
605
- Congestion Smoothing
606
- <span class="info-wrap ml-1">
607
- <span class="info-btn" style="background:#1a1a1a;color:#888"><i class="fa-solid fa-info"></i></span>
608
- <span class="info-tip">Reduces jitter/noise in the line chart. Low values (0.05-0.2) create very smooth trends; high values (0.8+) show raw spiky data.</span>
 
 
 
609
  </span>
610
  </div>
611
  <div class="text-[10px] text-slate-500">EMA Alpha factor for the rolling average</div>
612
  </div>
613
- <div class="s-stepper"><button onclick="stepParam('smoothing',-0.05)">&#8249;</button><span class="s-val" id="sv-smoothing">0.25</span><button onclick="stepParam('smoothing',0.05)">&#8250;</button></div>
 
 
614
  </div>
615
  </div>
616
  </div>
617
 
618
  <!-- Artifact Generation Settings -->
619
- <div class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col" style="border-color:#2a2a2a">
 
620
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
621
  <h3 class="font-bold text-white text-sm">Artifact Settings</h3>
622
- <p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">Configured visual overlay options</p>
 
623
  </div>
624
  <div class="px-6 py-4 flex-1 flex flex-col">
625
  <div class="s-row" data-param="report">
626
  <div>
627
- <div class="text-xs font-semibold text-slate-300">Individual Chart Type</div>
628
  <div class="text-[10px] text-slate-500">Format for separate KPI chart files</div>
629
  </div>
630
- <select class="custom-select" id="sv-report">
631
- <option value="png" selected>PNG Image</option>
632
- <option value="pdf">PDF Document</option>
633
- </select>
 
 
 
 
 
 
 
634
  </div>
635
- <div class="s-row flex-col items-stretch" data-param="annotated">
636
- <div class="flex items-center justify-between w-full">
637
- <div>
638
- <div class="text-xs font-semibold text-slate-300">Export Annotated Video</div>
639
- <div class="text-[10px] text-slate-500">Layered visual overlays for diagnostic analysis</div>
640
  </div>
641
  <div class="toggle-track" id="sv-annotated" onclick="toggleExportMaster(this)">
642
  <div class="toggle-thumb"></div>
@@ -663,10 +627,12 @@
663
  </div>
664
  <div class="s-row">
665
  <div>
666
- <div class="text-xs font-semibold text-slate-300 flex items-center">Export Run Details (JSON)
667
  <span class="info-wrap ml-1">
668
- <span class="info-btn" style="background:#1a1a1a;color:#888"><i class="fa-solid fa-info"></i></span>
669
- <span class="info-tip">Include analysis.json payload with all run parameters and data structures.</span>
 
 
670
  </span>
671
  </div>
672
  <div class="text-[10px] text-slate-500">Structurally represented technical details</div>
@@ -679,7 +645,8 @@
679
  <div>
680
  <div class="text-xs font-semibold text-slate-300 flex items-center">Export CSV Results
681
  <span class="info-wrap ml-1">
682
- <span class="info-btn" style="background:#1a1a1a;color:#888"><i class="fa-solid fa-info"></i></span>
 
683
  <span class="info-tip">Include frame-by-frame data in raw CSV format.</span>
684
  </span>
685
  </div>
@@ -693,8 +660,11 @@
693
  <div>
694
  <div class="text-xs font-semibold text-slate-300 flex items-center">Auto-Download Report
695
  <span class="info-wrap ml-1">
696
- <span class="info-btn" style="background:#1a1a1a;color:#888"><i class="fa-solid fa-info"></i></span>
697
- <span class="info-tip">Enable before processing starts. The artifact bundle (ZIP) will download automatically once analysis completes. Cannot be changed after processing finishes.</span>
 
 
 
698
  </span>
699
  </div>
700
  <div class="text-[10px] text-slate-500">Save reports automatically</div>
@@ -708,7 +678,8 @@
708
  <div class="text-xs font-semibold text-slate-300">Interface Mode</div>
709
  <div class="text-[10px] text-slate-500">Locked to Professional Dark</div>
710
  </div>
711
- <div class="text-xs font-bold text-white px-3 py-1 bg-slate-800 rounded-full">Dark Mode Only</div>
 
712
  </div>
713
  </div>
714
  </div>
@@ -737,10 +708,12 @@
737
  <div id="tab-help" class="hidden flex-1 min-h-0 overflow-y-auto">
738
  <div class="space-y-6 w-full">
739
  <div class="bg-black border rounded-xl overflow-hidden shadow-sm" style="border-color:#2a2a2a">
740
- <div class="px-6 py-4 border-b flex justify-between items-center" style="border-color:#1a1a1a;background:#050505">
 
741
  <div>
742
  <h3 class="font-bold text-white text-sm flex items-center">Navigator</h3>
743
- <p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">Quick
 
744
  assistance and platform documentation</p>
745
  </div>
746
  </div>
@@ -800,10 +773,12 @@
800
  to the upload interface without requiring a full browser refresh.
801
  </div>
802
  </div>
803
- <div class="bg-[#050505] border border-slate-900 rounded-lg overflow-hidden transition hover:border-[#1a1a1a]">
 
804
  <button onclick="toggleHelp('h4')"
805
  class="w-full px-6 py-5 text-left flex items-center justify-between hover:bg-[#080808] transition">
806
- <span class="text-sm font-semibold" style="color:#a89f97">How accurate are UrbanFlow's
 
807
  traffic calculations?</span>
808
  <i id="h4-icon" class="fa-solid fa-plus text-xs" style="color:#555"></i>
809
  </button>
@@ -816,10 +791,12 @@
816
  human-level perception.
817
  </div>
818
  </div>
819
- <div class="bg-[#050505] border border-slate-900 rounded-lg overflow-hidden transition hover:border-[#1a1a1a]">
 
820
  <button onclick="toggleHelp('h5')"
821
  class="w-full px-6 py-5 text-left flex items-center justify-between hover:bg-[#080808] transition">
822
- <span class="text-sm font-semibold" style="color:#a89f97">What is PCU analysis and
 
823
  which speed metrics are calculated?</span>
824
  <i id="h5-icon" class="fa-solid fa-plus text-xs" style="color:#555"></i>
825
  </button>
@@ -957,16 +934,18 @@
957
 
958
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
959
  <!-- Left Side: Experience & Requirements (All Selective Choices) -->
960
- <div
961
- class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col" style="border-color:#2a2a2a">
962
  <div class="px-6 py-4 border-b bg-[#050505]" style="border-color:#1a1a1a">
963
  <h3 class="font-bold text-white text-sm flex items-center">Experience & Priorities</h3>
964
- <p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">Rate your
 
965
  experience</p>
966
  </div>
967
  <div class="p-6 flex-1 space-y-8">
968
  <!-- Overall Experience Centered -->
969
- <div class="flex flex-col items-center justify-center border-b pb-6" style="border-color:#1a1a1a">
 
970
  <label class="text-[10px] font-bold uppercase tracking-widest block mb-3 text-center"
971
  style="color:#a89f97">Overall Experience</label>
972
  <div class="fb-stars" id="fb-stars">
@@ -986,7 +965,8 @@
986
  <div class="flex gap-2" id="fb-recommend">
987
  <div class="fb-emoji-btn" onclick="setEmoji(this, 'fb-recommend', 'Unlikely')">
988
  <i class="fa-solid fa-face-frown text-xl"></i><span
989
- class="block mt-1 text-[8px] uppercase">Unlikely</span></div>
 
990
  <div class="fb-emoji-btn" onclick="setEmoji(this, 'fb-recommend', 'Maybe')"><i
991
  class="fa-solid fa-face-meh text-xl"></i><span
992
  class="block mt-1 text-[8px] uppercase">Maybe</span></div>
@@ -1094,11 +1074,12 @@
1094
  </div>
1095
 
1096
  <!-- Right Side: Categorization & Feedback Text -->
1097
- <div
1098
- class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col" style="border-color:#2a2a2a">
1099
  <div class="px-6 py-4 border-b bg-[#050505]" style="border-color:#1a1a1a">
1100
  <h3 class="font-bold text-white text-sm flex items-center">Categorization & Feedback</h3>
1101
- <p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">Detailed
 
1102
  workflow insights</p>
1103
  </div>
1104
  <div class="p-6 flex-1 flex flex-col space-y-6">
@@ -1106,25 +1087,37 @@
1106
  <div>
1107
  <label class="text-[10px] font-bold uppercase tracking-widest block mb-3"
1108
  style="color:#a89f97">Primary Use Case</label>
1109
- <select class="fb-select w-full" id="fb-usecase">
1110
- <option value="" disabled selected>Select your use case</option>
1111
- <option value="research">Academic Research</option>
1112
- <option value="planning">Urban Planning</option>
1113
- <option value="highway">Business Modelling</option>
1114
- <option value="smartcity">Smart City Investment</option>
1115
- <option value="other">Other...</option>
1116
- </select>
 
 
 
 
 
 
1117
  </div>
1118
  <div>
1119
  <label class="text-[10px] font-bold uppercase tracking-widest block mb-3"
1120
  style="color:#a89f97">Feedback Category</label>
1121
- <select class="fb-select w-full" id="fb-type">
1122
- <option value="" disabled selected>General Feedback</option>
1123
- <option value="bug">Technical Issue / Bug Report</option>
1124
- <option value="feature">Feature Request</option>
1125
- <option value="accuracy">Inference Accuracy Review</option>
1126
- <option value="ux">UI/ UX</option>
1127
- </select>
 
 
 
 
 
 
1128
  </div>
1129
  </div>
1130
 
@@ -1302,12 +1295,67 @@
1302
  const el = document.getElementById('appModal-' + id);
1303
  if (el) { el.style.display = 'none'; document.body.style.overflow = ''; }
1304
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1305
  document.addEventListener('keydown', function (e) {
1306
- if (e.key === 'Escape') { closeAppModal('privacyModal'); closeAppModal('termsModal'); closeAppModal('shortcutsModal'); }
 
 
 
 
 
 
1307
  });
1308
 
1309
- // Auto-show keyboard shortcuts on every page visit
1310
- setTimeout(function () { openAppModal('shortcutsModal'); }, 800);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1311
  </script>
1312
  </body>
1313
 
 
3
 
4
  <head>
5
  <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
7
  <title>UrbanFlow</title>
8
  <link rel="icon" type="image/png" href="assets/rf.png">
9
+ <link rel="manifest" href="manifest.json">
10
+ <meta name="theme-color" content="#000000">
11
+ <meta name="apple-mobile-web-app-capable" content="yes">
12
+ <meta name="apple-mobile-web-app-status-bar-style" content="black">
13
+ <meta name="apple-mobile-web-app-title" content="UrbanFlow">
14
+ <link rel="apple-touch-icon" href="assets/icon-192.png">
15
  <script src="https://cdn.tailwindcss.com"></script>
16
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
17
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 
22
  <link rel="stylesheet" href="css/vehicles.css">
23
  </head>
24
 
25
+ <body class="bg-black text-white h-screen w-screen flex" style="overflow:hidden">
26
+ <!-- Fixed portal: bubbles, toasts, overlays append here — never clipped -->
27
+ <div id="fixed-portal" style="position:fixed;inset:0;pointer-events:none;z-index:8999"></div>
28
+ <div class="mobile-top-bar" id="mobile-top-bar">
29
+ <img src="assets/uf_rf.png" alt="UrbanFlow" class="h-14 w-auto object-contain">
30
+
31
+ <!-- Mobile Legal Overflow -->
32
+ <div class="absolute right-4 top-1/2 -translate-y-1/2 flex items-center">
33
+ <button onclick="toggleLegalMenu(event)" class="text-[#a89f97] hover:text-white p-2 transition-colors" aria-label="Legal Menu">
34
+ <i class="fa-solid fa-ellipsis-vertical text-lg"></i>
35
+ </button>
36
+ <div id="legal-menu" class="hidden absolute right-0 top-full mt-2 w-48 bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg shadow-2xl z-50 overflow-hidden">
37
+ <button onclick="openAppModal('privacyModal'); toggleLegalMenu(event)" class="w-full text-left px-4 py-3.5 text-[10px] font-bold uppercase tracking-widest text-[#a89f97] hover:text-white hover:bg-[#111] border-b border-[#1a1a1a] transition-all">
38
+ Privacy Policy
39
+ </button>
40
+ <button onclick="openAppModal('termsModal'); toggleLegalMenu(event)" class="w-full text-left px-4 py-3.5 text-[10px] font-bold uppercase tracking-widest text-[#a89f97] hover:text-white hover:bg-[#111] transition-all">
41
+ Terms & Conditions
42
+ </button>
43
+ </div>
44
+ </div>
45
+ </div>
46
 
47
  <!-- Sidebar -->
48
  <aside class="w-60 bg-white shadow-xl flex flex-col z-20 flex-shrink-0 border-r border-slate-200 relative">
 
122
 
123
  <!-- TAB: About -->
124
  <div id="tab-about" class="hidden flex-1 min-h-0 overflow-y-auto">
125
+ <div class="bg-black border rounded-xl p-12 shadow-2xl space-y-8 flex flex-col justify-center"
126
+ style="border-color:#2a2a2a">
127
  <div class="text-center">
128
  <p class="text-sm italic font-bold leading-relaxed max-w-3xl mx-auto"
129
  style="color:#c89a6c;font-family:'Montserrat',sans-serif">
 
158
  </p>
159
  </div>
160
 
161
+ <div class="grid grid-cols-2 gap-12 pt-8 text-left border-t max-w-5xl mx-auto"
162
+ style="border-color:#1a1a1a">
163
  <div class="space-y-4">
164
  <h4 class="font-bold text-[13px] uppercase tracking-wider" style="color:#f0ece6">What You Get
165
  </h4>
 
183
  </ul>
184
  </div>
185
  <div class="space-y-4">
186
+ <h4 class="font-bold text-[13px] uppercase tracking-wider" style="color:#f0ece6">Precision &
187
+ Accuracy
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  </h4>
189
+ <p class="text-xs leading-relaxed" style="color:#a89f97">
190
+ UrbanFlow provides an estimated accuracy of ±5–8% on dense mixed-traffic footage. Results
191
+ may vary slightly across runs due to the nature of real-time frame-by-frame inference.
192
+ </p>
193
+ <p class="text-xs leading-relaxed" style="color:#a89f97">
194
+ For research or planning use, we recommend processing the same video 2–3 times and taking
195
+ the average count for maximum reliability.
196
+ </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  </div>
198
  </div>
199
 
 
214
 
215
 
216
  <!-- Progress Bar (shared) -->
217
+ <div id="progress-bar-wrapper"
218
+ class="w-full bg-white rounded-xl px-6 py-4 border border-slate-200 shadow-sm flex items-center justify-between flex-shrink-0">
219
  <div class="flex items-center space-x-4 flex-1 mr-6">
220
  <span class="text-[11px] font-black text-white uppercase tracking-wider whitespace-nowrap"
221
  id="proc-label">Waiting</span>
 
227
  <div class="flex items-center space-x-6 text-xs font-bold text-white whitespace-nowrap">
228
  <span id="proc-frames">0 / 0 Frames</span>
229
  <span id="proc-pct">0%</span>
230
+ <div class="uf-select-wrap" id="live-palette-wrap">
231
+ <div class="uf-select-trigger" id="live-palette-trigger" onclick="ufSelectToggle('live-palette')">
232
+ <span class="uf-select-label" id="live-palette-label">Default</span>
233
+ <i class="fa-solid fa-chevron-down uf-select-arrow" id="live-palette-arrow"></i>
234
+ </div>
235
+ <div class="uf-select-dropdown hidden" id="live-palette-dropdown">
236
+ <div class="uf-select-option" data-value="default" onclick="ufSelectPick('live-palette','default','Default')">Default</div>
237
+ <div class="uf-select-option" data-value="vibrant" onclick="ufSelectPick('live-palette','vibrant','Vibrant')">Vibrant</div>
238
+ <div class="uf-select-option" data-value="corporate" onclick="ufSelectPick('live-palette','corporate','Corporate')">Corporate</div>
239
+ <div class="uf-select-option" data-value="neon" onclick="ufSelectPick('live-palette','neon','Neon Night')">Neon Night</div>
240
+ <div class="uf-select-option" data-value="earth" onclick="ufSelectPick('live-palette','earth','Earth Tones')">Earth Tones</div>
241
+ <div class="uf-select-option" data-value="ocean" onclick="ufSelectPick('live-palette','ocean','Ocean Breeze')">Ocean Breeze</div>
242
+ <div class="uf-select-option" data-value="sunset" onclick="ufSelectPick('live-palette','sunset','Sunset Glow')">Sunset Glow</div>
243
+ <div class="uf-select-option" data-value="midnight" onclick="ufSelectPick('live-palette','midnight','Midnight Deep')">Midnight Deep</div>
244
+ <div class="uf-select-option" data-value="gold" onclick="ufSelectPick('live-palette','gold','Monochrome Gold')">Monochrome Gold</div>
245
+ </div>
246
+ <input type="hidden" id="live-palette-select" value="default">
247
+ </div>
248
  </div>
249
  </div>
250
 
 
319
  <span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
320
  <span class="info-tip">Aggregated vehicle count grouped by dominant category.</span></span>
321
  </h3>
322
+ <span
323
+ class="text-[10px] text-slate-500 font-bold uppercase tracking-wider whitespace-nowrap">Dominant
324
  Intelligence</span>
325
  </div>
326
  <div class="flex-1 w-full relative min-h-[220px]">
 
353
  <div class="space-y-6 w-full max-w-[1400px] mx-auto">
354
 
355
  <!-- HERO: Process Analytics -->
356
+ <div id="run-results-card" class="bg-white rounded-xl border shadow-sm overflow-hidden flex flex-col"
357
+ style="border-color:#2a2a2a">
358
+ <div class="px-6 py-4 border-b flex flex-col lg:flex-row justify-between items-center gap-4"
359
+ style="border-color:#1a1a1a;background:#050505">
360
+ <div class="text-center lg:text-left">
361
  <h3 class="font-bold text-sm" style="color:#f0ece6">Process Analytics</h3>
362
+ <p class="text-[10px] mt-0.5 uppercase tracking-widest font-medium"
363
+ style="color:#a89f97">
364
  Execution Telemetry</p>
365
  </div>
366
  <div class="flex items-center gap-3">
 
 
 
 
 
 
 
367
  </div>
368
  </div>
369
  <div class="p-8">
370
  <div id="run-results-content" class="grid grid-cols-3 gap-12">
371
+ <div class="flex flex-col items-center justify-center p-12 rounded-2xl col-span-3"
372
+ style="color:#555">
373
+ <span class="text-[11px] font-bold uppercase tracking-[0.2em]">Initiate a run to view performance insights</span>
 
374
  </div>
375
  </div>
376
  </div>
 
378
 
379
  <!-- Technical Context Row -->
380
  <div class="grid grid-cols-2 gap-6">
381
+ <div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
382
+ style="border-color:#2a2a2a">
383
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
384
  <h3 class="font-bold text-sm" style="color:#f0ece6">Stream Source Profile</h3>
385
  </div>
386
  <div class="p-6 space-y-4" id="panel-video"></div>
387
  </div>
388
+ <div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
389
+ style="border-color:#2a2a2a">
390
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
391
  <h3 class="font-bold text-sm" style="color:#f0ece6">System Resource Utilization</h3>
392
  </div>
393
  <div class="p-6 space-y-4" id="panel-perf"></div>
394
  </div>
395
+ <div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
396
+ style="border-color:#2a2a2a">
397
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
398
  <h3 class="font-bold text-sm" style="color:#f0ece6">Model Architecture &amp; Logic</h3>
399
  </div>
400
  <div class="p-6 space-y-4" id="panel-model"></div>
401
  </div>
402
+ <div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
403
+ style="border-color:#2a2a2a">
404
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
405
  <h3 class="font-bold text-sm" style="color:#f0ece6">Inference Parameters</h3>
406
  </div>
 
410
 
411
  <!-- Insights Panel (xAI) -->
412
  <div id="insights-panel" class="hidden">
413
+ <div class="mt-6">
414
+ <div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
415
+ style="border-color:#2a2a2a">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
417
+ <h3 class="font-bold text-sm flex items-center" style="color:#f0ece6">Congestion
418
+ Insights
419
  <span class="info-wrap"><span class="info-btn"><i
420
  class="fa-solid fa-info"></i></span>
421
  <span class="info-tip">Automated peak detection and congestion
 
435
  <!-- TAB: Reports -->
436
  <div id="tab-reports" class="hidden flex-1 min-h-0 overflow-y-auto">
437
  <div id="reports-pending-message"
438
+ class="mb-4 text-center p-8 border border-[#222] bg-[#0a0a0a] rounded-xl flex flex-col items-center justify-center gap-4 shadow-sm min-h-[200px]">
439
+ <i id="reports-pending-icon" class="fa-solid fa-layer-group text-3xl text-[#c89a6c] opacity-50"></i>
440
  <span id="reports-pending-text"
441
+ class="text-xs text-[#a89f97] font-medium tracking-wide uppercase leading-relaxed max-w-[240px]">
442
+ Please return once processing is 100% complete to access artifacts.
443
+ </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  </div>
445
 
446
  <!-- Post-Processing Insights (Populated after completion) -->
447
+ <div id="post-process-cards" class="hidden mb-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  <!-- PCU Summary -->
449
  <div class="bg-black rounded-xl p-6 border border-slate-800 shadow-sm flex flex-col min-h-[140px]">
450
  <div class="flex justify-between items-center mb-4 relative">
 
469
  <div id="tab-settings" class="hidden flex-1 min-h-0 overflow-y-auto">
470
  <div class="grid grid-cols-2 gap-6 w-full">
471
  <!-- Processing Parameters -->
472
+ <div class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col"
473
+ style="border-color:#2a2a2a">
474
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
475
  <h3 class="font-bold text-white text-sm flex items-center">Inference Configuration Profile
476
  <span class="info-wrap ml-2">
477
+ <span class="info-btn" style="background:#1a1a1a;color:#888"><i
478
+ class="fa-solid fa-info"></i></span>
479
+ <span class="info-tip">Figures and numbers below are auto-calculated for best
480
+ performance.</span>
481
  </span>
482
  </h3>
483
+ <p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
484
+ Auto-configured pipeline parameters</p>
485
  </div>
486
  <div class="px-6 py-2 flex-1" id="settings-params">
487
  <div class="s-row" data-param="imgsz">
488
  <div>
489
  <div class="text-xs font-semibold text-slate-300 flex items-center">Image Size
490
  <span class="info-wrap ml-1">
491
+ <span class="info-btn" style="background:#1a1a1a;color:#888"><i
492
+ class="fa-solid fa-info"></i></span>
493
+ <span class="info-tip">Input resolution fed to the detection model. Optimized
494
+ for the resolution compiled into the OpenVINO weights.</span>
495
  </span>
496
  </div>
497
  <div class="text-[10px] text-slate-500">Model input resolution</div>
498
  </div>
499
+ <div class="s-stepper"><button onclick="stepParam('imgsz',-32)">&#8249;</button><span
500
+ class="s-val" id="sv-imgsz">640</span><button
501
+ onclick="stepParam('imgsz',32)">&#8250;</button></div>
502
  </div>
503
  <div class="s-row" data-param="conf">
504
  <div>
505
  <div class="text-xs font-semibold text-slate-300 flex items-center">Confidence
506
  <span class="info-wrap ml-1">
507
+ <span class="info-btn" style="background:#1a1a1a;color:#888"><i
508
+ class="fa-solid fa-info"></i></span>
509
+ <span class="info-tip">Minimum score a detection must reach to be counted. Lower
510
+ values detect more but may include false positives.</span>
511
  </span>
512
  </div>
513
  <div class="text-[10px] text-slate-500">Minimum detection threshold</div>
514
  </div>
515
+ <div class="s-stepper"><button onclick="stepParam('conf',-0.01)">&#8249;</button><span
516
+ class="s-val" id="sv-conf">0.12</span><button
517
+ onclick="stepParam('conf',0.01)">&#8250;</button></div>
518
  </div>
519
  <div class="s-row" data-param="iou">
520
  <div>
521
  <div class="text-xs font-semibold text-slate-300 flex items-center">IoU Threshold
522
  <span class="info-wrap ml-1">
523
+ <span class="info-btn" style="background:#1a1a1a;color:#888"><i
524
+ class="fa-solid fa-info"></i></span>
525
+ <span class="info-tip">Controls how aggressively overlapping detections are
526
+ merged. Higher values allow more overlap before suppression.</span>
527
  </span>
528
  </div>
529
  <div class="text-[10px] text-slate-500">Non-max suppression overlap</div>
530
  </div>
531
+ <div class="s-stepper"><button onclick="stepParam('iou',-0.05)">&#8249;</button><span
532
+ class="s-val" id="sv-iou">0.60</span><button
533
+ onclick="stepParam('iou',0.05)">&#8250;</button></div>
534
  </div>
535
  <div class="s-row" data-param="stride">
536
  <div>
537
  <div class="text-xs font-semibold text-slate-300 flex items-center">Frame Stride
538
  <span class="info-wrap ml-1">
539
+ <span class="info-btn" style="background:#1a1a1a;color:#888"><i
540
+ class="fa-solid fa-info"></i></span>
541
+ <span class="info-tip">Number of frames skipped between each detection pass.
542
+ Higher values trade accuracy for speed.</span>
543
  </span>
544
  </div>
545
  <div class="text-[10px] text-slate-500">Frames skipped between detections</div>
546
  </div>
547
+ <div class="s-stepper"><button onclick="stepParam('stride',-1)">&#8249;</button><span
548
+ class="s-val" id="sv-stride">2</span><button
549
+ onclick="stepParam('stride',1)">&#8250;</button></div>
550
  </div>
551
 
552
  <div class="s-row" data-param="smoothing">
553
  <div>
554
+ <div class="text-xs font-semibold text-slate-300 flex items-center gap-1">
555
+ <span>Congestion Smoothing</span>
556
+ <span class="info-wrap">
557
+ <span class="info-btn" style="background:#1a1a1a;color:#888"><i
558
+ class="fa-solid fa-info"></i></span>
559
+ <span class="info-tip">Reduces jitter/noise in the line chart. Low values
560
+ (0.05-0.2) create very smooth trends; high values (0.8+) show raw spiky
561
+ data.</span>
562
  </span>
563
  </div>
564
  <div class="text-[10px] text-slate-500">EMA Alpha factor for the rolling average</div>
565
  </div>
566
+ <div class="s-stepper"><button onclick="stepParam('smoothing',-0.05)">&#8249;</button><span
567
+ class="s-val" id="sv-smoothing">0.25</span><button
568
+ onclick="stepParam('smoothing',0.05)">&#8250;</button></div>
569
  </div>
570
  </div>
571
  </div>
572
 
573
  <!-- Artifact Generation Settings -->
574
+ <div class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col"
575
+ style="border-color:#2a2a2a">
576
  <div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
577
  <h3 class="font-bold text-white text-sm">Artifact Settings</h3>
578
+ <p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
579
+ Configured visual overlay options</p>
580
  </div>
581
  <div class="px-6 py-4 flex-1 flex flex-col">
582
  <div class="s-row" data-param="report">
583
  <div>
584
+ <div class="text-xs font-semibold text-slate-300">Chart Format</div>
585
  <div class="text-[10px] text-slate-500">Format for separate KPI chart files</div>
586
  </div>
587
+ <div class="uf-select-wrap" id="sv-report-wrap">
588
+ <div class="uf-select-trigger" id="sv-report-trigger" onclick="ufSelectToggle('sv-report')">
589
+ <span class="uf-select-label" id="sv-report-label">PNG Image</span>
590
+ <i class="fa-solid fa-chevron-down uf-select-arrow" id="sv-report-arrow"></i>
591
+ </div>
592
+ <div class="uf-select-dropdown hidden" id="sv-report-dropdown">
593
+ <div class="uf-select-option uf-select-option-active" data-value="png" onclick="ufSelectPick('sv-report','png','PNG Image')">PNG Image</div>
594
+ <div class="uf-select-option" data-value="pdf" onclick="ufSelectPick('sv-report','pdf','PDF Document')">PDF Document</div>
595
+ </div>
596
+ <input type="hidden" id="sv-report" value="png">
597
+ </div>
598
  </div>
599
+ <div class="s-row flex-col items-stretch !gap-0" data-param="annotated">
600
+ <div class="flex items-center justify-between w-full py-2">
601
+ <div class="flex-1 pr-2">
602
+ <div class="text-xs font-semibold text-slate-300">Export Annotated Video</div>
603
+ <div class="text-[10px] text-slate-500">Diagnostic visual overlays</div>
604
  </div>
605
  <div class="toggle-track" id="sv-annotated" onclick="toggleExportMaster(this)">
606
  <div class="toggle-thumb"></div>
 
627
  </div>
628
  <div class="s-row">
629
  <div>
630
+ <div class="text-xs font-semibold text-slate-300 flex items-center">Export Run Details
631
  <span class="info-wrap ml-1">
632
+ <span class="info-btn" style="background:#1a1a1a;color:#888"><i
633
+ class="fa-solid fa-info"></i></span>
634
+ <span class="info-tip">Include analysis.json payload with all run parameters and
635
+ data structures.</span>
636
  </span>
637
  </div>
638
  <div class="text-[10px] text-slate-500">Structurally represented technical details</div>
 
645
  <div>
646
  <div class="text-xs font-semibold text-slate-300 flex items-center">Export CSV Results
647
  <span class="info-wrap ml-1">
648
+ <span class="info-btn" style="background:#1a1a1a;color:#888"><i
649
+ class="fa-solid fa-info"></i></span>
650
  <span class="info-tip">Include frame-by-frame data in raw CSV format.</span>
651
  </span>
652
  </div>
 
660
  <div>
661
  <div class="text-xs font-semibold text-slate-300 flex items-center">Auto-Download Report
662
  <span class="info-wrap ml-1">
663
+ <span class="info-btn" style="background:#1a1a1a;color:#888"><i
664
+ class="fa-solid fa-info"></i></span>
665
+ <span class="info-tip">Enable before processing starts. The artifact bundle
666
+ (ZIP) will download automatically once analysis completes. Cannot be changed
667
+ after processing finishes.</span>
668
  </span>
669
  </div>
670
  <div class="text-[10px] text-slate-500">Save reports automatically</div>
 
678
  <div class="text-xs font-semibold text-slate-300">Interface Mode</div>
679
  <div class="text-[10px] text-slate-500">Locked to Professional Dark</div>
680
  </div>
681
+ <div class="text-xs font-bold text-white px-3 py-1 bg-slate-800 rounded-full">Dark Mode Only
682
+ </div>
683
  </div>
684
  </div>
685
  </div>
 
708
  <div id="tab-help" class="hidden flex-1 min-h-0 overflow-y-auto">
709
  <div class="space-y-6 w-full">
710
  <div class="bg-black border rounded-xl overflow-hidden shadow-sm" style="border-color:#2a2a2a">
711
+ <div class="px-6 py-4 border-b flex justify-between items-center"
712
+ style="border-color:#1a1a1a;background:#050505">
713
  <div>
714
  <h3 class="font-bold text-white text-sm flex items-center">Navigator</h3>
715
+ <p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
716
+ Quick
717
  assistance and platform documentation</p>
718
  </div>
719
  </div>
 
773
  to the upload interface without requiring a full browser refresh.
774
  </div>
775
  </div>
776
+ <div
777
+ class="bg-[#050505] border border-slate-900 rounded-lg overflow-hidden transition hover:border-[#1a1a1a]">
778
  <button onclick="toggleHelp('h4')"
779
  class="w-full px-6 py-5 text-left flex items-center justify-between hover:bg-[#080808] transition">
780
+ <span class="text-sm font-semibold" style="color:#a89f97">How accurate are
781
+ UrbanFlow's
782
  traffic calculations?</span>
783
  <i id="h4-icon" class="fa-solid fa-plus text-xs" style="color:#555"></i>
784
  </button>
 
791
  human-level perception.
792
  </div>
793
  </div>
794
+ <div
795
+ class="bg-[#050505] border border-slate-900 rounded-lg overflow-hidden transition hover:border-[#1a1a1a]">
796
  <button onclick="toggleHelp('h5')"
797
  class="w-full px-6 py-5 text-left flex items-center justify-between hover:bg-[#080808] transition">
798
+ <span class="text-sm font-semibold" style="color:#a89f97">What is PCU analysis
799
+ and
800
  which speed metrics are calculated?</span>
801
  <i id="h5-icon" class="fa-solid fa-plus text-xs" style="color:#555"></i>
802
  </button>
 
934
 
935
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
936
  <!-- Left Side: Experience & Requirements (All Selective Choices) -->
937
+ <div class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col"
938
+ style="border-color:#2a2a2a">
939
  <div class="px-6 py-4 border-b bg-[#050505]" style="border-color:#1a1a1a">
940
  <h3 class="font-bold text-white text-sm flex items-center">Experience & Priorities</h3>
941
+ <p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
942
+ Rate your
943
  experience</p>
944
  </div>
945
  <div class="p-6 flex-1 space-y-8">
946
  <!-- Overall Experience Centered -->
947
+ <div class="flex flex-col items-center justify-center border-b pb-6"
948
+ style="border-color:#1a1a1a">
949
  <label class="text-[10px] font-bold uppercase tracking-widest block mb-3 text-center"
950
  style="color:#a89f97">Overall Experience</label>
951
  <div class="fb-stars" id="fb-stars">
 
965
  <div class="flex gap-2" id="fb-recommend">
966
  <div class="fb-emoji-btn" onclick="setEmoji(this, 'fb-recommend', 'Unlikely')">
967
  <i class="fa-solid fa-face-frown text-xl"></i><span
968
+ class="block mt-1 text-[8px] uppercase">Unlikely</span>
969
+ </div>
970
  <div class="fb-emoji-btn" onclick="setEmoji(this, 'fb-recommend', 'Maybe')"><i
971
  class="fa-solid fa-face-meh text-xl"></i><span
972
  class="block mt-1 text-[8px] uppercase">Maybe</span></div>
 
1074
  </div>
1075
 
1076
  <!-- Right Side: Categorization & Feedback Text -->
1077
+ <div class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col"
1078
+ style="border-color:#2a2a2a">
1079
  <div class="px-6 py-4 border-b bg-[#050505]" style="border-color:#1a1a1a">
1080
  <h3 class="font-bold text-white text-sm flex items-center">Categorization & Feedback</h3>
1081
+ <p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
1082
+ Detailed
1083
  workflow insights</p>
1084
  </div>
1085
  <div class="p-6 flex-1 flex flex-col space-y-6">
 
1087
  <div>
1088
  <label class="text-[10px] font-bold uppercase tracking-widest block mb-3"
1089
  style="color:#a89f97">Primary Use Case</label>
1090
+ <div class="uf-select-wrap w-full" id="fb-usecase-wrap">
1091
+ <div class="uf-select-trigger" id="fb-usecase-trigger" onclick="ufSelectToggle('fb-usecase')">
1092
+ <span class="uf-select-label" id="fb-usecase-label" style="color:#666">Select your use case</span>
1093
+ <i class="fa-solid fa-chevron-down uf-select-arrow" id="fb-usecase-arrow"></i>
1094
+ </div>
1095
+ <div class="uf-select-dropdown hidden" id="fb-usecase-dropdown">
1096
+ <div class="uf-select-option" data-value="research" onclick="ufSelectPick('fb-usecase','research','Academic Research')">Academic Research</div>
1097
+ <div class="uf-select-option" data-value="planning" onclick="ufSelectPick('fb-usecase','planning','Urban Planning')">Urban Planning</div>
1098
+ <div class="uf-select-option" data-value="highway" onclick="ufSelectPick('fb-usecase','highway','Business Modelling')">Business Modelling</div>
1099
+ <div class="uf-select-option" data-value="smartcity" onclick="ufSelectPick('fb-usecase','smartcity','Smart City Investment')">Smart City Investment</div>
1100
+ <div class="uf-select-option" data-value="other" onclick="ufSelectPick('fb-usecase','other','Other...')">Other...</div>
1101
+ </div>
1102
+ <input type="hidden" id="fb-usecase" value="">
1103
+ </div>
1104
  </div>
1105
  <div>
1106
  <label class="text-[10px] font-bold uppercase tracking-widest block mb-3"
1107
  style="color:#a89f97">Feedback Category</label>
1108
+ <div class="uf-select-wrap w-full" id="fb-type-wrap">
1109
+ <div class="uf-select-trigger" id="fb-type-trigger" onclick="ufSelectToggle('fb-type')">
1110
+ <span class="uf-select-label" id="fb-type-label" style="color:#666">General Feedback</span>
1111
+ <i class="fa-solid fa-chevron-down uf-select-arrow" id="fb-type-arrow"></i>
1112
+ </div>
1113
+ <div class="uf-select-dropdown hidden" id="fb-type-dropdown">
1114
+ <div class="uf-select-option" data-value="bug" onclick="ufSelectPick('fb-type','bug','Technical Issue / Bug Report')">Technical Issue / Bug Report</div>
1115
+ <div class="uf-select-option" data-value="feature" onclick="ufSelectPick('fb-type','feature','Feature Request')">Feature Request</div>
1116
+ <div class="uf-select-option" data-value="accuracy" onclick="ufSelectPick('fb-type','accuracy','Inference Accuracy Review')">Inference Accuracy Review</div>
1117
+ <div class="uf-select-option" data-value="ux" onclick="ufSelectPick('fb-type','ux','UI / UX')">UI / UX</div>
1118
+ </div>
1119
+ <input type="hidden" id="fb-type" value="">
1120
+ </div>
1121
  </div>
1122
  </div>
1123
 
 
1295
  const el = document.getElementById('appModal-' + id);
1296
  if (el) { el.style.display = 'none'; document.body.style.overflow = ''; }
1297
  }
1298
+
1299
+ // Legal Menu Toggle
1300
+ function toggleLegalMenu(e) {
1301
+ if (e) e.stopPropagation();
1302
+ const menu = document.getElementById('legal-menu');
1303
+ if (menu) menu.classList.toggle('hidden');
1304
+ }
1305
+
1306
+ document.addEventListener('click', function(e) {
1307
+ const menu = document.getElementById('legal-menu');
1308
+ if (menu && !menu.classList.contains('hidden')) {
1309
+ if (!e.target.closest('.mobile-top-bar')) {
1310
+ menu.classList.add('hidden');
1311
+ }
1312
+ }
1313
+ });
1314
+
1315
  document.addEventListener('keydown', function (e) {
1316
+ if (e.key === 'Escape') {
1317
+ closeAppModal('privacyModal');
1318
+ closeAppModal('termsModal');
1319
+ closeAppModal('shortcutsModal');
1320
+ const menu = document.getElementById('legal-menu');
1321
+ if (menu) menu.classList.add('hidden');
1322
+ }
1323
  });
1324
 
1325
+ // Auto-show keyboard shortcuts on every page visit (Desktop only)
1326
+ if (window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
1327
+ setTimeout(function () { openAppModal('shortcutsModal'); }, 800);
1328
+ }
1329
+ </script>
1330
+ <nav class="mobile-bottom-nav" id="mobile-bottom-nav">
1331
+ <button class="mob-nav-item" id="mob-nav-about" onclick="switchTab('about')">
1332
+ <i class="fa-solid fa-circle-info"></i>
1333
+ </button>
1334
+ <button class="mob-nav-item" id="mob-nav-overview" onclick="switchTab('overview')">
1335
+ <i class="fa-solid fa-desktop"></i>
1336
+ </button>
1337
+ <button class="mob-nav-item" id="mob-nav-run-details" onclick="switchTab('run-details')">
1338
+ <i class="fa-solid fa-microchip"></i>
1339
+ </button>
1340
+ <button class="mob-nav-item" id="mob-nav-reports" onclick="switchTab('reports')">
1341
+ <i class="fa-solid fa-file-lines"></i>
1342
+ </button>
1343
+ <button class="mob-nav-item" id="mob-nav-settings" onclick="switchTab('settings')">
1344
+ <i class="fa-solid fa-sliders"></i>
1345
+ </button>
1346
+ <button class="mob-nav-item" id="mob-nav-help" onclick="switchTab('help')">
1347
+ <i class="fa-solid fa-circle-question"></i>
1348
+ </button>
1349
+ <button class="mob-nav-item" id="mob-nav-feedback" onclick="switchTab('feedback')">
1350
+ <i class="fa-solid fa-comment-dots"></i>
1351
+ </button>
1352
+ </nav>
1353
+ <script>
1354
+ if ('serviceWorker' in navigator) {
1355
+ window.addEventListener('load', () => {
1356
+ navigator.serviceWorker.register('./sw.js');
1357
+ });
1358
+ }
1359
  </script>
1360
  </body>
1361
 
requirements.txt CHANGED
@@ -7,11 +7,7 @@ ultralytics>=8.3.0
7
  numpy>=1.23.0,<2.0.0
8
  python-dotenv==1.0.0
9
  websockets==12.0
10
- onnxruntime
11
  openvino>=2024.0.0
12
- nncf>=2.14.0,<3.0.0
13
- onnx>=1.12.0
14
- onnxslim>=0.1.71
15
  lap>=0.5.12
16
  resend
17
  torch==2.1.0+cpu
 
7
  numpy>=1.23.0,<2.0.0
8
  python-dotenv==1.0.0
9
  websockets==12.0
 
10
  openvino>=2024.0.0
 
 
 
11
  lap>=0.5.12
12
  resend
13
  torch==2.1.0+cpu