Subh775 commited on
Commit
dde3ae9
·
1 Parent(s): 0bdf61b

major fixes & improvements..

Browse files
backend/engine.py CHANGED
@@ -194,7 +194,7 @@ def run(model, video_path, line, config, on_frame, save_annotated=False, annotat
194
  cx = int((box[0] + box[2]) / 2)
195
  cy = int((box[1] + box[3]) / 2)
196
 
197
- heatmap_points.append([cx, cy])
198
  track_positions[obj_id].append((frame_idx, cx, cy))
199
 
200
  current = _side((cx, cy), a, b)
@@ -280,4 +280,4 @@ def run(model, video_path, line, config, on_frame, save_annotated=False, annotat
280
  if annotated_path and os.path.exists(annotated_path):
281
  result["annotated_video"] = annotated_path
282
 
283
- return result
 
194
  cx = int((box[0] + box[2]) / 2)
195
  cy = int((box[1] + box[3]) / 2)
196
 
197
+ heatmap_points.append([cx, cy, float(r.boxes.conf.cpu().numpy()[list(ids).index(obj_id)])])
198
  track_positions[obj_id].append((frame_idx, cx, cy))
199
 
200
  current = _side((cx, cy), a, b)
 
280
  if annotated_path and os.path.exists(annotated_path):
281
  result["annotated_video"] = annotated_path
282
 
283
+ return result
backend/visualize.py CHANGED
@@ -171,9 +171,31 @@ def export_csv(raw_events, out_dir):
171
  return "raw_data.csv"
172
 
173
  def spatial_heatmap(heatmap_points, video_path, out_dir, fmt="png"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  if not heatmap_points or not video_path or not os.path.exists(video_path):
175
  return None
176
-
177
  cap = cv2.VideoCapture(video_path)
178
  ret, frame = cap.read()
179
  cap.release()
@@ -182,32 +204,111 @@ def spatial_heatmap(heatmap_points, video_path, out_dir, fmt="png"):
182
 
183
  h, w = frame.shape[:2]
184
  density = np.zeros((h, w), dtype=np.float32)
185
-
 
 
 
 
 
 
 
 
186
  for pt in heatmap_points:
187
  cx, cy = int(pt[0]), int(pt[1])
188
- if 0 <= cx < w and 0 <= cy < h:
189
- density[cy, cx] += 1.0
190
-
191
- density = cv2.GaussianBlur(density, (75, 75), 0)
192
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  max_val = np.max(density)
194
- if max_val > 0:
195
- density = (density / max_val) * 255.0
196
- density = density.astype(np.uint8)
 
 
 
 
 
 
197
 
198
- heatmap = cv2.applyColorMap(density, cv2.COLORMAP_JET)
199
- mask = density > 10
200
-
201
  overlay = frame.copy()
202
- overlay[mask] = cv2.addWeighted(frame[mask], 0.3, heatmap[mask], 0.7, 0).squeeze()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
  if fmt == "pdf":
205
- # Wrap OpenCV image in matplotlib for PDF export
206
  overlay_rgb = cv2.cvtColor(overlay, cv2.COLOR_BGR2RGB)
207
- fig, ax = plt.subplots(figsize=(10, 6), facecolor=C_BG)
208
  ax.imshow(overlay_rgb)
209
- ax.set_title("Spatial Density Heatmap", fontsize=13, fontweight="700", color=C_PRIMARY, pad=14)
 
210
  ax.axis('off')
 
 
 
 
211
  path = os.path.join(out_dir, "heatmap.pdf")
212
  fig.savefig(path, dpi=200, bbox_inches="tight", facecolor=C_BG, edgecolor="none")
213
  plt.close(fig)
@@ -261,4 +362,4 @@ def generate_all(data, model_classes, out_dir, report_format="png"):
261
  name = fn()
262
  if name:
263
  files.append(name)
264
- return files
 
171
  return "raw_data.csv"
172
 
173
  def spatial_heatmap(heatmap_points, video_path, out_dir, fmt="png"):
174
+ """
175
+ Confidence-Weighted Spatial Density Map (xAI / Explainability Overlay).
176
+
177
+ Each detection contributes a Gaussian kernel to the accumulation grid,
178
+ weighted by the model's own confidence score for that detection.
179
+ This means the heatmap directly encodes WHERE the model is most certain
180
+ vehicles exist — making it a faithful spatial explanation of the detector's
181
+ attention, without requiring backpropagation.
182
+
183
+ This is distinct from Grad-CAM (which needs a differentiable classifier) and
184
+ is the correct xAI approach for a post-processing YOLO/OpenVINO deployment
185
+ where gradients are not available at runtime.
186
+
187
+ Algorithm:
188
+ 1. For each detection (cx, cy, conf): stamp a 2D Gaussian kernel of
189
+ radius proportional to frame size, weighted by conf.
190
+ 2. Accumulate all weighted kernels into a float32 density grid.
191
+ 3. Apply a mild additional Gaussian blur for visual smoothness.
192
+ 4. Normalize [0, 255], apply COLORMAP_JET.
193
+ 5. Blend over the original frame only where density > threshold.
194
+ 6. Annotate with a legend showing the confidence scale.
195
+ """
196
  if not heatmap_points or not video_path or not os.path.exists(video_path):
197
  return None
198
+
199
  cap = cv2.VideoCapture(video_path)
200
  ret, frame = cap.read()
201
  cap.release()
 
204
 
205
  h, w = frame.shape[:2]
206
  density = np.zeros((h, w), dtype=np.float32)
207
+
208
+ # Kernel radius: ~3% of the shorter dimension, min 20px
209
+ kernel_r = max(20, int(min(h, w) * 0.03))
210
+ kernel_size = kernel_r * 2 + 1
211
+
212
+ # Pre-build a unit Gaussian kernel to stamp for each detection
213
+ _kx = cv2.getGaussianKernel(kernel_size, kernel_r / 2.5)
214
+ _unit_kernel = (_kx @ _kx.T).astype(np.float32) # shape (ks, ks)
215
+
216
  for pt in heatmap_points:
217
  cx, cy = int(pt[0]), int(pt[1])
218
+ # Support both old [cx, cy] and new [cx, cy, conf] formats
219
+ conf = float(pt[2]) if len(pt) > 2 else 1.0
220
+
221
+ # Kernel bounding box (clip to frame)
222
+ x0 = max(0, cx - kernel_r)
223
+ y0 = max(0, cy - kernel_r)
224
+ x1 = min(w, cx + kernel_r + 1)
225
+ y1 = min(h, cy + kernel_r + 1)
226
+
227
+ # Corresponding slice in the kernel
228
+ kx0 = x0 - (cx - kernel_r)
229
+ ky0 = y0 - (cy - kernel_r)
230
+ kx1 = kx0 + (x1 - x0)
231
+ ky1 = ky0 + (y1 - y0)
232
+
233
+ if x1 > x0 and y1 > y0 and kx1 > kx0 and ky1 > ky0:
234
+ density[y0:y1, x0:x1] += conf * _unit_kernel[ky0:ky1, kx0:kx1]
235
+
236
+ # Mild additional smoothing pass
237
+ density = cv2.GaussianBlur(density, (31, 31), 0)
238
+
239
  max_val = np.max(density)
240
+ if max_val <= 0:
241
+ return None
242
+ density_norm = (density / max_val * 255.0).astype(np.uint8)
243
+
244
+ heatmap_color = cv2.applyColorMap(density_norm, cv2.COLORMAP_JET)
245
+
246
+ # Blend: only paint where density is meaningful (>4% of max)
247
+ threshold = int(0.04 * 255)
248
+ mask = density_norm > threshold
249
 
 
 
 
250
  overlay = frame.copy()
251
+ # Smooth alpha blend using the density as alpha weight
252
+ alpha_map = (density_norm.astype(np.float32) / 255.0) * 0.72
253
+ alpha_map = np.clip(alpha_map, 0, 0.72)
254
+ for c in range(3):
255
+ overlay[:, :, c] = np.where(
256
+ mask,
257
+ (1.0 - alpha_map) * frame[:, :, c] + alpha_map * heatmap_color[:, :, c],
258
+ frame[:, :, c]
259
+ ).astype(np.uint8)
260
+
261
+ # ── xAI legend bar ──────────────────────────────────────────────────────
262
+ # Draw a horizontal colorbar with labels in the bottom-left corner
263
+ bar_w, bar_h = min(240, w // 4), 14
264
+ bar_x, bar_y = 16, h - bar_h - 36
265
+
266
+ gradient = np.tile(np.arange(256, dtype=np.uint8), (bar_h, 1))
267
+ gradient_color = cv2.applyColorMap(gradient, cv2.COLORMAP_JET) # (bar_h, 256, 3)
268
+ gradient_resized = cv2.resize(gradient_color, (bar_w, bar_h))
269
+
270
+ # Semi-transparent background panel behind the legend
271
+ panel_pad = 10
272
+ panel = overlay[bar_y - panel_pad : bar_y + bar_h + panel_pad + 18,
273
+ bar_x - panel_pad : bar_x + bar_w + panel_pad]
274
+ if panel.size > 0:
275
+ dark = np.full_like(panel, 15)
276
+ overlay[bar_y - panel_pad : bar_y + bar_h + panel_pad + 18,
277
+ bar_x - panel_pad : bar_x + bar_w + panel_pad] = cv2.addWeighted(panel, 0.35, dark, 0.65, 0)
278
+
279
+ overlay[bar_y : bar_y + bar_h, bar_x : bar_x + bar_w] = gradient_resized
280
+
281
+ font = cv2.FONT_HERSHEY_SIMPLEX
282
+ font_s = 0.35
283
+ thickness = 1
284
+ label_color = (220, 220, 220)
285
+
286
+ cv2.putText(overlay, "Low Confidence", (bar_x, bar_y + bar_h + 14),
287
+ font, font_s, label_color, thickness, cv2.LINE_AA)
288
+ high_label = "High Confidence"
289
+ (tw, _), _ = cv2.getTextSize(high_label, font, font_s, thickness)
290
+ cv2.putText(overlay, high_label, (bar_x + bar_w - tw, bar_y + bar_h + 14),
291
+ font, font_s, label_color, thickness, cv2.LINE_AA)
292
+
293
+ # Title label above the bar
294
+ title_label = "Detection Confidence Density (xAI)"
295
+ (ttw, _), _ = cv2.getTextSize(title_label, font, 0.38, thickness)
296
+ cv2.putText(overlay, title_label,
297
+ (bar_x, bar_y - panel_pad + 8),
298
+ font, 0.38, (180, 180, 180), thickness, cv2.LINE_AA)
299
+ # ── end legend ──────────────────────────────────────────────────────────
300
 
301
  if fmt == "pdf":
 
302
  overlay_rgb = cv2.cvtColor(overlay, cv2.COLOR_BGR2RGB)
303
+ fig, ax = plt.subplots(figsize=(12, 7), facecolor=C_BG)
304
  ax.imshow(overlay_rgb)
305
+ ax.set_title("Detection Confidence Density Map · xAI Spatial Explanation",
306
+ fontsize=13, fontweight="700", color=C_PRIMARY, pad=14)
307
  ax.axis('off')
308
+ fig.text(0.5, 0.01,
309
+ "Brighter regions = higher accumulated detector confidence. "
310
+ "Generated from confidence-weighted Gaussian kernel density estimation.",
311
+ ha="center", fontsize=7, color=C_ACCENT)
312
  path = os.path.join(out_dir, "heatmap.pdf")
313
  fig.savefig(path, dpi=200, bbox_inches="tight", facecolor=C_BG, edgecolor="none")
314
  plt.close(fig)
 
362
  name = fn()
363
  if name:
364
  files.append(name)
365
+ return files
frontend/css/vehicles.css CHANGED
@@ -1,1389 +1,1410 @@
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
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ flex-direction: row !important;
878
+ flex-wrap: nowrap !important;
879
+ align-items: center !important;
880
+ justify-content: space-between !important;
881
+ padding: 14px 0 !important;
882
+ border-bottom: 1px solid #1a1a1a !important;
883
+ gap: 12px !important;
884
+ }
885
+
886
+ .s-row:last-child {
887
+ border-bottom: none !important;
888
+ }
889
+
890
+ /* Never let mobile flex override Tailwind .hidden utility */
891
+ .s-row.hidden {
892
+ display: none !important;
893
+ }
894
+
895
+ /* chip-selector sits as a sibling below the annotated s-row on mobile */
896
+ #chip-selector:not(.hidden-chip-container) {
897
+ margin-top: 0 !important;
898
+ border-top: none !important;
899
+ padding-top: 0 !important;
900
+ padding-bottom: 12px !important;
901
+ }
902
+
903
+ /* Lock toggle in annotated row — must never wrap or shrink */
904
+ .s-row[data-param="annotated"]>.toggle-track {
905
+ flex-shrink: 0 !important;
906
+ flex-grow: 0 !important;
907
+ flex-basis: 36px !important;
908
+ width: 36px !important;
909
+ min-width: 36px !important;
910
+ align-self: center !important;
911
+ }
912
+
913
+ /* Label side must absorb remaining space and never overflow */
914
+ .s-row[data-param="annotated"]>div:first-child {
915
+ flex: 1 1 0 !important;
916
+ min-width: 0 !important;
917
+ overflow: hidden !important;
918
+ }
919
+
920
+ .s-stepper {
921
+ width: 140px !important;
922
+ /* Compact fixed width */
923
+ scale: 0.9;
924
+ transform-origin: right;
925
+ display: inline-flex !important;
926
+ }
927
+
928
+ .toggle-track {
929
+ width: 36px !important;
930
+ scale: 0.9;
931
+ transform-origin: right;
932
+ }
933
+
934
+ @media (max-width: 480px) {
935
+ .s-row {
936
+ flex-direction: row !important;
937
+ flex-wrap: nowrap !important;
938
+ align-items: center !important;
939
+ justify-content: space-between !important;
940
+ gap: 12px !important;
941
+ padding: 10px 16px !important;
942
+ width: 100% !important;
943
+ box-sizing: border-box !important;
944
+ }
945
+
946
+ /* chip panel below annotated row — remove extra top gap */
947
+ #chip-selector:not(.hidden-chip-container) {
948
+ margin-top: 0 !important;
949
+ padding-bottom: 12px !important;
950
+ padding-left: 16px !important;
951
+ padding-right: 16px !important;
952
+ border-top: none !important;
953
+ }
954
+
955
+ .s-row {
956
+ padding: 12px 16px !important;
957
+ }
958
+
959
+ #tab-run-details .p-8 {
960
+ padding: 20px !important;
961
+ }
962
+
963
+ #run-results-content {
964
+ grid-template-columns: 1fr !important;
965
+ gap: 16px !important;
966
+ }
967
+
968
+ #panel-video .flex,
969
+ #panel-perf .flex,
970
+ #panel-model .flex,
971
+ #panel-infer .flex {
972
+ padding-bottom: 8px !important;
973
+ }
974
+
975
+ .s-row .info-wrap {
976
+ display: inline-flex !important;
977
+ vertical-align: middle;
978
+ }
979
+
980
+ .s-row>div:first-child {
981
+ width: auto !important;
982
+ max-width: 75% !important;
983
+ flex: 1 !important;
984
+ }
985
+
986
+ .toggle-track {
987
+ width: 36px !important;
988
+ min-width: 36px !important;
989
+ height: 20px !important;
990
+ flex-shrink: 0 !important;
991
+ display: block !important;
992
+ position: relative !important;
993
+ }
994
+
995
+ #run-results-card .text-[10px] {
996
+ font-size: 9px !important;
997
+ letter-spacing: 0.05em !important;
998
+ }
999
+
1000
+ .s-row>.s-stepper {
1001
+ width: 130px !important;
1002
+ flex-shrink: 0 !important;
1003
+ display: inline-flex !important;
1004
+ flex-direction: row !important;
1005
+ }
1006
+
1007
+ .chip-container {
1008
+ display: grid !important;
1009
+ grid-template-columns: 1fr 1fr !important;
1010
+ gap: 6px !important;
1011
+ margin-top: 12px !important;
1012
+ padding: 10px !important;
1013
+ background: rgba(255, 255, 255, 0.03);
1014
+ border-radius: 8px;
1015
+ border: 1px solid #1a1a1a;
1016
+ width: 100% !important;
1017
+ box-sizing: border-box !important;
1018
+ }
1019
+
1020
+ .chip {
1021
+ padding: 6px !important;
1022
+ font-size: 9px !important;
1023
+ min-height: 32px !important;
1024
+ border-radius: 6px !important;
1025
+ justify-content: center !important;
1026
+ width: 100% !important;
1027
+ white-space: nowrap !important;
1028
+ }
1029
+
1030
+ .s-stepper {
1031
+ width: 130px !important;
1032
+ min-width: 130px !important;
1033
+ display: inline-flex !important;
1034
+ flex-direction: row !important;
1035
+ align-items: center !important;
1036
+ justify-content: space-between !important;
1037
+ transform-origin: right !important;
1038
+ }
1039
+
1040
+ .toggle-track {
1041
+ transform-origin: right !important;
1042
+ }
1043
+ }
1044
+
1045
+ /* --- Progress bar wrapper — remove extra margin to fix huge gap --- */
1046
+ #progress-bar-wrapper {
1047
+ width: 100% !important;
1048
+ max-width: 100% !important;
1049
+ box-sizing: border-box !important;
1050
+ margin-top: auto !important;
1051
+ margin-bottom: 4px !important;
1052
+ padding: 8px 12px !important;
1053
+ flex-direction: column !important;
1054
+ align-items: flex-start !important;
1055
+ gap: 6px !important;
1056
+ position: relative;
1057
+ z-index: 10;
1058
+ }
1059
+
1060
+ #progress-bar-wrapper>div:first-child {
1061
+ width: 100% !important;
1062
+ flex: 1 !important;
1063
+ min-width: 0 !important;
1064
+ margin-right: 0 !important;
1065
+ }
1066
+
1067
+ #progress-bar-wrapper>div:last-child {
1068
+ width: 100% !important;
1069
+ justify-content: space-between !important;
1070
+ font-size: 10px !important;
1071
+ }
1072
+
1073
+ /* --- All other grids collapse to single column --- */
1074
+ .grid-cols-3,
1075
+ .grid-cols-2,
1076
+ .lg\:grid-cols-2,
1077
+ .xl\:grid-cols-3 {
1078
+ grid-template-columns: 1fr !important;
1079
+ }
1080
+
1081
+ /* --- Run details tab --- */
1082
+ #run-results-content {
1083
+ grid-template-columns: 1fr !important;
1084
+ }
1085
+
1086
+ #tab-run-details .grid-cols-2,
1087
+ #tab-run-details .grid-cols-3 {
1088
+ grid-template-columns: 1fr !important;
1089
+ }
1090
+
1091
+ /* --- Reports grid --- */
1092
+ #reports-grid,
1093
+ #reports-pending {
1094
+ grid-template-columns: 1fr !important;
1095
+ }
1096
+
1097
+ /* --- About tab grid --- */
1098
+ #tab-about .grid.grid-cols-3 {
1099
+ grid-template-columns: 1fr !important;
1100
+ }
1101
+
1102
+ /* --- Post-process cards --- */
1103
+ #post-process-cards {
1104
+ grid-template-columns: 1fr !important;
1105
+ }
1106
+
1107
+ /* --- Insights panel --- */
1108
+ #insights-panel .grid {
1109
+ grid-template-columns: 1fr !important;
1110
+ }
1111
+
1112
+ /* --- Feedback tab --- */
1113
+ #tab-feedback .grid {
1114
+ grid-template-columns: 1fr !important;
1115
+ }
1116
+
1117
+ /* --- About tab cards --- */
1118
+ #tab-about .bg-black.border.rounded-xl {
1119
+ padding: 20px !important;
1120
+ }
1121
+
1122
+ /* --- Stepper — ensure full tap area --- */
1123
+ .s-stepper button {
1124
+ padding: 10px 14px;
1125
+ min-width: 40px;
1126
+ min-height: 40px;
1127
+ }
1128
+
1129
+ /* --- s-row label text — allow wrap --- */
1130
+ .s-row>div:first-child .text-xs {
1131
+ font-size: 11px;
1132
+ }
1133
+
1134
+ /* --- Help accordion buttons --- */
1135
+ #tab-help button.w-full {
1136
+ min-height: 52px;
1137
+ padding: 14px 16px !important;
1138
+ }
1139
+
1140
+ /* --- Feedback priority chips grid --- */
1141
+ #fb-priorities {
1142
+ grid-template-columns: 1fr !important;
1143
+ }
1144
+
1145
+ /* --- Keyboard shortcut modal --- */
1146
+ #appModal-shortcutsModal>div {
1147
+ max-width: 95% !important;
1148
+ padding: 20px !important;
1149
+ }
1150
+
1151
+ /* --- Privacy / Terms modals --- */
1152
+ [id^="appModal-"]>div {
1153
+ max-width: 95% !important;
1154
+ max-height: 80dvh !important;
1155
+ overflow-y: auto !important;
1156
+ }
1157
+
1158
+ #tab-overview>div:last-child {
1159
+ min-height: 300px !important;
1160
+ padding-bottom: 4px !important;
1161
+ margin-bottom: 0 !important;
1162
+ }
1163
+
1164
+ /* --- Vehicle Classification Internal Scroll --- */
1165
+ #tab-overview>div:nth-child(4) {
1166
+ max-height: 380px !important;
1167
+ display: flex !important;
1168
+ flex-direction: column !important;
1169
+ }
1170
+
1171
+ #tab-overview>div:nth-child(4) #class-breakdown {
1172
+ flex: 1 !important;
1173
+ overflow-y: auto !important;
1174
+ min-height: 0 !important;
1175
+ }
1176
+ }
1177
+
1178
+ /* =============================================
1179
+ BOTTOM NAVIGATION BAR — mobile only
1180
+ ============================================= */
1181
+ .mobile-bottom-nav {
1182
+ display: none;
1183
+ /* hidden by default, shown on mobile */
1184
+ position: fixed;
1185
+ bottom: 0;
1186
+ left: 0;
1187
+ right: 0;
1188
+ height: 68px;
1189
+ background: #000000;
1190
+ border-top: 1px solid #1a1a1a;
1191
+ z-index: 40;
1192
+ align-items: stretch;
1193
+ }
1194
+
1195
+ .mob-nav-item {
1196
+ flex: 1;
1197
+ display: flex;
1198
+ flex-direction: column;
1199
+ align-items: center;
1200
+ justify-content: center;
1201
+ gap: 3px;
1202
+ cursor: pointer;
1203
+ color: #444444;
1204
+ font-size: 0;
1205
+ font-weight: 700;
1206
+ text-transform: uppercase;
1207
+ letter-spacing: 0.05em;
1208
+ transition: color 0.15s ease;
1209
+ border: none;
1210
+ background: none;
1211
+ padding: 8px 2px;
1212
+ -webkit-tap-highlight-color: transparent;
1213
+ }
1214
+
1215
+ .mob-nav-item i {
1216
+ font-size: 22px;
1217
+ transition: color 0.15s ease;
1218
+ }
1219
+
1220
+ .mob-nav-item.active {
1221
+ color: var(--cocoa-l);
1222
+ }
1223
+
1224
+ .mob-nav-item.active i {
1225
+ color: var(--cocoa-l);
1226
+ }
1227
+
1228
+ .mob-nav-item:active {
1229
+ color: var(--cocoa-xl);
1230
+ }
1231
+
1232
+ /* Show bottom nav only on mobile */
1233
+ @media (max-width: 1023px) {
1234
+ .mobile-bottom-nav {
1235
+ display: flex !important;
1236
+ }
1237
+ }
1238
+
1239
+ /* =============================================
1240
+ MEDIUM TABLET (640px–1023px) adjustments
1241
+ ============================================= */
1242
+ @media (min-width: 640px) and (max-width: 1023px) {
1243
+
1244
+ /* 2-column grids on tablet where it fits */
1245
+ #tab-overview>div {
1246
+ min-height: 280px;
1247
+ }
1248
+
1249
+ #reports-grid,
1250
+ #reports-pending {
1251
+ grid-template-columns: repeat(2, 1fr) !important;
1252
+ }
1253
+
1254
+ #fb-priorities {
1255
+ grid-template-columns: repeat(2, 1fr) !important;
1256
+ }
1257
+
1258
+ #tab-about .grid.grid-cols-3 {
1259
+ grid-template-columns: repeat(2, 1fr) !important;
1260
+ }
1261
+ }
1262
+
1263
+ /* =============================================
1264
+ TOUCH DEVICES — remove hover jank
1265
+ ============================================= */
1266
+ @media (hover: none) and (pointer: coarse) {
1267
+ .nav-item-inactive:hover {
1268
+ color: #555555 !important;
1269
+ background-color: transparent !important;
1270
+ }
1271
+
1272
+ .chip:hover {
1273
+ border-color: #333333;
1274
+ }
1275
+
1276
+ .chip.active:hover {
1277
+ background: var(--cocoa-l);
1278
+ }
1279
+
1280
+ .s-stepper button:hover {
1281
+ background: transparent;
1282
+ color: #666666;
1283
+ }
1284
+
1285
+ /* Make all interactive elements minimum 44px tall */
1286
+ button,
1287
+ .fb-emoji-btn,
1288
+ .mob-nav-item {
1289
+ min-height: 44px;
1290
+ }
1291
+ }
1292
+
1293
+ /* ============================================================
1294
+ Custom Select Dropdown (uf-select)
1295
+ Replaces native <select> to prevent OS picker sheet on mobile
1296
+ ============================================================ */
1297
+ .uf-select-wrap {
1298
+ position: relative;
1299
+ display: inline-block;
1300
+ min-width: 110px;
1301
+ }
1302
+
1303
+ .uf-select-wrap.w-full {
1304
+ display: block;
1305
+ width: 100%;
1306
+ }
1307
+
1308
+ .uf-select-trigger {
1309
+ display: flex;
1310
+ align-items: center;
1311
+ justify-content: space-between;
1312
+ gap: 6px;
1313
+ padding: 5px 10px;
1314
+ background: #111111;
1315
+ border: 1px solid #222222;
1316
+ border-radius: 6px;
1317
+ font-size: 11px;
1318
+ font-weight: 600;
1319
+ color: #ffffff;
1320
+ cursor: pointer;
1321
+ user-select: none;
1322
+ -webkit-tap-highlight-color: transparent;
1323
+ transition: border-color 0.15s;
1324
+ white-space: nowrap;
1325
+ }
1326
+
1327
+ .uf-select-trigger:hover,
1328
+ .uf-select-trigger:active {
1329
+ border-color: #444444;
1330
+ }
1331
+
1332
+ .uf-select-arrow {
1333
+ font-size: 9px;
1334
+ color: #666666;
1335
+ transition: transform 0.2s ease;
1336
+ flex-shrink: 0;
1337
+ }
1338
+
1339
+ .uf-select-arrow-open {
1340
+ transform: rotate(180deg);
1341
+ }
1342
+
1343
+ /* Dropdown panel — opens downward by default */
1344
+ .uf-select-dropdown {
1345
+ position: absolute;
1346
+ top: calc(100% + 4px);
1347
+ left: 0;
1348
+ min-width: 100%;
1349
+ background: #111111;
1350
+ border: 1px solid #2a2a2a;
1351
+ border-radius: 8px;
1352
+ z-index: 9999;
1353
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8);
1354
+ overflow: hidden;
1355
+ max-height: 240px;
1356
+ overflow-y: auto;
1357
+ }
1358
+
1359
+ /* Upward variant — anchors above trigger, for bottom-of-screen selects */
1360
+ .uf-select-dropdown-up {
1361
+ top: auto;
1362
+ bottom: calc(100% + 4px);
1363
+ }
1364
+
1365
+ .uf-select-option {
1366
+ padding: 10px 14px;
1367
+ font-size: 11px;
1368
+ font-weight: 600;
1369
+ color: #aaaaaa;
1370
+ cursor: pointer;
1371
+ transition: background 0.1s, color 0.1s;
1372
+ -webkit-tap-highlight-color: transparent;
1373
+ }
1374
+
1375
+ .uf-select-option:hover,
1376
+ .uf-select-option:active {
1377
+ background: #1a1a1a;
1378
+ color: #ffffff;
1379
+ }
1380
+
1381
+ .uf-select-option-active {
1382
+ color: var(--cocoa-l);
1383
+ background: #0a0a0a;
1384
+ }
1385
+
1386
+ /* Hide scrollbar inside dropdown — options fit within max-height */
1387
+ .uf-select-dropdown::-webkit-scrollbar {
1388
+ width: 0;
1389
+ height: 0;
1390
+ }
1391
+
1392
+ /* Desktop: Vehicle Classification thin grey scrollbar (matches reference) */
1393
+ @media (min-width: 1024px) {
1394
+ #class-breakdown::-webkit-scrollbar {
1395
+ width: 4px;
1396
+ }
1397
+
1398
+ #class-breakdown::-webkit-scrollbar-track {
1399
+ background: #000000;
1400
+ }
1401
+
1402
+ #class-breakdown::-webkit-scrollbar-thumb {
1403
+ background: #333333;
1404
+ border-radius: 4px;
1405
+ }
1406
+
1407
+ #class-breakdown::-webkit-scrollbar-thumb:hover {
1408
+ background: #444444;
1409
+ }
1410
+ }
frontend/js/vehicles.js CHANGED
@@ -1096,8 +1096,8 @@ function switchTab(tab) {
1096
  'confidence_dist.png': { title: 'Confidence Curve', desc: 'Distribution of detection confidences' },
1097
  'confidence_dist.pdf': { title: 'Confidence Curve', desc: 'Distribution of detection confidences' },
1098
  'annotated.mp4': { title: 'Annotated Video Export', desc: 'Rendered video with tracking overlays' },
1099
- 'heatmap.png': { title: 'Spatial Density Heatmap', desc: 'Aggregated path utilization mapping' },
1100
- 'heatmap.pdf': { title: 'Spatial Density Heatmap', desc: 'Aggregated path utilization mapping' },
1101
  'raw_data.csv': { title: 'Raw Analytics Export', desc: 'Comma-separated values of all crossings' },
1102
  'analysis.json': { title: 'Structured JSON Export', desc: 'Complete analysis data with metadata for API consumption' }
1103
  };
@@ -1158,11 +1158,9 @@ function switchTab(tab) {
1158
 
1159
  const isHeatmap = name.includes('heatmap');
1160
  const tooltipHTML = isHeatmap ? `
1161
- <span class="info-wrap ml-1.5 relative inline-flex items-center">
1162
- <span class="info-btn cursor-pointer inline-flex items-center justify-center rounded-full border border-neutral-700 bg-neutral-900 text-[8px] text-slate-400 hover:text-white hover:border-slate-400 transition-all" style="width: 14px; height: 14px;">
1163
- <i class="fa-solid fa-info"></i>
1164
- </span>
1165
- <span class="info-tip">Aggregated path utilization mapping. Hot spots represent true accumulated vehicle presence and dwell time across paths.</span>
1166
  </span>` : '';
1167
 
1168
  card.innerHTML = `
@@ -1223,13 +1221,15 @@ function switchTab(tab) {
1223
  }
1224
 
1225
  function showRetryBubble() {
 
 
 
 
1226
  if (sessionStorage.getItem('uf_retry_shown')) return;
1227
  sessionStorage.setItem('uf_retry_shown', '1');
1228
 
1229
  const existing = document.getElementById('retry-bubble');
1230
  if (existing) existing.remove();
1231
-
1232
- const isMobile = window.innerWidth < 1024;
1233
  const bubble = document.createElement('div');
1234
  bubble.id = 'retry-bubble';
1235
 
 
1096
  'confidence_dist.png': { title: 'Confidence Curve', desc: 'Distribution of detection confidences' },
1097
  'confidence_dist.pdf': { title: 'Confidence Curve', desc: 'Distribution of detection confidences' },
1098
  'annotated.mp4': { title: 'Annotated Video Export', desc: 'Rendered video with tracking overlays' },
1099
+ 'heatmap.png': { title: 'Detection Confidence Density Map', desc: 'xAI spatial explanation — confidence-weighted Gaussian kernel density over all detections' },
1100
+ 'heatmap.pdf': { title: 'Detection Confidence Density Map', desc: 'xAI spatial explanation — confidence-weighted Gaussian kernel density over all detections' },
1101
  'raw_data.csv': { title: 'Raw Analytics Export', desc: 'Comma-separated values of all crossings' },
1102
  'analysis.json': { title: 'Structured JSON Export', desc: 'Complete analysis data with metadata for API consumption' }
1103
  };
 
1158
 
1159
  const isHeatmap = name.includes('heatmap');
1160
  const tooltipHTML = isHeatmap ? `
1161
+ <span class="info-wrap">
1162
+ <span class="info-btn"><i class="fa-solid fa-info"></i></span>
1163
+ <span class="info-tip">Confidence-weighted spatial density map — a faithful xAI explanation of WHERE the detector is most certain vehicles exist. Each detection stamps a Gaussian kernel scaled by its confidence score. Brighter regions = higher accumulated detection confidence.</span>
 
 
1164
  </span>` : '';
1165
 
1166
  card.innerHTML = `
 
1221
  }
1222
 
1223
  function showRetryBubble() {
1224
+ // Desktop has the sidebar nav always visible — the hint is redundant there.
1225
+ const isMobile = window.innerWidth < 1024;
1226
+ if (!isMobile) return;
1227
+
1228
  if (sessionStorage.getItem('uf_retry_shown')) return;
1229
  sessionStorage.setItem('uf_retry_shown', '1');
1230
 
1231
  const existing = document.getElementById('retry-bubble');
1232
  if (existing) existing.remove();
 
 
1233
  const bubble = document.createElement('div');
1234
  bubble.id = 'retry-bubble';
1235
 
frontend/vehicles.html CHANGED
@@ -365,9 +365,10 @@
365
  </div>
366
  <div class="p-8">
367
  <div id="run-results-content" class="grid grid-cols-3 gap-12">
368
- <div class="flex flex-col items-center justify-center p-12 rounded-2xl col-span-3"
369
- style="color:#555">
370
- <span class="text-[11px] font-bold uppercase tracking-[0.2em]">Initiate a run to view performance insights</span>
 
371
  </div>
372
  </div>
373
  </div>
@@ -415,10 +416,16 @@
415
  <!-- TAB: Reports -->
416
  <div id="tab-reports" class="hidden flex-1 min-h-0 overflow-y-auto">
417
  <div id="reports-pending-message"
418
- class="mb-4 text-center p-8 flex flex-col items-center justify-center gap-4 min-h-[200px]">
 
 
 
 
 
419
  <span id="reports-pending-text"
420
- class="text-xs text-[#a89f97] font-medium tracking-wide uppercase leading-relaxed max-w-[240px]">
421
- Return once processing is complete to access results.
 
422
  </span>
423
  </div>
424
 
@@ -575,24 +582,22 @@
575
  <i class="fa-solid fa-chevron-down uf-select-arrow" id="sv-report-arrow"></i>
576
  </div>
577
  <div class="uf-select-dropdown hidden" id="sv-report-dropdown">
578
- <div class="uf-select-option uf-select-option-active" data-value="png" onclick="ufSelectPick('sv-report','png','PNG Image')">PNG</div>
579
- <div class="uf-select-option" data-value="pdf" onclick="ufSelectPick('sv-report','pdf','PDF Document')">PDF</div>
580
  </div>
581
  <input type="hidden" id="sv-report" value="png">
582
  </div>
583
  </div>
584
- <div class="s-row flex-col items-stretch !gap-0" data-param="annotated">
585
- <div class="flex items-center justify-between w-full py-2">
586
- <div class="flex-1 pr-2">
587
- <div class="text-xs font-semibold text-slate-300">Export Annotated Video</div>
588
- <div class="text-[10px] text-slate-500">Diagnostic visual overlays</div>
589
- </div>
590
- <div class="toggle-track" id="sv-annotated" onclick="toggleExportMaster(this)">
591
- <div class="toggle-thumb"></div>
592
- </div>
593
  </div>
594
-
595
- <div id="chip-selector" class="chip-container hidden-chip-container">
 
 
 
596
  <div class="chip frozen" id="chip-bbox">
597
  <i class="fa-solid fa-check"></i> Bounding Boxes
598
  </div>
@@ -608,7 +613,6 @@
608
  <div class="chip" id="chip-track_id" onclick="toggleChip('track_id')">
609
  <i class="fa-solid fa-plus"></i> Track IDs
610
  </div>
611
- </div>
612
  </div>
613
  <div class="s-row">
614
  <div>
 
365
  </div>
366
  <div class="p-8">
367
  <div id="run-results-content" class="grid grid-cols-3 gap-12">
368
+ <div class="flex flex-col items-center justify-center p-12 rounded-2xl col-span-3 text-center w-full"
369
+ style="color:#555; min-height: 180px;">
370
+ <i class="fa-solid fa-chart-line text-4xl mb-4" style="color:#3a3a3a;"></i>
371
+ <span class="text-[11px] font-bold uppercase tracking-[0.2em] block w-full text-center">Initiate a run to view performance insights</span>
372
  </div>
373
  </div>
374
  </div>
 
416
  <!-- TAB: Reports -->
417
  <div id="tab-reports" class="hidden flex-1 min-h-0 overflow-y-auto">
418
  <div id="reports-pending-message"
419
+ class="mb-4 text-center p-8 flex flex-col items-center justify-center gap-4"
420
+ style="min-height: 60vh;">
421
+ <i class="fa-solid fa-hourglass-half text-5xl mb-2" style="color:#3a3230;"></i>
422
+ <div style="color:#c89a6c; font-size:13px; font-weight:700; letter-spacing:0.18em; text-transform:uppercase;">
423
+ Processing Pending
424
+ </div>
425
  <span id="reports-pending-text"
426
+ class="text-xs font-medium tracking-wide uppercase leading-relaxed text-center"
427
+ style="color:#5a5450; max-width:220px; display:block;">
428
+ Upload a video &amp; run analysis to access results and reports here.
429
  </span>
430
  </div>
431
 
 
582
  <i class="fa-solid fa-chevron-down uf-select-arrow" id="sv-report-arrow"></i>
583
  </div>
584
  <div class="uf-select-dropdown hidden" id="sv-report-dropdown">
585
+ <div class="uf-select-option uf-select-option-active" data-value="png" onclick="ufSelectPick('sv-report','png','PNG')">PNG</div>
586
+ <div class="uf-select-option" data-value="pdf" onclick="ufSelectPick('sv-report','pdf','PDF')">PDF</div>
587
  </div>
588
  <input type="hidden" id="sv-report" value="png">
589
  </div>
590
  </div>
591
+ <div class="s-row" data-param="annotated">
592
+ <div>
593
+ <div class="text-xs font-semibold text-slate-300">Export Annotated Video</div>
594
+ <div class="text-[10px] text-slate-500">Diagnostic visual overlays</div>
 
 
 
 
 
595
  </div>
596
+ <div class="toggle-track" id="sv-annotated" onclick="toggleExportMaster(this)">
597
+ <div class="toggle-thumb"></div>
598
+ </div>
599
+ </div>
600
+ <div id="chip-selector" class="chip-container hidden-chip-container" data-param="annotated-chips">
601
  <div class="chip frozen" id="chip-bbox">
602
  <i class="fa-solid fa-check"></i> Bounding Boxes
603
  </div>
 
613
  <div class="chip" id="chip-track_id" onclick="toggleChip('track_id')">
614
  <i class="fa-solid fa-plus"></i> Track IDs
615
  </div>
 
616
  </div>
617
  <div class="s-row">
618
  <div>