@harshankur/viewcounter 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.env.example +20 -0
  2. package/README.md +189 -14
  3. package/admin/apple-touch-icon.png +0 -0
  4. package/admin/assets/world-map.json +1 -0
  5. package/admin/css/admin.css +1875 -0
  6. package/admin/favicon.ico +0 -0
  7. package/admin/favicon.svg +9 -0
  8. package/admin/icon-192.png +0 -0
  9. package/admin/icon-512.png +0 -0
  10. package/admin/index.html +89 -0
  11. package/admin/js/api.js +100 -0
  12. package/admin/js/charts.js +502 -0
  13. package/admin/js/clamp.js +41 -0
  14. package/admin/js/constants.js +150 -0
  15. package/admin/js/dom.js +83 -0
  16. package/admin/js/format.js +79 -0
  17. package/admin/js/i18n.js +80 -0
  18. package/admin/js/insights.js +192 -0
  19. package/admin/js/listbox.js +144 -0
  20. package/admin/js/logs.js +167 -0
  21. package/admin/js/main.js +235 -0
  22. package/admin/js/modal.js +171 -0
  23. package/admin/js/table.js +134 -0
  24. package/admin/js/theme.js +72 -0
  25. package/admin/js/toast.js +47 -0
  26. package/admin/js/viewDialogs.js +208 -0
  27. package/admin/js/views.js +685 -0
  28. package/admin/locales/en.json +394 -0
  29. package/admin/site.webmanifest +20 -0
  30. package/config/index.js +83 -0
  31. package/constants.js +215 -2
  32. package/db/AdminRepository.js +562 -0
  33. package/db/DatabaseManager.js +94 -20
  34. package/db/LogRepository.js +217 -0
  35. package/db/adminSchema.js +244 -0
  36. package/db/retention.js +97 -0
  37. package/index.js +39 -6
  38. package/middleware/adminAuth.js +204 -0
  39. package/middleware/adminValidation.js +253 -0
  40. package/package.json +17 -10
  41. package/routes/admin.js +438 -0
  42. package/routes/analytics.js +11 -2
  43. package/utils/cookieUtils.js +47 -0
  44. package/utils/errorUtils.js +35 -0
@@ -0,0 +1,1875 @@
1
+ /*
2
+ * ViewCounter admin UI.
3
+ *
4
+ * Same design language as the documentation site (docs/index.html): the
5
+ * terminal palette, window chrome, grid and glow. Differences, on purpose:
6
+ * - system monospace fonts only. The docs site loads Google Fonts; the admin
7
+ * UI never contacts a third party, so an admin's IP is never disclosed to a
8
+ * font host and the strict CSP needs no exception.
9
+ * - the theme is a class on <html> (FRONTEND.md §6), not a data attribute.
10
+ */
11
+
12
+ /* ---------- Tokens (FRONTEND.md §4) ---------- */
13
+
14
+ :root,
15
+ .theme-dark {
16
+ /* Brand */
17
+ --primary: #3ddc84;
18
+ --primary-bright: #7dffb3;
19
+ --primary-glow: rgba(61, 220, 132, 0.35);
20
+ --secondary: #ffb454;
21
+ /* Background */
22
+ --bg: #0a0f0c;
23
+ /* Surface */
24
+ --card-bg: rgba(16, 24, 20, 0.82);
25
+ --surface-solid: #101814;
26
+ --nav-bg: rgba(8, 11, 9, 0.86);
27
+ --code-bg: #0d1310;
28
+ --row-hover: rgba(61, 220, 132, 0.06);
29
+ --row-selected: rgba(61, 220, 132, 0.12);
30
+ /* Border */
31
+ --border: rgba(61, 220, 132, 0.22);
32
+ --border-strong: rgba(61, 220, 132, 0.45);
33
+ /* Text */
34
+ --text: #e8f6ec;
35
+ --text-muted: #7c9686;
36
+ --text-on-primary: #0a0f0c;
37
+ /* Semantic status */
38
+ --success: #3ddc84;
39
+ --danger: #ef4444;
40
+ --danger-bg: rgba(239, 68, 68, 0.14);
41
+ --warning: #f5b942;
42
+ --warning-bg: rgba(245, 185, 66, 0.14);
43
+ --info: #5aa9e6;
44
+ --info-bg: rgba(90, 169, 230, 0.14);
45
+ --muted-bg: rgba(124, 150, 134, 0.16);
46
+ /* Effects */
47
+ --scanline-opacity: 0.4;
48
+ --shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.45);
49
+ --backdrop: rgba(4, 7, 5, 0.72);
50
+ color-scheme: dark;
51
+
52
+ /* Charts. Validated with the data-viz palette checks against the card
53
+ surfaces of each theme: one series hue inside the lightness band, and a
54
+ one-hue sequential map ramp, monotone, with its low end >= 2:1 so "few
55
+ views" never reads as "no views" (--map-0). */
56
+ --chart-series: #2aa862;
57
+ --chart-area: rgba(42, 168, 98, 0.1);
58
+ --chart-grid: rgba(61, 220, 132, 0.1);
59
+ --chart-baseline: rgba(61, 220, 132, 0.3);
60
+ --map-0: #1a2420;
61
+ --map-1: #236642;
62
+ --map-2: #288552;
63
+ --map-3: #2ea563;
64
+ --map-4: #4cc57f;
65
+ --map-5: #8fe3b2;
66
+
67
+ --font-mono: ui-monospace, 'SFMono-Regular', 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
68
+ --radius: 12px;
69
+ --radius-small: 8px;
70
+ }
71
+
72
+ .theme-light {
73
+ --primary: #1f7a4a;
74
+ --primary-bright: #12653c;
75
+ --primary-glow: rgba(31, 122, 74, 0.16);
76
+ --secondary: #b5590f;
77
+ --bg: #f3efe2;
78
+ --card-bg: rgba(255, 253, 246, 0.9);
79
+ --surface-solid: #fffdf6;
80
+ --nav-bg: rgba(243, 239, 226, 0.9);
81
+ --code-bg: #eae4d1;
82
+ --row-hover: rgba(31, 122, 74, 0.05);
83
+ --row-selected: rgba(31, 122, 74, 0.1);
84
+ --border: rgba(31, 122, 74, 0.25);
85
+ --border-strong: rgba(31, 122, 74, 0.5);
86
+ --text: #1b2a20;
87
+ --text-muted: #5f6f63;
88
+ --text-on-primary: #fffdf6;
89
+ --success: #1f7a4a;
90
+ --danger: #b42318;
91
+ --danger-bg: rgba(180, 35, 24, 0.1);
92
+ --warning: #8a5a00;
93
+ --warning-bg: rgba(181, 89, 15, 0.12);
94
+ --info: #1c6fa8;
95
+ --info-bg: rgba(28, 111, 168, 0.1);
96
+ --muted-bg: rgba(95, 111, 99, 0.12);
97
+ --scanline-opacity: 0;
98
+ --shadow: 0 24px 48px -20px rgba(27, 42, 32, 0.25);
99
+ --backdrop: rgba(27, 42, 32, 0.4);
100
+ --chart-series: #1f7a4a;
101
+ --chart-area: rgba(31, 122, 74, 0.1);
102
+ --chart-grid: rgba(31, 122, 74, 0.1);
103
+ --chart-baseline: rgba(31, 122, 74, 0.35);
104
+ --map-0: #ebe5d2;
105
+ --map-1: #7cc298;
106
+ --map-2: #52ab79;
107
+ --map-3: #33935f;
108
+ --map-4: #1f7a4a;
109
+ --map-5: #11532f;
110
+ color-scheme: light;
111
+ }
112
+
113
+ /* ---------- Base ---------- */
114
+
115
+ *,
116
+ *::before,
117
+ *::after {
118
+ margin: 0;
119
+ padding: 0;
120
+ box-sizing: border-box;
121
+ }
122
+
123
+ html {
124
+ background-color: var(--bg);
125
+ }
126
+
127
+ body {
128
+ font-family: var(--font-mono);
129
+ font-size: 14px;
130
+ background-color: var(--bg);
131
+ color: var(--text);
132
+ line-height: 1.5;
133
+ min-height: 100vh;
134
+ transition: background-color 0.3s, color 0.3s;
135
+ }
136
+
137
+ body.booting main {
138
+ visibility: hidden;
139
+ }
140
+
141
+ body.modal-open {
142
+ overflow: hidden;
143
+ }
144
+
145
+ h1, h2, h3 {
146
+ font-weight: 700;
147
+ letter-spacing: -0.01em;
148
+ }
149
+
150
+ button,
151
+ input,
152
+ textarea {
153
+ font: inherit;
154
+ color: inherit;
155
+ }
156
+
157
+ [hidden] {
158
+ display: none !important;
159
+ }
160
+
161
+ .sr-only {
162
+ position: absolute;
163
+ width: 1px;
164
+ height: 1px;
165
+ overflow: hidden;
166
+ clip: rect(0 0 0 0);
167
+ white-space: nowrap;
168
+ }
169
+
170
+ .mono {
171
+ font-family: var(--font-mono);
172
+ }
173
+
174
+ :focus-visible {
175
+ outline: 2px solid var(--primary);
176
+ outline-offset: 2px;
177
+ }
178
+
179
+ .skip-link {
180
+ position: absolute;
181
+ left: 1rem;
182
+ top: -3rem;
183
+ z-index: 300;
184
+ padding: 0.5rem 0.9rem;
185
+ background: var(--primary);
186
+ color: var(--text-on-primary);
187
+ border-radius: var(--radius-small);
188
+ text-decoration: none;
189
+ }
190
+
191
+ .skip-link:focus {
192
+ top: 1rem;
193
+ }
194
+
195
+ /* ---------- Ambient background (as on the docs site) ---------- */
196
+
197
+ .bg-grid {
198
+ position: fixed;
199
+ inset: 0;
200
+ z-index: -2;
201
+ pointer-events: none;
202
+ background-image:
203
+ linear-gradient(var(--border) 1px, transparent 1px),
204
+ linear-gradient(90deg, var(--border) 1px, transparent 1px);
205
+ background-size: 48px 48px;
206
+ opacity: 0.35;
207
+ mask-image: radial-gradient(ellipse 80% 55% at 50% 0%, black 30%, transparent 100%);
208
+ }
209
+
210
+ .bg-glow {
211
+ position: fixed;
212
+ top: -10%;
213
+ left: 50%;
214
+ transform: translateX(-50%);
215
+ width: min(900px, 100vw);
216
+ height: 500px;
217
+ background: radial-gradient(ellipse at center, var(--primary-glow) 0%, transparent 70%);
218
+ z-index: -1;
219
+ pointer-events: none;
220
+ }
221
+
222
+ .crt-overlay {
223
+ position: fixed;
224
+ inset: 0;
225
+ z-index: 250;
226
+ pointer-events: none;
227
+ background: repeating-linear-gradient(to bottom,
228
+ rgba(255, 255, 255, 0.05) 0px,
229
+ rgba(255, 255, 255, 0.05) 1px,
230
+ transparent 1px,
231
+ transparent 3px);
232
+ opacity: var(--scanline-opacity);
233
+ mix-blend-mode: overlay;
234
+ }
235
+
236
+ /* ---------- Top bar ---------- */
237
+
238
+ .topbar {
239
+ position: sticky;
240
+ top: 0;
241
+ z-index: 100;
242
+ display: flex;
243
+ align-items: center;
244
+ justify-content: space-between;
245
+ gap: 1rem;
246
+ padding: 1rem 2rem;
247
+ background: var(--nav-bg);
248
+ backdrop-filter: blur(10px);
249
+ border-bottom: 1px solid var(--border);
250
+ }
251
+
252
+ .logo {
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 0.55rem;
256
+ }
257
+
258
+ .logo-mark {
259
+ height: 24px;
260
+ width: auto;
261
+ color: var(--primary);
262
+ }
263
+
264
+ .logo-cursor {
265
+ animation: blink 1.1s steps(1) infinite;
266
+ }
267
+
268
+ .logo-text {
269
+ font-weight: 700;
270
+ font-size: 1.05rem;
271
+ }
272
+
273
+ .logo-badge {
274
+ font-size: 0.72rem;
275
+ font-weight: 700;
276
+ text-transform: uppercase;
277
+ letter-spacing: 0.08em;
278
+ color: var(--secondary);
279
+ border: 1px solid currentColor;
280
+ border-radius: 4px;
281
+ padding: 0.05rem 0.4rem;
282
+ }
283
+
284
+ .topbar-actions {
285
+ display: flex;
286
+ align-items: center;
287
+ gap: 0.75rem;
288
+ }
289
+
290
+ .theme-switcher {
291
+ display: flex;
292
+ background: var(--code-bg);
293
+ border: 1px solid var(--border);
294
+ padding: 0.2rem;
295
+ border-radius: var(--radius-small);
296
+ gap: 0.2rem;
297
+ }
298
+
299
+ .theme-btn {
300
+ background: transparent;
301
+ border: none;
302
+ color: var(--text-muted);
303
+ width: 30px;
304
+ height: 30px;
305
+ border-radius: 5px;
306
+ display: flex;
307
+ align-items: center;
308
+ justify-content: center;
309
+ cursor: pointer;
310
+ transition: all 0.2s;
311
+ }
312
+
313
+ .theme-btn:hover {
314
+ color: var(--text);
315
+ background: var(--border);
316
+ }
317
+
318
+ .theme-btn.active {
319
+ background: var(--primary);
320
+ color: var(--bg);
321
+ box-shadow: 0 0 12px var(--primary-glow);
322
+ }
323
+
324
+ /* ---------- Layout ---------- */
325
+
326
+ main {
327
+ max-width: 1440px;
328
+ margin: 0 auto;
329
+ padding: 1.75rem 2rem 6rem;
330
+ }
331
+
332
+ /* ---------- Terminal window chrome ---------- */
333
+
334
+ .term-window {
335
+ background: var(--card-bg);
336
+ border: 1px solid var(--border);
337
+ border-radius: var(--radius);
338
+ overflow: hidden;
339
+ backdrop-filter: blur(10px);
340
+ box-shadow: var(--shadow);
341
+ }
342
+
343
+ .term-titlebar {
344
+ display: flex;
345
+ align-items: center;
346
+ gap: 0.6rem;
347
+ padding: 0.65rem 1rem;
348
+ background: var(--code-bg);
349
+ border-bottom: 1px solid var(--border);
350
+ }
351
+
352
+ .term-dots {
353
+ display: flex;
354
+ gap: 0.4rem;
355
+ }
356
+
357
+ .term-dot {
358
+ width: 10px;
359
+ height: 10px;
360
+ border-radius: 50%;
361
+ }
362
+
363
+ .term-dot.red { background: #ef4444; }
364
+ .term-dot.amber { background: #f5b942; }
365
+ .term-dot.green { background: var(--primary); }
366
+
367
+ .term-title {
368
+ font-size: 0.78rem;
369
+ color: var(--text-muted);
370
+ margin-left: 0.25rem;
371
+ }
372
+
373
+ .term-body {
374
+ padding: 1.75rem;
375
+ }
376
+
377
+ .prompt-char {
378
+ color: var(--primary);
379
+ font-weight: 700;
380
+ }
381
+
382
+ /* ---------- Buttons ---------- */
383
+
384
+ .btn {
385
+ display: inline-flex;
386
+ align-items: center;
387
+ justify-content: center;
388
+ gap: 0.5rem;
389
+ padding: 0.7rem 1.3rem;
390
+ border-radius: var(--radius-small);
391
+ font-weight: 600;
392
+ font-size: 0.9rem;
393
+ border: 1px solid transparent;
394
+ cursor: pointer;
395
+ transition: transform 0.15s, box-shadow 0.2s, border-color 0.2s, color 0.2s, background-color 0.2s;
396
+ white-space: nowrap;
397
+ }
398
+
399
+ .btn:disabled {
400
+ opacity: 0.5;
401
+ cursor: not-allowed;
402
+ transform: none;
403
+ }
404
+
405
+ .btn-small {
406
+ padding: 0.4rem 0.85rem;
407
+ font-size: 0.82rem;
408
+ }
409
+
410
+ .btn-primary {
411
+ background: var(--primary);
412
+ color: var(--text-on-primary);
413
+ }
414
+
415
+ .btn-primary:hover:not(:disabled) {
416
+ transform: translateY(-1px);
417
+ box-shadow: 0 8px 24px var(--primary-glow);
418
+ }
419
+
420
+ .btn-secondary {
421
+ background: transparent;
422
+ color: var(--text);
423
+ border-color: var(--border-strong);
424
+ }
425
+
426
+ .btn-secondary:hover:not(:disabled) {
427
+ border-color: var(--primary);
428
+ color: var(--primary);
429
+ }
430
+
431
+ .btn-danger {
432
+ background: var(--danger-bg);
433
+ color: var(--danger);
434
+ border-color: var(--danger);
435
+ }
436
+
437
+ .btn-danger:hover:not(:disabled) {
438
+ background: var(--danger);
439
+ color: #fff;
440
+ }
441
+
442
+ .btn-ghost {
443
+ background: transparent;
444
+ color: var(--text-muted);
445
+ }
446
+
447
+ .btn-ghost:hover:not(:disabled) {
448
+ color: var(--text);
449
+ }
450
+
451
+ .icon-btn {
452
+ display: inline-flex;
453
+ align-items: center;
454
+ justify-content: center;
455
+ width: 28px;
456
+ height: 28px;
457
+ border-radius: 6px;
458
+ border: 1px solid transparent;
459
+ background: transparent;
460
+ color: var(--text-muted);
461
+ cursor: pointer;
462
+ font-size: 0.95rem;
463
+ line-height: 1;
464
+ transition: all 0.15s;
465
+ }
466
+
467
+ .icon-btn:hover {
468
+ color: var(--primary);
469
+ border-color: var(--border);
470
+ background: var(--row-hover);
471
+ }
472
+
473
+ .icon-btn.danger:hover {
474
+ color: var(--danger);
475
+ border-color: var(--danger);
476
+ background: var(--danger-bg);
477
+ }
478
+
479
+ /* ---------- Form controls ---------- */
480
+
481
+ .input {
482
+ width: 100%;
483
+ padding: 0.6rem 0.8rem;
484
+ background: var(--code-bg);
485
+ border: 1px solid var(--border);
486
+ border-radius: var(--radius-small);
487
+ transition: border-color 0.2s, box-shadow 0.2s;
488
+ }
489
+
490
+ .input:focus {
491
+ outline: none;
492
+ border-color: var(--primary);
493
+ box-shadow: 0 0 0 3px var(--primary-glow);
494
+ }
495
+
496
+ textarea.input {
497
+ resize: vertical;
498
+ min-height: 5rem;
499
+ }
500
+
501
+ .field-label {
502
+ display: block;
503
+ font-size: 0.82rem;
504
+ color: var(--text-muted);
505
+ margin-bottom: 0.4rem;
506
+ }
507
+
508
+ .field-error {
509
+ color: var(--danger);
510
+ font-size: 0.82rem;
511
+ margin-top: 0.4rem;
512
+ }
513
+
514
+ .notice {
515
+ font-size: 0.82rem;
516
+ color: var(--text-muted);
517
+ border-left: 2px solid var(--secondary);
518
+ padding: 0.2rem 0 0.2rem 0.75rem;
519
+ margin: 0.25rem 0 1rem;
520
+ }
521
+
522
+ /* Custom listbox (FRONTEND.md §0: never a native select) */
523
+
524
+ .listbox {
525
+ position: relative;
526
+ min-width: 10rem;
527
+ }
528
+
529
+ .listbox-button {
530
+ width: 100%;
531
+ display: flex;
532
+ align-items: center;
533
+ justify-content: space-between;
534
+ gap: 0.5rem;
535
+ padding: 0.55rem 0.8rem;
536
+ background: var(--code-bg);
537
+ border: 1px solid var(--border);
538
+ border-radius: var(--radius-small);
539
+ cursor: pointer;
540
+ text-align: left;
541
+ }
542
+
543
+ .listbox-button:hover,
544
+ .listbox-button[aria-expanded='true'] {
545
+ border-color: var(--primary);
546
+ }
547
+
548
+ .listbox-value {
549
+ overflow: hidden;
550
+ text-overflow: ellipsis;
551
+ white-space: nowrap;
552
+ }
553
+
554
+ .listbox-caret {
555
+ color: var(--primary);
556
+ font-size: 0.75rem;
557
+ }
558
+
559
+ .listbox-popup {
560
+ position: absolute;
561
+ z-index: 150;
562
+ top: calc(100% + 4px);
563
+ left: 0;
564
+ min-width: 100%;
565
+ max-height: 18rem;
566
+ overflow-y: auto;
567
+ list-style: none;
568
+ padding: 0.3rem;
569
+ background: var(--surface-solid);
570
+ border: 1px solid var(--border-strong);
571
+ border-radius: var(--radius-small);
572
+ box-shadow: var(--shadow);
573
+ animation: pop-in 0.12s ease-out;
574
+ }
575
+
576
+ .listbox-popup:focus {
577
+ outline: none;
578
+ }
579
+
580
+ .listbox-option {
581
+ padding: 0.45rem 0.7rem;
582
+ border-radius: 5px;
583
+ cursor: pointer;
584
+ white-space: nowrap;
585
+ }
586
+
587
+ .listbox-option::before {
588
+ content: ' ';
589
+ white-space: pre;
590
+ color: var(--primary);
591
+ }
592
+
593
+ .listbox-option[aria-selected='true']::before {
594
+ content: '❯ ';
595
+ }
596
+
597
+ .listbox-option.active,
598
+ .listbox-option:hover {
599
+ background: var(--row-selected);
600
+ }
601
+
602
+ /* Segmented control */
603
+
604
+ .segmented {
605
+ display: inline-flex;
606
+ flex-wrap: wrap;
607
+ max-width: 100%;
608
+ background: var(--code-bg);
609
+ border: 1px solid var(--border);
610
+ border-radius: var(--radius-small);
611
+ padding: 0.2rem;
612
+ gap: 0.2rem;
613
+ }
614
+
615
+ .segmented-btn {
616
+ border: none;
617
+ background: transparent;
618
+ color: var(--text-muted);
619
+ padding: 0.35rem 0.75rem;
620
+ border-radius: 5px;
621
+ cursor: pointer;
622
+ font-size: 0.82rem;
623
+ white-space: nowrap;
624
+ }
625
+
626
+ .segmented-btn:hover {
627
+ color: var(--text);
628
+ }
629
+
630
+ .segmented-btn.active {
631
+ background: var(--primary);
632
+ color: var(--text-on-primary);
633
+ }
634
+
635
+ /* Switch */
636
+
637
+ .switch {
638
+ display: inline-flex;
639
+ align-items: center;
640
+ gap: 0.55rem;
641
+ cursor: pointer;
642
+ white-space: nowrap;
643
+ font-size: 0.85rem;
644
+ }
645
+
646
+ .switch-input {
647
+ position: absolute;
648
+ opacity: 0;
649
+ width: 1px;
650
+ height: 1px;
651
+ }
652
+
653
+ .switch-track {
654
+ position: relative;
655
+ width: 34px;
656
+ height: 20px;
657
+ border-radius: 10px;
658
+ background: var(--code-bg);
659
+ border: 1px solid var(--border-strong);
660
+ transition: background-color 0.2s;
661
+ }
662
+
663
+ .switch-track::after {
664
+ content: '';
665
+ position: absolute;
666
+ top: 2px;
667
+ left: 2px;
668
+ width: 14px;
669
+ height: 14px;
670
+ border-radius: 50%;
671
+ background: var(--text-muted);
672
+ transition: transform 0.2s, background-color 0.2s;
673
+ }
674
+
675
+ .switch-input:checked + .switch-track {
676
+ background: var(--primary-glow);
677
+ border-color: var(--primary);
678
+ }
679
+
680
+ .switch-input:checked + .switch-track::after {
681
+ transform: translateX(14px);
682
+ background: var(--primary);
683
+ }
684
+
685
+ .switch-input:focus-visible + .switch-track {
686
+ outline: 2px solid var(--primary);
687
+ outline-offset: 2px;
688
+ }
689
+
690
+ /* ---------- Login ---------- */
691
+
692
+ .login {
693
+ display: flex;
694
+ justify-content: center;
695
+ padding-top: 8vh;
696
+ }
697
+
698
+ .login-window {
699
+ width: min(460px, 100%);
700
+ }
701
+
702
+ .login-form {
703
+ display: flex;
704
+ flex-direction: column;
705
+ gap: 0.25rem;
706
+ }
707
+
708
+ .login-heading {
709
+ font-size: 1.25rem;
710
+ display: flex;
711
+ gap: 0.5rem;
712
+ margin-bottom: 0.4rem;
713
+ }
714
+
715
+ .login-intro {
716
+ color: var(--text-muted);
717
+ font-size: 0.85rem;
718
+ margin-bottom: 1.2rem;
719
+ }
720
+
721
+ .login-form .btn {
722
+ margin-top: 1.1rem;
723
+ }
724
+
725
+ /* ---------- Tabs ---------- */
726
+
727
+ .tabs {
728
+ display: flex;
729
+ gap: 0.25rem;
730
+ border-bottom: 1px solid var(--border);
731
+ margin-bottom: 1.25rem;
732
+ }
733
+
734
+ .tab {
735
+ display: inline-flex;
736
+ gap: 0.4rem;
737
+ align-items: center;
738
+ padding: 0.7rem 1rem;
739
+ border: none;
740
+ border-bottom: 2px solid transparent;
741
+ background: transparent;
742
+ color: var(--text-muted);
743
+ cursor: pointer;
744
+ font-weight: 600;
745
+ margin-bottom: -1px;
746
+ }
747
+
748
+ .tab .prompt-char {
749
+ opacity: 0.4;
750
+ }
751
+
752
+ .tab:hover {
753
+ color: var(--text);
754
+ }
755
+
756
+ .tab.active {
757
+ color: var(--primary);
758
+ border-bottom-color: var(--primary);
759
+ }
760
+
761
+ .tab.active .prompt-char {
762
+ opacity: 1;
763
+ }
764
+
765
+ /* ---------- Panels and toolbar ---------- */
766
+
767
+ .panel {
768
+ display: flex;
769
+ flex-direction: column;
770
+ gap: 0.9rem;
771
+ }
772
+
773
+ .toolbar {
774
+ display: flex;
775
+ flex-wrap: wrap;
776
+ align-items: center;
777
+ justify-content: space-between;
778
+ gap: 0.75rem;
779
+ }
780
+
781
+ .toolbar-group {
782
+ display: flex;
783
+ flex-wrap: wrap;
784
+ align-items: center;
785
+ gap: 0.6rem;
786
+ }
787
+
788
+ .search-input {
789
+ width: 20rem;
790
+ max-width: 100%;
791
+ }
792
+
793
+ .app-tabs {
794
+ display: flex;
795
+ flex-wrap: wrap;
796
+ gap: 0.5rem;
797
+ }
798
+
799
+ .app-tab {
800
+ display: inline-flex;
801
+ align-items: center;
802
+ gap: 0.45rem;
803
+ padding: 0.45rem 0.8rem;
804
+ background: var(--code-bg);
805
+ border: 1px solid var(--border);
806
+ border-radius: var(--radius-small);
807
+ color: var(--text-muted);
808
+ cursor: pointer;
809
+ font-weight: 600;
810
+ transition: color 0.15s, border-color 0.15s, background-color 0.15s, box-shadow 0.2s;
811
+ }
812
+
813
+ .app-tab .prompt-char {
814
+ opacity: 0.4;
815
+ }
816
+
817
+ .app-tab:hover {
818
+ color: var(--text);
819
+ border-color: var(--border-strong);
820
+ }
821
+
822
+ .app-tab.active {
823
+ color: var(--primary);
824
+ border-color: var(--primary);
825
+ background: var(--row-selected);
826
+ box-shadow: 0 0 12px var(--primary-glow);
827
+ }
828
+
829
+ .app-tab.active .prompt-char {
830
+ opacity: 1;
831
+ }
832
+
833
+ .app-tab.unavailable {
834
+ opacity: 0.55;
835
+ text-decoration: line-through;
836
+ }
837
+
838
+ .app-tab-count {
839
+ font-size: 0.72rem;
840
+ font-weight: 700;
841
+ padding: 0.05rem 0.45rem;
842
+ border-radius: 999px;
843
+ background: var(--muted-bg);
844
+ color: var(--text-muted);
845
+ }
846
+
847
+ .app-tab.active .app-tab-count {
848
+ background: var(--primary);
849
+ color: var(--text-on-primary);
850
+ }
851
+
852
+ .panel-summary {
853
+ color: var(--text-muted);
854
+ font-size: 0.85rem;
855
+ }
856
+
857
+ /* ---------- Insights ---------- */
858
+
859
+ .insights {
860
+ display: flex;
861
+ flex-direction: column;
862
+ gap: 0.9rem;
863
+ transition: opacity 0.2s;
864
+ }
865
+
866
+ /* Refetch keeps the frame: the last render stays, dimmed, until the new one. */
867
+ .insights.loading .insights-body {
868
+ opacity: 0.55;
869
+ }
870
+
871
+ .insights-header {
872
+ display: flex;
873
+ align-items: baseline;
874
+ gap: 0.9rem;
875
+ flex-wrap: wrap;
876
+ }
877
+
878
+ .insights-toggle {
879
+ display: inline-flex;
880
+ align-items: center;
881
+ gap: 0.45rem;
882
+ border: none;
883
+ background: none;
884
+ cursor: pointer;
885
+ font-weight: 700;
886
+ font-size: 1rem;
887
+ padding: 0;
888
+ }
889
+
890
+ .insights-caret {
891
+ color: var(--text-muted);
892
+ font-size: 0.8rem;
893
+ transition: transform 0.2s;
894
+ }
895
+
896
+ .insights-toggle[aria-expanded='false'] .insights-caret {
897
+ transform: rotate(-90deg);
898
+ }
899
+
900
+ .insights-caption,
901
+ .insights-span {
902
+ color: var(--text-muted);
903
+ font-size: 0.82rem;
904
+ }
905
+
906
+ .insights-body {
907
+ display: flex;
908
+ flex-direction: column;
909
+ gap: 1rem;
910
+ }
911
+
912
+ .stat-tiles {
913
+ display: grid;
914
+ grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
915
+ gap: 0.75rem;
916
+ }
917
+
918
+ .stat-tile {
919
+ padding: 0.85rem 1rem;
920
+ background: var(--card-bg);
921
+ border: 1px solid var(--border);
922
+ border-radius: var(--radius-small);
923
+ }
924
+
925
+ .stat-label {
926
+ color: var(--text-muted);
927
+ font-size: 0.78rem;
928
+ }
929
+
930
+ .stat-value {
931
+ font-size: 1.6rem;
932
+ font-weight: 700;
933
+ letter-spacing: -0.02em;
934
+ }
935
+
936
+ .insights-grid {
937
+ display: grid;
938
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr);
939
+ gap: 1rem;
940
+ }
941
+
942
+ .breakdown-grid {
943
+ display: grid;
944
+ grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
945
+ gap: 1rem;
946
+ }
947
+
948
+ .chart-card {
949
+ position: relative;
950
+ display: flex;
951
+ flex-direction: column;
952
+ gap: 0.6rem;
953
+ padding: 1rem 1.1rem;
954
+ background: var(--card-bg);
955
+ border: 1px solid var(--border);
956
+ border-radius: var(--radius);
957
+ min-width: 0;
958
+ }
959
+
960
+ .chart-card-header {
961
+ display: flex;
962
+ align-items: center;
963
+ justify-content: space-between;
964
+ gap: 0.75rem;
965
+ }
966
+
967
+ .chart-title {
968
+ font-size: 0.85rem;
969
+ font-weight: 700;
970
+ }
971
+
972
+ .chart-body {
973
+ position: relative;
974
+ min-width: 0;
975
+ }
976
+
977
+ .chart-empty {
978
+ color: var(--text-muted);
979
+ font-size: 0.85rem;
980
+ padding: 1.5rem 0;
981
+ text-align: center;
982
+ list-style: none;
983
+ }
984
+
985
+ /* Views over time */
986
+
987
+ .trend-svg {
988
+ display: block;
989
+ width: 100%;
990
+ height: auto;
991
+ overflow: visible;
992
+ }
993
+
994
+ .trend-svg:focus-visible {
995
+ outline: 2px solid var(--primary);
996
+ outline-offset: 4px;
997
+ border-radius: 4px;
998
+ }
999
+
1000
+ .chart-grid {
1001
+ stroke: var(--chart-grid);
1002
+ stroke-width: 1;
1003
+ shape-rendering: crispEdges;
1004
+ }
1005
+
1006
+ .chart-baseline {
1007
+ stroke: var(--chart-baseline);
1008
+ stroke-width: 1;
1009
+ shape-rendering: crispEdges;
1010
+ }
1011
+
1012
+ .chart-tick {
1013
+ fill: var(--text-muted);
1014
+ font-size: 11px;
1015
+ font-variant-numeric: tabular-nums;
1016
+ }
1017
+
1018
+ .chart-area {
1019
+ fill: var(--chart-area);
1020
+ }
1021
+
1022
+ .chart-line {
1023
+ fill: none;
1024
+ stroke: var(--chart-series);
1025
+ stroke-width: 2;
1026
+ stroke-linejoin: round;
1027
+ stroke-linecap: round;
1028
+ }
1029
+
1030
+ /* >= 8px across, with a 2px ring in the surface colour. */
1031
+ .chart-dot {
1032
+ fill: var(--chart-series);
1033
+ stroke: var(--surface-solid);
1034
+ stroke-width: 2;
1035
+ }
1036
+
1037
+ .chart-crosshair {
1038
+ stroke: var(--text-muted);
1039
+ stroke-width: 1;
1040
+ shape-rendering: crispEdges;
1041
+ }
1042
+
1043
+ .chart-table-wrap {
1044
+ max-height: 16rem;
1045
+ overflow-y: auto;
1046
+ }
1047
+
1048
+ .chart-table {
1049
+ width: 100%;
1050
+ border-collapse: collapse;
1051
+ font-size: 0.82rem;
1052
+ font-variant-numeric: tabular-nums;
1053
+ }
1054
+
1055
+ .chart-table th,
1056
+ .chart-table td {
1057
+ text-align: left;
1058
+ padding: 0.3rem 0.5rem;
1059
+ border-bottom: 1px solid var(--border);
1060
+ }
1061
+
1062
+ .chart-table th {
1063
+ color: var(--text-muted);
1064
+ font-weight: 700;
1065
+ }
1066
+
1067
+ /* Tooltip: values lead, labels follow, keyed with a short stroke. */
1068
+
1069
+ .chart-tooltip {
1070
+ position: absolute;
1071
+ z-index: 20;
1072
+ pointer-events: none;
1073
+ min-width: 9rem;
1074
+ padding: 0.55rem 0.7rem;
1075
+ background: var(--surface-solid);
1076
+ border: 1px solid var(--border-strong);
1077
+ border-radius: var(--radius-small);
1078
+ box-shadow: var(--shadow);
1079
+ font-size: 0.8rem;
1080
+ }
1081
+
1082
+ .chart-tooltip-title {
1083
+ color: var(--text-muted);
1084
+ margin-bottom: 0.25rem;
1085
+ }
1086
+
1087
+ .chart-tooltip-row {
1088
+ display: flex;
1089
+ align-items: center;
1090
+ gap: 0.45rem;
1091
+ }
1092
+
1093
+ .chart-tooltip-row strong {
1094
+ color: var(--text);
1095
+ font-variant-numeric: tabular-nums;
1096
+ }
1097
+
1098
+ .chart-tooltip-row span:last-child {
1099
+ color: var(--text-muted);
1100
+ }
1101
+
1102
+ .chart-tooltip-key {
1103
+ width: 0.8rem;
1104
+ height: 2px;
1105
+ background: var(--chart-series);
1106
+ flex-shrink: 0;
1107
+ }
1108
+
1109
+ /* Breakdown bars and the country list */
1110
+
1111
+ .bar-list,
1112
+ .country-list {
1113
+ display: flex;
1114
+ flex-direction: column;
1115
+ gap: 0.35rem;
1116
+ list-style: none;
1117
+ }
1118
+
1119
+ .bar-row,
1120
+ .country-row {
1121
+ display: grid;
1122
+ grid-template-columns: minmax(0, 7.5rem) minmax(0, 1fr) auto;
1123
+ align-items: center;
1124
+ gap: 0.6rem;
1125
+ font-size: 0.82rem;
1126
+ padding: 0.1rem 0.25rem;
1127
+ border-radius: 4px;
1128
+ cursor: default;
1129
+ }
1130
+
1131
+ .bar-row:hover,
1132
+ .bar-row:focus-visible,
1133
+ .country-row:hover,
1134
+ .country-row:focus-visible {
1135
+ background: var(--row-hover);
1136
+ }
1137
+
1138
+ .bar-other .bar-label {
1139
+ color: var(--text-muted);
1140
+ }
1141
+
1142
+ .bar-track {
1143
+ display: block;
1144
+ height: 10px;
1145
+ }
1146
+
1147
+ .bar-fill {
1148
+ display: block;
1149
+ height: 100%;
1150
+ min-width: 2px;
1151
+ background: var(--chart-series);
1152
+ border-radius: 0 4px 4px 0;
1153
+ }
1154
+
1155
+ .bar-other .bar-fill {
1156
+ background: var(--text-muted);
1157
+ opacity: 0.6;
1158
+ }
1159
+
1160
+ .bar-value {
1161
+ font-variant-numeric: tabular-nums;
1162
+ color: var(--text);
1163
+ text-align: right;
1164
+ min-width: 2.5rem;
1165
+ }
1166
+
1167
+ /* World map */
1168
+
1169
+ .map-layout {
1170
+ display: grid;
1171
+ grid-template-columns: minmax(0, 1fr) minmax(11rem, 14rem);
1172
+ gap: 1rem;
1173
+ align-items: start;
1174
+ }
1175
+
1176
+ .map-column {
1177
+ display: flex;
1178
+ flex-direction: column;
1179
+ gap: 0.6rem;
1180
+ min-width: 0;
1181
+ }
1182
+
1183
+ .map-host {
1184
+ position: relative;
1185
+ }
1186
+
1187
+ .map-svg {
1188
+ display: block;
1189
+ width: 100%;
1190
+ height: auto;
1191
+ }
1192
+
1193
+ /* Borders are gaps in the surface colour, not drawn outlines. */
1194
+ .country {
1195
+ stroke: var(--surface-solid);
1196
+ stroke-width: 0.5;
1197
+ transition: opacity 0.15s;
1198
+ }
1199
+
1200
+ .country-point {
1201
+ stroke-width: 2;
1202
+ }
1203
+
1204
+ .country.active {
1205
+ stroke: var(--text);
1206
+ stroke-width: 1;
1207
+ }
1208
+
1209
+ .map-0 { fill: var(--map-0); background: var(--map-0); }
1210
+ .map-1 { fill: var(--map-1); background: var(--map-1); }
1211
+ .map-2 { fill: var(--map-2); background: var(--map-2); }
1212
+ .map-3 { fill: var(--map-3); background: var(--map-3); }
1213
+ .map-4 { fill: var(--map-4); background: var(--map-4); }
1214
+ .map-5 { fill: var(--map-5); background: var(--map-5); }
1215
+
1216
+ .map-legend {
1217
+ display: flex;
1218
+ flex-wrap: wrap;
1219
+ gap: 0.4rem 0.9rem;
1220
+ font-size: 0.75rem;
1221
+ color: var(--text-muted);
1222
+ }
1223
+
1224
+ .legend-item {
1225
+ display: inline-flex;
1226
+ align-items: center;
1227
+ gap: 0.35rem;
1228
+ }
1229
+
1230
+ .legend-swatch {
1231
+ width: 0.85rem;
1232
+ height: 0.6rem;
1233
+ border-radius: 2px;
1234
+ }
1235
+
1236
+ .country-name {
1237
+ color: var(--text);
1238
+ }
1239
+
1240
+ .country-more {
1241
+ padding: 0.2rem 0.25rem;
1242
+ }
1243
+
1244
+ .country-more .btn {
1245
+ padding: 0.25rem 0.5rem;
1246
+ }
1247
+
1248
+ .event-type-picker {
1249
+ min-width: 11rem;
1250
+ }
1251
+
1252
+ /* Batch bar */
1253
+
1254
+ .batch-bar {
1255
+ position: sticky;
1256
+ top: 4.2rem;
1257
+ z-index: 50;
1258
+ display: flex;
1259
+ flex-wrap: wrap;
1260
+ align-items: center;
1261
+ gap: 0.75rem;
1262
+ padding: 0.6rem 1rem;
1263
+ background: var(--surface-solid);
1264
+ border: 1px solid var(--primary);
1265
+ border-radius: var(--radius-small);
1266
+ box-shadow: 0 0 18px var(--primary-glow);
1267
+ animation: pop-in 0.15s ease-out;
1268
+ }
1269
+
1270
+ .batch-count {
1271
+ font-weight: 700;
1272
+ }
1273
+
1274
+ .batch-actions {
1275
+ display: flex;
1276
+ flex-wrap: wrap;
1277
+ gap: 0.5rem;
1278
+ margin-left: auto;
1279
+ }
1280
+
1281
+ /* ---------- Data table ---------- */
1282
+
1283
+ .table-wrap {
1284
+ background: var(--card-bg);
1285
+ border: 1px solid var(--border);
1286
+ border-radius: var(--radius);
1287
+ backdrop-filter: blur(10px);
1288
+ }
1289
+
1290
+ .data-table {
1291
+ width: 100%;
1292
+ border-collapse: collapse;
1293
+ table-layout: fixed;
1294
+ }
1295
+
1296
+ .data-table th,
1297
+ .data-table td {
1298
+ padding: 0.55rem 0.7rem;
1299
+ text-align: left;
1300
+ vertical-align: middle;
1301
+ border-bottom: 1px solid var(--border);
1302
+ }
1303
+
1304
+ .data-table thead th {
1305
+ font-size: 0.72rem;
1306
+ text-transform: uppercase;
1307
+ letter-spacing: 0.06em;
1308
+ color: var(--text-muted);
1309
+ background: var(--code-bg);
1310
+ font-weight: 700;
1311
+ }
1312
+
1313
+ .data-table thead th:first-child {
1314
+ border-top-left-radius: var(--radius);
1315
+ }
1316
+
1317
+ .data-table thead th:last-child {
1318
+ border-top-right-radius: var(--radius);
1319
+ }
1320
+
1321
+ .data-table tbody tr:last-child td {
1322
+ /* Transparent rather than removed, so the last row is exactly as tall as
1323
+ every other (FRONTEND.md §14). */
1324
+ border-bottom-color: transparent;
1325
+ }
1326
+
1327
+ .data-table tbody tr {
1328
+ height: 3.6rem;
1329
+ transition: background-color 0.15s;
1330
+ }
1331
+
1332
+ .data-table tbody tr:hover {
1333
+ background: var(--row-hover);
1334
+ }
1335
+
1336
+ .data-table tbody tr:has(input[type='checkbox']:checked) {
1337
+ background: var(--row-selected);
1338
+ }
1339
+
1340
+ .data-table[aria-busy='true'] tbody {
1341
+ opacity: 0.55;
1342
+ }
1343
+
1344
+ .row-deleted td:not(.col-select):not(.col-actions):not(.col-status) {
1345
+ opacity: 0.55;
1346
+ text-decoration: line-through;
1347
+ text-decoration-color: var(--danger);
1348
+ }
1349
+
1350
+ .table-message {
1351
+ text-align: center !important;
1352
+ color: var(--text-muted);
1353
+ padding: 2.5rem 1rem !important;
1354
+ }
1355
+
1356
+ .sort-btn {
1357
+ display: inline-flex;
1358
+ align-items: center;
1359
+ gap: 0.35rem;
1360
+ border: none;
1361
+ background: none;
1362
+ color: inherit;
1363
+ font: inherit;
1364
+ text-transform: inherit;
1365
+ letter-spacing: inherit;
1366
+ cursor: pointer;
1367
+ padding: 0;
1368
+ }
1369
+
1370
+ .sort-btn:hover,
1371
+ .sort-btn.active {
1372
+ color: var(--primary);
1373
+ }
1374
+
1375
+ .sort-indicator {
1376
+ font-size: 0.7rem;
1377
+ opacity: 0.8;
1378
+ }
1379
+
1380
+ input[type='checkbox'] {
1381
+ width: 16px;
1382
+ height: 16px;
1383
+ accent-color: var(--primary);
1384
+ cursor: pointer;
1385
+ }
1386
+
1387
+ .col-select { width: 2.6rem; }
1388
+ .col-time { width: 8.5rem; }
1389
+ .col-app { width: 8rem; }
1390
+ .col-page { width: auto; }
1391
+ .col-source { width: 9.5rem; }
1392
+ .col-device { width: 6.5rem; }
1393
+ .col-country { width: 5.5rem; }
1394
+ .col-client { width: 8rem; }
1395
+ .col-event { width: 7rem; }
1396
+ .col-status { width: 10.5rem; }
1397
+ .col-actions { width: 8.5rem; }
1398
+
1399
+ .log-table .col-action { width: 11rem; }
1400
+ .log-table .col-app { width: 9rem; }
1401
+ .log-table .col-rows { width: 5rem; }
1402
+ .log-table .col-fields { width: auto; }
1403
+ .log-table .col-session { width: 7rem; }
1404
+ .log-table .col-ip { width: 11rem; }
1405
+ .log-table .col-source { width: 8rem; }
1406
+ .log-table .col-event { width: 9rem; }
1407
+ .log-table .col-unique { width: 6rem; }
1408
+ .log-table .col-view { width: auto; }
1409
+
1410
+ /*
1411
+ * Reflow (FRONTEND.md §12). A table answers to its own width, not the
1412
+ * viewport's: it drops optional columns, least useful first, and becomes cards
1413
+ * where even its essential columns no longer fit (views below 53rem, logs
1414
+ * below 46rem). Each threshold is the width at which the columns still shown
1415
+ * leave the flexible column (page, fields, or view) at least 13rem:
1416
+ *
1417
+ * views, one app always 39.6 (select, time, source, status, actions);
1418
+ * + event 7 from 60, country 5.5 from 65.5,
1419
+ * device 6.5 from 72, client 8 from 80
1420
+ * views, All apps the same plus app 8, so each step comes 8rem later;
1421
+ * source also steps aside between the cards and 61
1422
+ * admin log always 45.5; + ip 11 from 57, session 7 from 64
1423
+ * view log always 40.5; + view ID 12 from 53
1424
+ */
1425
+ .table-wrap {
1426
+ container: table / inline-size;
1427
+ }
1428
+
1429
+ /* One app's tab already names the app. */
1430
+ .views-table:not(.has-app) .col-app {
1431
+ display: none;
1432
+ }
1433
+
1434
+ @container table (width < 88rem) {
1435
+ .views-table.has-app .col-client { display: none; }
1436
+ }
1437
+
1438
+ @container table (width < 80rem) {
1439
+ .views-table .col-client,
1440
+ .views-table.has-app .col-device { display: none; }
1441
+ }
1442
+
1443
+ @container table (width < 73.5rem) {
1444
+ .views-table.has-app .col-country { display: none; }
1445
+ }
1446
+
1447
+ @container table (width < 72rem) {
1448
+ .views-table .col-device { display: none; }
1449
+ }
1450
+
1451
+ @container table (width < 68rem) {
1452
+ .views-table.has-app .col-event { display: none; }
1453
+ }
1454
+
1455
+ @container table (width < 65.5rem) {
1456
+ .views-table .col-country { display: none; }
1457
+ }
1458
+
1459
+ @container table (width < 64rem) {
1460
+ .log-table .col-session { display: none; }
1461
+ }
1462
+
1463
+ @container table (53rem <= width < 61rem) {
1464
+ .views-table.has-app .col-source { display: none; }
1465
+ }
1466
+
1467
+ @container table (width < 60rem) {
1468
+ .views-table .col-event { display: none; }
1469
+ }
1470
+
1471
+ @container table (width < 57rem) {
1472
+ .log-table .col-ip { display: none; }
1473
+ }
1474
+
1475
+ @container table (width < 53rem) {
1476
+ .log-table .col-view {
1477
+ display: none;
1478
+ }
1479
+
1480
+ .views-table thead {
1481
+ display: none;
1482
+ }
1483
+
1484
+ .views-table,
1485
+ .views-table tbody,
1486
+ .views-table tr,
1487
+ .views-table td {
1488
+ display: block;
1489
+ width: 100%;
1490
+ }
1491
+
1492
+ /* The left gutter holds the row's checkbox. */
1493
+ .views-table tbody tr {
1494
+ height: auto;
1495
+ position: relative;
1496
+ padding: 0.6rem 0.8rem 0.6rem 2.8rem;
1497
+ border-bottom: 1px solid var(--border);
1498
+ }
1499
+
1500
+ .views-table td {
1501
+ border: none;
1502
+ padding: 0.15rem 0;
1503
+ }
1504
+
1505
+ .views-table td.col-select {
1506
+ position: absolute;
1507
+ left: 0.8rem;
1508
+ top: 0.8rem;
1509
+ width: auto;
1510
+ }
1511
+
1512
+ /* A card's status line is full width, so every badge fits on one line;
1513
+ fixed, so a card with no badges is as tall as one with some. */
1514
+ .views-table .badges {
1515
+ height: 1.4rem;
1516
+ }
1517
+ }
1518
+
1519
+ /* Log rows have no checkbox, so their cards need no gutter. */
1520
+ @container table (width < 46rem) {
1521
+ .log-table thead {
1522
+ display: none;
1523
+ }
1524
+
1525
+ .log-table,
1526
+ .log-table tbody,
1527
+ .log-table tr,
1528
+ .log-table td {
1529
+ display: block;
1530
+ width: 100%;
1531
+ }
1532
+
1533
+ .log-table tbody tr {
1534
+ height: auto;
1535
+ padding: 0.6rem 0.8rem;
1536
+ border-bottom: 1px solid var(--border);
1537
+ }
1538
+
1539
+ .log-table td {
1540
+ border: none;
1541
+ padding: 0.15rem 0;
1542
+ }
1543
+ }
1544
+
1545
+ .cell-primary {
1546
+ color: var(--text);
1547
+ }
1548
+
1549
+ .cell-secondary {
1550
+ color: var(--text-muted);
1551
+ font-size: 0.8rem;
1552
+ }
1553
+
1554
+ /* Clamp: text never outgrows its cell (FRONTEND.md §14) */
1555
+
1556
+ .clamp {
1557
+ display: block;
1558
+ overflow: hidden;
1559
+ overflow-wrap: anywhere;
1560
+ }
1561
+
1562
+ .clamp-1 {
1563
+ white-space: nowrap;
1564
+ text-overflow: ellipsis;
1565
+ }
1566
+
1567
+ .clamp-2 {
1568
+ display: -webkit-box;
1569
+ -webkit-line-clamp: 2;
1570
+ -webkit-box-orient: vertical;
1571
+ }
1572
+
1573
+ /* Badges */
1574
+
1575
+ .badges {
1576
+ display: flex;
1577
+ flex-wrap: wrap;
1578
+ align-content: center;
1579
+ align-items: center;
1580
+ gap: 0.2rem 0.3rem;
1581
+ overflow: hidden;
1582
+ /* Fixed at the row's two text lines, so a row with no badges is exactly
1583
+ as tall as one with four, which wrap onto a second line rather than
1584
+ being cut off (FRONTEND.md §14: like items share one height). */
1585
+ height: 2.5rem;
1586
+ }
1587
+
1588
+ .badge {
1589
+ font-size: 0.68rem;
1590
+ font-weight: 700;
1591
+ text-transform: uppercase;
1592
+ letter-spacing: 0.04em;
1593
+ padding: 0.1rem 0.45rem;
1594
+ border-radius: 4px;
1595
+ border: 1px solid currentColor;
1596
+ line-height: 1.2;
1597
+ white-space: nowrap;
1598
+ }
1599
+
1600
+ .badge-danger { color: var(--danger); background: var(--danger-bg); }
1601
+ .badge-warning { color: var(--warning); background: var(--warning-bg); }
1602
+ .badge-info { color: var(--info); background: var(--info-bg); }
1603
+ .badge-muted { color: var(--text-muted); background: var(--muted-bg); }
1604
+
1605
+ .row-actions {
1606
+ display: flex;
1607
+ gap: 0.15rem;
1608
+ }
1609
+
1610
+ /* Pager */
1611
+
1612
+ .pager {
1613
+ display: flex;
1614
+ align-items: center;
1615
+ justify-content: space-between;
1616
+ gap: 1rem;
1617
+ }
1618
+
1619
+ .pager-summary {
1620
+ color: var(--text-muted);
1621
+ font-size: 0.85rem;
1622
+ }
1623
+
1624
+ /* ---------- Modal (FRONTEND.md §1.1) ---------- */
1625
+
1626
+ #modal-root {
1627
+ position: fixed;
1628
+ inset: 0;
1629
+ z-index: 200;
1630
+ }
1631
+
1632
+ .modal-backdrop {
1633
+ position: absolute;
1634
+ inset: 0;
1635
+ display: flex;
1636
+ align-items: flex-start;
1637
+ justify-content: center;
1638
+ padding: 8vh 1rem 2rem;
1639
+ background: var(--backdrop);
1640
+ backdrop-filter: blur(3px);
1641
+ overflow-y: auto;
1642
+ animation: fade-in 0.15s ease-out;
1643
+ }
1644
+
1645
+ .modal {
1646
+ width: min(520px, 100%);
1647
+ background: var(--surface-solid);
1648
+ animation: pop-in 0.18s ease-out;
1649
+ }
1650
+
1651
+ .modal-wide {
1652
+ width: min(760px, 100%);
1653
+ }
1654
+
1655
+ .modal-title {
1656
+ font-size: 0.9rem;
1657
+ margin-left: 0.25rem;
1658
+ flex: 1;
1659
+ }
1660
+
1661
+ .modal-close {
1662
+ margin-left: auto;
1663
+ }
1664
+
1665
+ .modal-body {
1666
+ padding: 1.4rem 1.5rem 0.5rem;
1667
+ }
1668
+
1669
+ .modal-message {
1670
+ margin-bottom: 1rem;
1671
+ }
1672
+
1673
+ .modal-actions {
1674
+ display: flex;
1675
+ justify-content: flex-end;
1676
+ flex-wrap: wrap;
1677
+ gap: 0.6rem;
1678
+ padding: 1rem 1.5rem 1.4rem;
1679
+ }
1680
+
1681
+ .detail-list {
1682
+ display: grid;
1683
+ grid-template-columns: 11rem 1fr;
1684
+ gap: 0.45rem 1rem;
1685
+ font-size: 0.85rem;
1686
+ }
1687
+
1688
+ .detail-list dt {
1689
+ color: var(--text-muted);
1690
+ }
1691
+
1692
+ .detail-list dd {
1693
+ overflow-wrap: anywhere;
1694
+ min-width: 0;
1695
+ }
1696
+
1697
+ .detail-json {
1698
+ background: var(--code-bg);
1699
+ border: 1px solid var(--border);
1700
+ border-radius: 6px;
1701
+ padding: 0.6rem;
1702
+ white-space: pre-wrap;
1703
+ overflow-wrap: anywhere;
1704
+ font-size: 0.8rem;
1705
+ }
1706
+
1707
+ .edit-grid {
1708
+ display: flex;
1709
+ flex-direction: column;
1710
+ gap: 0.8rem;
1711
+ margin-bottom: 0.5rem;
1712
+ }
1713
+
1714
+ .edit-row {
1715
+ display: grid;
1716
+ grid-template-columns: 1.4rem 9rem 1fr;
1717
+ align-items: start;
1718
+ gap: 0.6rem;
1719
+ }
1720
+
1721
+ .edit-row .field-include {
1722
+ margin-top: 0.7rem;
1723
+ }
1724
+
1725
+ .edit-label {
1726
+ padding-top: 0.6rem;
1727
+ color: var(--text-muted);
1728
+ font-size: 0.85rem;
1729
+ }
1730
+
1731
+ .edit-control {
1732
+ min-width: 0;
1733
+ }
1734
+
1735
+ /* ---------- Toasts (FRONTEND.md §2) ---------- */
1736
+
1737
+ .toast-root {
1738
+ position: fixed;
1739
+ right: 1.5rem;
1740
+ bottom: 1.5rem;
1741
+ z-index: 260;
1742
+ display: flex;
1743
+ flex-direction: column;
1744
+ gap: 0.6rem;
1745
+ width: min(420px, calc(100vw - 2rem));
1746
+ }
1747
+
1748
+ .toast {
1749
+ display: flex;
1750
+ align-items: flex-start;
1751
+ gap: 0.6rem;
1752
+ padding: 0.8rem 0.9rem;
1753
+ background: var(--surface-solid);
1754
+ border: 1px solid var(--border-strong);
1755
+ border-left: 3px solid var(--primary);
1756
+ border-radius: var(--radius-small);
1757
+ box-shadow: var(--shadow);
1758
+ animation: slide-in 0.2s ease-out;
1759
+ }
1760
+
1761
+ .toast-error { border-left-color: var(--danger); }
1762
+ .toast-error .toast-prompt { color: var(--danger); }
1763
+ .toast-info { border-left-color: var(--info); }
1764
+
1765
+ .toast-prompt {
1766
+ color: var(--primary);
1767
+ font-weight: 700;
1768
+ }
1769
+
1770
+ .toast-message {
1771
+ flex: 1;
1772
+ overflow-wrap: anywhere;
1773
+ }
1774
+
1775
+ .toast-leaving {
1776
+ animation: slide-out 0.2s ease-in forwards;
1777
+ }
1778
+
1779
+ /* ---------- Motion ---------- */
1780
+
1781
+ @keyframes blink {
1782
+ 50% { opacity: 0; }
1783
+ }
1784
+
1785
+ @keyframes fade-in {
1786
+ from { opacity: 0; }
1787
+ }
1788
+
1789
+ @keyframes pop-in {
1790
+ from { opacity: 0; transform: scale(0.97) translateY(-4px); }
1791
+ }
1792
+
1793
+ @keyframes slide-in {
1794
+ from { opacity: 0; transform: translateX(16px); }
1795
+ }
1796
+
1797
+ @keyframes slide-out {
1798
+ to { opacity: 0; transform: translateX(16px); }
1799
+ }
1800
+
1801
+ @media (prefers-reduced-motion: reduce) {
1802
+ *,
1803
+ *::before,
1804
+ *::after {
1805
+ animation: none !important;
1806
+ transition: none !important;
1807
+ }
1808
+ }
1809
+
1810
+ /* ---------- Narrow screens (FRONTEND.md §12: reflow, never scroll sideways) ---------- */
1811
+
1812
+ @media (max-width: 1180px) {
1813
+ .insights-grid {
1814
+ grid-template-columns: minmax(0, 1fr);
1815
+ }
1816
+ }
1817
+
1818
+ @media (max-width: 960px) {
1819
+ main {
1820
+ padding: 1.25rem 1rem 5rem;
1821
+ }
1822
+
1823
+ .topbar {
1824
+ padding: 0.8rem 1rem;
1825
+ }
1826
+
1827
+ .batch-bar {
1828
+ top: 3.6rem;
1829
+ }
1830
+ }
1831
+
1832
+ @media (max-width: 720px) {
1833
+ .tabs {
1834
+ flex-wrap: wrap;
1835
+ }
1836
+
1837
+ .search-input {
1838
+ width: 100%;
1839
+ }
1840
+
1841
+ .toolbar-group {
1842
+ width: 100%;
1843
+ }
1844
+
1845
+ /* A segmented control spans the row and shares it evenly, rather than
1846
+ pushing the page sideways (FRONTEND.md §12). */
1847
+ .segmented {
1848
+ width: 100%;
1849
+ }
1850
+
1851
+ .segmented-btn {
1852
+ flex: 1 1 auto;
1853
+ padding: 0.35rem 0.45rem;
1854
+ }
1855
+
1856
+ .edit-row {
1857
+ grid-template-columns: 1.4rem 1fr;
1858
+ }
1859
+
1860
+ .map-layout {
1861
+ grid-template-columns: minmax(0, 1fr);
1862
+ }
1863
+
1864
+ .edit-row .edit-control {
1865
+ grid-column: 1 / -1;
1866
+ }
1867
+
1868
+ .detail-list {
1869
+ grid-template-columns: 1fr;
1870
+ }
1871
+
1872
+ .logo-text {
1873
+ display: none;
1874
+ }
1875
+ }