kevinconka commited on
Commit
af724e7
Β·
1 Parent(s): 3f78240

Add theme toggle functionality and update styles for light and dark modes

Browse files

- Implemented theme toggle buttons in index.html for light, dark, and system themes.
- Enhanced app.js to manage theme application and color loading from CSS custom properties.
- Updated style.css with new design tokens for light and dark themes, improving overall UI consistency and readability.
- Adjusted font styles and sizes for better visual hierarchy and user experience.

Files changed (3) hide show
  1. app.js +99 -45
  2. index.html +30 -2
  3. style.css +303 -186
app.js CHANGED
@@ -36,6 +36,61 @@
36
  var running = false
37
  var camDir = -1
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  function resetStreaks() {
40
  detectedStreak = 0
41
  blindStreak = 0
@@ -121,24 +176,24 @@
121
  r = 14
122
  ctx.beginPath()
123
  ctx.arc(cx, cy, r, 0, Math.PI * 2)
124
- ctx.strokeStyle = 'rgba(74,158,255,0.2)'
125
  ctx.lineWidth = 1
126
  ctx.stroke()
127
  var start = -Math.PI / 2
128
  var end = start + (d > 0 ? 1.2 : -1.2)
129
  ctx.beginPath()
130
  ctx.arc(cx, cy, r - 3, start, end, d < 0)
131
- ctx.strokeStyle = 'rgba(74,158,255,0.7)'
132
  ctx.lineWidth = 2
133
  ctx.stroke()
134
  var tx = cx + Math.cos(end) * (r - 3)
135
  var ty = cy + Math.sin(end) * (r - 3)
136
  ctx.beginPath()
137
  ctx.arc(tx, ty, 2, 0, Math.PI * 2)
138
- ctx.fillStyle = 'rgba(74,158,255,0.7)'
139
  ctx.fill()
140
- ctx.fillStyle = 'rgba(74,158,255,0.55)'
141
- ctx.font = "7px 'Courier New', monospace"
142
  ctx.textAlign = 'center'
143
  ctx.fillText(d > 0 ? 'CW' : 'CCW', cx, cy + 3)
144
  }
@@ -147,15 +202,15 @@
147
  if (!ctx) return
148
  ctx.clearRect(0, 0, W, H)
149
 
150
- ctx.fillStyle = '#060d16'
151
  ctx.fillRect(0, 0, W, H)
152
- ctx.fillStyle = '#06100a'
153
  ctx.fillRect(0, camCY, W, H - camCY)
154
 
155
  ctx.beginPath()
156
  ctx.moveTo(0, camCY)
157
  ctx.lineTo(W, camCY)
158
- ctx.strokeStyle = 'rgba(74,158,255,0.3)'
159
  ctx.lineWidth = 1
160
  ctx.setLineDash([6, 4])
161
  ctx.stroke()
@@ -184,17 +239,17 @@
184
  var r = ri * ringInterval * SCALE
185
  ctx.beginPath()
186
  ctx.arc(camCX, camCY, r, 0, Math.PI * 2)
187
- ctx.strokeStyle = 'rgba(74,158,255,0.10)'
188
  ctx.lineWidth = 0.5
189
  ctx.stroke()
190
  ctx.beginPath()
191
  ctx.moveTo(camCX - 5, camCY - r)
192
  ctx.lineTo(camCX + 5, camCY - r)
193
- ctx.strokeStyle = 'rgba(74,158,255,0.5)'
194
  ctx.lineWidth = 1
195
  ctx.stroke()
196
- ctx.fillStyle = 'rgba(74,158,255,0.55)'
197
- ctx.font = "10px 'Courier New', monospace"
198
  ctx.textAlign = 'left'
199
  ctx.fillText(fmtDist(ri * ringInterval), camCX + 8, camCY - r + 4)
200
  }
@@ -209,14 +264,14 @@
209
  ctx.beginPath()
210
  ctx.moveTo(camCX + Math.cos(a) * r0, camCY + Math.sin(a) * r0)
211
  ctx.lineTo(camCX + Math.cos(a) * r1, camCY + Math.sin(a) * r1)
212
- ctx.strokeStyle = isMajor ? 'rgba(74,158,255,0.5)' : 'rgba(74,158,255,0.2)'
213
  ctx.lineWidth = isMajor ? 1 : 0.5
214
  ctx.stroke()
215
  if (isMajor) {
216
  var lx = camCX + Math.cos(a) * (r1 + 10)
217
  var ly = camCY + Math.sin(a) * (r1 + 10)
218
- ctx.fillStyle = 'rgba(74,158,255,0.4)'
219
- ctx.font = "8px 'Courier New', monospace"
220
  ctx.textAlign = 'center'
221
  ctx.textBaseline = 'middle'
222
  ctx.fillText(deg + 'Β°', lx, ly)
@@ -227,7 +282,7 @@
227
  ctx.beginPath()
228
  ctx.moveTo(camCX, camCY)
229
  ctx.lineTo(camCX, 0)
230
- ctx.strokeStyle = 'rgba(74,158,255,0.12)'
231
  ctx.lineWidth = 0.5
232
  ctx.setLineDash([4, 4])
233
  ctx.stroke()
@@ -243,14 +298,14 @@
243
  ctx.lineTo(camCX + Math.cos(frontA1) * INF, camCY + Math.sin(frontA1) * INF)
244
  ctx.arc(camCX, camCY, INF, frontA1, frontA2)
245
  ctx.closePath()
246
- ctx.fillStyle = 'rgba(74,158,255,0.03)'
247
  ctx.fill()
248
  ctx.beginPath()
249
  ctx.moveTo(camCX, camCY)
250
  ctx.lineTo(camCX + Math.cos(frontA2) * INF, camCY + Math.sin(frontA2) * INF)
251
  ctx.arc(camCX, camCY, INF, frontA2, frontA1 + Math.PI * 2)
252
  ctx.closePath()
253
- ctx.fillStyle = 'rgba(255,74,74,0.03)'
254
  ctx.fill()
255
  ctx.restore()
256
 
@@ -259,7 +314,7 @@
259
  ctx.beginPath()
260
  ctx.moveTo(camCX, camCY)
261
  ctx.lineTo(camCX + Math.cos(aedge) * INF, camCY + Math.sin(aedge) * INF)
262
- ctx.strokeStyle = 'rgba(74,158,255,0.3)'
263
  ctx.lineWidth = 1
264
  ctx.setLineDash([5, 5])
265
  ctx.stroke()
@@ -267,13 +322,13 @@
267
  }
268
 
269
  var labelR = 80
270
- ctx.font = "8px 'Courier New', monospace"
271
  ctx.textAlign = 'center'
272
- ctx.fillStyle = 'rgba(74,158,255,0.45)'
273
  ctx.fillText('FRONT ' + p.frontArc + 'Β°', camCX, camCY - labelR)
274
- ctx.fillStyle = 'rgba(255,74,74,0.35)'
275
  if (camCY + 18 < H - 2) ctx.fillText('BACK ' + (360 - p.frontArc) + 'Β°', camCX, camCY + 18)
276
- ctx.fillStyle = 'rgba(74,158,255,0.4)'
277
  ctx.fillText('Β±' + frontHalf.toFixed(0) + 'Β°', camCX + Math.cos(frontA1) * labelR, camCY + Math.sin(frontA1) * labelR)
278
  ctx.fillText('Β±' + frontHalf.toFixed(0) + 'Β°', camCX + Math.cos(frontA2) * labelR, camCY + Math.sin(frontA2) * labelR)
279
 
@@ -284,20 +339,20 @@
284
  ctx.moveTo(camCX, camCY)
285
  ctx.arc(camCX, camCY, coneLen, canvasBase + camRad - fovRad / 2, canvasBase + camRad + fovRad / 2)
286
  ctx.closePath()
287
- ctx.fillStyle = 'rgba(74,158,255,0.07)'
288
  ctx.fill()
289
  ctx.beginPath()
290
  ctx.moveTo(camCX, camCY)
291
  ctx.arc(camCX, camCY, coneLen, canvasBase + camRad - fovRad / 2, canvasBase + camRad + fovRad / 2)
292
  ctx.closePath()
293
- ctx.strokeStyle = 'rgba(74,158,255,0.6)'
294
  ctx.lineWidth = 1
295
  ctx.stroke()
296
 
297
  ctx.beginPath()
298
  ctx.moveTo(camCX, camCY)
299
  ctx.lineTo(camCX + Math.cos(canvasBase + camRad) * 32, camCY + Math.sin(canvasBase + camRad) * 32)
300
- ctx.strokeStyle = 'rgba(74,158,255,0.9)'
301
  ctx.lineWidth = 1.5
302
  ctx.stroke()
303
 
@@ -307,7 +362,7 @@
307
  ctx.beginPath()
308
  ctx.moveTo(bsC.x, bsC.y)
309
  ctx.lineTo(beC.x, beC.y)
310
- ctx.strokeStyle = 'rgba(255,74,74,0.25)'
311
  ctx.lineWidth = 1
312
  ctx.setLineDash([4, 3])
313
  ctx.stroke()
@@ -316,51 +371,51 @@
316
  ctx.beginPath()
317
  ctx.moveTo(camCX, camCY)
318
  ctx.lineTo(bC.x, bC.y)
319
- ctx.strokeStyle = 'rgba(255,74,74,0.12)'
320
  ctx.lineWidth = 0.5
321
  ctx.stroke()
322
 
323
  if (inFov) {
324
  ctx.beginPath()
325
  ctx.arc(bC.x, bC.y, 14, 0, Math.PI * 2)
326
- ctx.fillStyle = 'rgba(0,229,160,0.12)'
327
  ctx.fill()
328
  }
329
 
330
  ctx.beginPath()
331
  ctx.arc(bC.x, bC.y, 5, 0, Math.PI * 2)
332
- ctx.fillStyle = inFov ? '#00e5a0' : '#ff4a4a'
333
  ctx.fill()
334
- ctx.strokeStyle = inFov ? 'rgba(0,229,160,0.5)' : 'rgba(255,74,74,0.4)'
335
  ctx.lineWidth = 1.5
336
  ctx.stroke()
337
 
338
- ctx.font = "9px 'Courier New', monospace"
339
  ctx.textAlign = 'left'
340
- ctx.fillStyle = inFov ? 'rgba(0,229,160,0.7)' : 'rgba(255,100,100,0.6)'
341
  ctx.fillText('TGT', bC.x + 8, bC.y - 4)
342
 
343
  ctx.beginPath()
344
  ctx.arc(bsC.x, bsC.y, 4, 0, Math.PI * 2)
345
- ctx.fillStyle = '#ff4a4a'
346
  ctx.fill()
347
  ctx.beginPath()
348
  ctx.arc(beC.x, beC.y, 4, 0, Math.PI * 2)
349
- ctx.fillStyle = '#ff9944'
350
  ctx.fill()
351
- ctx.font = "8px 'Courier New', monospace"
352
- ctx.fillStyle = 'rgba(255,74,74,0.5)'
353
  ctx.textAlign = 'center'
354
  ctx.fillText('S', bsC.x, bsC.y + 14)
355
- ctx.fillStyle = 'rgba(255,153,68,0.5)'
356
  ctx.fillText('E', beC.x, beC.y + 14)
357
 
358
  var cr = 8
359
  ctx.beginPath()
360
  ctx.arc(camCX, camCY, cr, 0, Math.PI * 2)
361
- ctx.fillStyle = '#4a9eff'
362
  ctx.fill()
363
- ctx.strokeStyle = 'rgba(74,158,255,0.5)'
364
  ctx.lineWidth = 1.5
365
  ctx.stroke()
366
  var corners = [
@@ -375,7 +430,7 @@
375
  ctx.beginPath()
376
  ctx.moveTo(camCX + dx * 0.4, camCY + dy * 0.4)
377
  ctx.lineTo(camCX + dx, camCY + dy)
378
- ctx.strokeStyle = 'rgba(74,158,255,0.5)'
379
  ctx.lineWidth = 1
380
  ctx.stroke()
381
  }
@@ -394,8 +449,6 @@
394
  setText('st-scan', st.toFixed(1) + 's')
395
  setText('st-angle', h360.toFixed(1) + 'Β°')
396
  setText('st-dist', (dist * 1000).toFixed(0) + 'm')
397
- setText('hdr-scan', st.toFixed(1) + 's')
398
- setText('hdr-angle', h360.toFixed(1) + 'Β°')
399
 
400
  var travelEl = $('st-travel')
401
  var blindEl = $('st-blind')
@@ -422,9 +475,9 @@
422
  if (total > 0) {
423
  var risk = (prevBlindStreak / total) * 100
424
  riskEl.textContent = risk.toFixed(0) + '%'
425
- riskEl.style.color = risk > 66 ? '#ff4a4a' : risk > 33 ? '#ffcc44' : '#00e5a0'
426
  riskBarEl.style.width = risk + '%'
427
- riskBarEl.style.background = risk > 66 ? '#ff4a4a' : risk > 33 ? '#ffcc44' : '#00e5a0'
428
  } else {
429
  riskEl.textContent = 'β€”'
430
  riskBarEl.style.width = '0%'
@@ -596,6 +649,7 @@
596
  ctx = canvas.getContext('2d')
597
  if (!ctx) return
598
 
 
599
  running = true
600
  resize()
601
  updateZoomLabel()
 
36
  var running = false
37
  var camDir = -1
38
 
39
+ // Color palette β€” loaded from CSS custom properties at init
40
+ var C = {
41
+ p: '', d: '', s: '', w: '', s1: '', s2: '',
42
+ pr: '', dr: '', sr: '', wr: '',
43
+ }
44
+
45
+ var systemQuery = window.matchMedia('(prefers-color-scheme: light)')
46
+
47
+ function applyTheme(pref) {
48
+ var sim = document.querySelector('.coastal-surveillance-sim')
49
+ var resolved = pref === 'system' ? (systemQuery.matches ? 'light' : 'dark') : pref
50
+ sim.dataset.theme = resolved
51
+ loadColors()
52
+ var btns = document.querySelectorAll('.theme-btn')
53
+ for (var i = 0; i < btns.length; i++) {
54
+ btns[i].classList.toggle('active', btns[i].dataset.themeVal === pref)
55
+ }
56
+ }
57
+
58
+ function initTheme() {
59
+ var saved = localStorage.getItem('theme') || 'system'
60
+ applyTheme(saved)
61
+ systemQuery.addEventListener('change', function () {
62
+ if (localStorage.getItem('theme') === 'system') applyTheme('system')
63
+ })
64
+ var btns = document.querySelectorAll('.theme-btn')
65
+ for (var i = 0; i < btns.length; i++) {
66
+ btns[i].addEventListener('click', function () {
67
+ var val = this.dataset.themeVal
68
+ localStorage.setItem('theme', val)
69
+ applyTheme(val)
70
+ })
71
+ }
72
+ }
73
+
74
+ function loadColors() {
75
+ var el = document.querySelector('.coastal-surveillance-sim')
76
+ var cs = getComputedStyle(el)
77
+ function v(n) { return cs.getPropertyValue(n).trim() }
78
+ C.p = v('--primary')
79
+ C.d = v('--danger')
80
+ C.s = v('--success')
81
+ C.w = v('--warning')
82
+ C.s1 = v('--surface-1')
83
+ C.s2 = v('--surface-2')
84
+ C.pr = v('--primary-rgb')
85
+ C.dr = v('--danger-rgb')
86
+ C.sr = v('--success-rgb')
87
+ C.wr = v('--warning-rgb')
88
+ }
89
+
90
+ function rgba(triplet, alpha) {
91
+ return 'rgba(' + triplet + ',' + alpha + ')'
92
+ }
93
+
94
  function resetStreaks() {
95
  detectedStreak = 0
96
  blindStreak = 0
 
176
  r = 14
177
  ctx.beginPath()
178
  ctx.arc(cx, cy, r, 0, Math.PI * 2)
179
+ ctx.strokeStyle = rgba(C.pr, 0.2)
180
  ctx.lineWidth = 1
181
  ctx.stroke()
182
  var start = -Math.PI / 2
183
  var end = start + (d > 0 ? 1.2 : -1.2)
184
  ctx.beginPath()
185
  ctx.arc(cx, cy, r - 3, start, end, d < 0)
186
+ ctx.strokeStyle = rgba(C.pr, 0.7)
187
  ctx.lineWidth = 2
188
  ctx.stroke()
189
  var tx = cx + Math.cos(end) * (r - 3)
190
  var ty = cy + Math.sin(end) * (r - 3)
191
  ctx.beginPath()
192
  ctx.arc(tx, ty, 2, 0, Math.PI * 2)
193
+ ctx.fillStyle = rgba(C.pr, 0.7)
194
  ctx.fill()
195
+ ctx.fillStyle = rgba(C.pr, 0.55)
196
+ ctx.font = "12px Saira Condensed, Arial Narrow, Arial, sans-serif"
197
  ctx.textAlign = 'center'
198
  ctx.fillText(d > 0 ? 'CW' : 'CCW', cx, cy + 3)
199
  }
 
202
  if (!ctx) return
203
  ctx.clearRect(0, 0, W, H)
204
 
205
+ ctx.fillStyle = C.s1
206
  ctx.fillRect(0, 0, W, H)
207
+ ctx.fillStyle = C.s2
208
  ctx.fillRect(0, camCY, W, H - camCY)
209
 
210
  ctx.beginPath()
211
  ctx.moveTo(0, camCY)
212
  ctx.lineTo(W, camCY)
213
+ ctx.strokeStyle = rgba(C.pr, 0.3)
214
  ctx.lineWidth = 1
215
  ctx.setLineDash([6, 4])
216
  ctx.stroke()
 
239
  var r = ri * ringInterval * SCALE
240
  ctx.beginPath()
241
  ctx.arc(camCX, camCY, r, 0, Math.PI * 2)
242
+ ctx.strokeStyle = rgba(C.pr, 0.10)
243
  ctx.lineWidth = 0.5
244
  ctx.stroke()
245
  ctx.beginPath()
246
  ctx.moveTo(camCX - 5, camCY - r)
247
  ctx.lineTo(camCX + 5, camCY - r)
248
+ ctx.strokeStyle = rgba(C.pr, 0.5)
249
  ctx.lineWidth = 1
250
  ctx.stroke()
251
+ ctx.fillStyle = rgba(C.pr, 0.55)
252
+ ctx.font = "12px Saira Condensed, Arial Narrow, Arial, sans-serif"
253
  ctx.textAlign = 'left'
254
  ctx.fillText(fmtDist(ri * ringInterval), camCX + 8, camCY - r + 4)
255
  }
 
264
  ctx.beginPath()
265
  ctx.moveTo(camCX + Math.cos(a) * r0, camCY + Math.sin(a) * r0)
266
  ctx.lineTo(camCX + Math.cos(a) * r1, camCY + Math.sin(a) * r1)
267
+ ctx.strokeStyle = isMajor ? rgba(C.pr, 0.5) : rgba(C.pr, 0.2)
268
  ctx.lineWidth = isMajor ? 1 : 0.5
269
  ctx.stroke()
270
  if (isMajor) {
271
  var lx = camCX + Math.cos(a) * (r1 + 10)
272
  var ly = camCY + Math.sin(a) * (r1 + 10)
273
+ ctx.fillStyle = rgba(C.pr, 0.4)
274
+ ctx.font = "11px Saira Condensed, Arial Narrow, Arial, sans-serif"
275
  ctx.textAlign = 'center'
276
  ctx.textBaseline = 'middle'
277
  ctx.fillText(deg + 'Β°', lx, ly)
 
282
  ctx.beginPath()
283
  ctx.moveTo(camCX, camCY)
284
  ctx.lineTo(camCX, 0)
285
+ ctx.strokeStyle = rgba(C.pr, 0.12)
286
  ctx.lineWidth = 0.5
287
  ctx.setLineDash([4, 4])
288
  ctx.stroke()
 
298
  ctx.lineTo(camCX + Math.cos(frontA1) * INF, camCY + Math.sin(frontA1) * INF)
299
  ctx.arc(camCX, camCY, INF, frontA1, frontA2)
300
  ctx.closePath()
301
+ ctx.fillStyle = rgba(C.pr, 0.03)
302
  ctx.fill()
303
  ctx.beginPath()
304
  ctx.moveTo(camCX, camCY)
305
  ctx.lineTo(camCX + Math.cos(frontA2) * INF, camCY + Math.sin(frontA2) * INF)
306
  ctx.arc(camCX, camCY, INF, frontA2, frontA1 + Math.PI * 2)
307
  ctx.closePath()
308
+ ctx.fillStyle = rgba(C.dr, 0.03)
309
  ctx.fill()
310
  ctx.restore()
311
 
 
314
  ctx.beginPath()
315
  ctx.moveTo(camCX, camCY)
316
  ctx.lineTo(camCX + Math.cos(aedge) * INF, camCY + Math.sin(aedge) * INF)
317
+ ctx.strokeStyle = rgba(C.pr, 0.3)
318
  ctx.lineWidth = 1
319
  ctx.setLineDash([5, 5])
320
  ctx.stroke()
 
322
  }
323
 
324
  var labelR = 80
325
+ ctx.font = "11px Saira Condensed, Arial Narrow, Arial, sans-serif"
326
  ctx.textAlign = 'center'
327
+ ctx.fillStyle = rgba(C.pr, 0.45)
328
  ctx.fillText('FRONT ' + p.frontArc + 'Β°', camCX, camCY - labelR)
329
+ ctx.fillStyle = rgba(C.dr, 0.35)
330
  if (camCY + 18 < H - 2) ctx.fillText('BACK ' + (360 - p.frontArc) + 'Β°', camCX, camCY + 18)
331
+ ctx.fillStyle = rgba(C.pr, 0.4)
332
  ctx.fillText('Β±' + frontHalf.toFixed(0) + 'Β°', camCX + Math.cos(frontA1) * labelR, camCY + Math.sin(frontA1) * labelR)
333
  ctx.fillText('Β±' + frontHalf.toFixed(0) + 'Β°', camCX + Math.cos(frontA2) * labelR, camCY + Math.sin(frontA2) * labelR)
334
 
 
339
  ctx.moveTo(camCX, camCY)
340
  ctx.arc(camCX, camCY, coneLen, canvasBase + camRad - fovRad / 2, canvasBase + camRad + fovRad / 2)
341
  ctx.closePath()
342
+ ctx.fillStyle = rgba(C.pr, 0.07)
343
  ctx.fill()
344
  ctx.beginPath()
345
  ctx.moveTo(camCX, camCY)
346
  ctx.arc(camCX, camCY, coneLen, canvasBase + camRad - fovRad / 2, canvasBase + camRad + fovRad / 2)
347
  ctx.closePath()
348
+ ctx.strokeStyle = rgba(C.pr, 0.6)
349
  ctx.lineWidth = 1
350
  ctx.stroke()
351
 
352
  ctx.beginPath()
353
  ctx.moveTo(camCX, camCY)
354
  ctx.lineTo(camCX + Math.cos(canvasBase + camRad) * 32, camCY + Math.sin(canvasBase + camRad) * 32)
355
+ ctx.strokeStyle = rgba(C.pr, 0.9)
356
  ctx.lineWidth = 1.5
357
  ctx.stroke()
358
 
 
362
  ctx.beginPath()
363
  ctx.moveTo(bsC.x, bsC.y)
364
  ctx.lineTo(beC.x, beC.y)
365
+ ctx.strokeStyle = rgba(C.dr, 0.25)
366
  ctx.lineWidth = 1
367
  ctx.setLineDash([4, 3])
368
  ctx.stroke()
 
371
  ctx.beginPath()
372
  ctx.moveTo(camCX, camCY)
373
  ctx.lineTo(bC.x, bC.y)
374
+ ctx.strokeStyle = rgba(C.dr, 0.12)
375
  ctx.lineWidth = 0.5
376
  ctx.stroke()
377
 
378
  if (inFov) {
379
  ctx.beginPath()
380
  ctx.arc(bC.x, bC.y, 14, 0, Math.PI * 2)
381
+ ctx.fillStyle = rgba(C.sr, 0.12)
382
  ctx.fill()
383
  }
384
 
385
  ctx.beginPath()
386
  ctx.arc(bC.x, bC.y, 5, 0, Math.PI * 2)
387
+ ctx.fillStyle = inFov ? C.s : C.d
388
  ctx.fill()
389
+ ctx.strokeStyle = inFov ? rgba(C.sr, 0.5) : rgba(C.dr, 0.4)
390
  ctx.lineWidth = 1.5
391
  ctx.stroke()
392
 
393
+ ctx.font = "11px Saira Condensed, Arial Narrow, Arial, sans-serif"
394
  ctx.textAlign = 'left'
395
+ ctx.fillStyle = inFov ? rgba(C.sr, 0.7) : rgba(C.dr, 0.7)
396
  ctx.fillText('TGT', bC.x + 8, bC.y - 4)
397
 
398
  ctx.beginPath()
399
  ctx.arc(bsC.x, bsC.y, 4, 0, Math.PI * 2)
400
+ ctx.fillStyle = C.d
401
  ctx.fill()
402
  ctx.beginPath()
403
  ctx.arc(beC.x, beC.y, 4, 0, Math.PI * 2)
404
+ ctx.fillStyle = C.w
405
  ctx.fill()
406
+ ctx.font = "11px Saira Condensed, Arial Narrow, Arial, sans-serif"
407
+ ctx.fillStyle = rgba(C.dr, 0.5)
408
  ctx.textAlign = 'center'
409
  ctx.fillText('S', bsC.x, bsC.y + 14)
410
+ ctx.fillStyle = rgba(C.wr, 0.5)
411
  ctx.fillText('E', beC.x, beC.y + 14)
412
 
413
  var cr = 8
414
  ctx.beginPath()
415
  ctx.arc(camCX, camCY, cr, 0, Math.PI * 2)
416
+ ctx.fillStyle = C.p
417
  ctx.fill()
418
+ ctx.strokeStyle = rgba(C.pr, 0.5)
419
  ctx.lineWidth = 1.5
420
  ctx.stroke()
421
  var corners = [
 
430
  ctx.beginPath()
431
  ctx.moveTo(camCX + dx * 0.4, camCY + dy * 0.4)
432
  ctx.lineTo(camCX + dx, camCY + dy)
433
+ ctx.strokeStyle = rgba(C.pr, 0.5)
434
  ctx.lineWidth = 1
435
  ctx.stroke()
436
  }
 
449
  setText('st-scan', st.toFixed(1) + 's')
450
  setText('st-angle', h360.toFixed(1) + 'Β°')
451
  setText('st-dist', (dist * 1000).toFixed(0) + 'm')
 
 
452
 
453
  var travelEl = $('st-travel')
454
  var blindEl = $('st-blind')
 
475
  if (total > 0) {
476
  var risk = (prevBlindStreak / total) * 100
477
  riskEl.textContent = risk.toFixed(0) + '%'
478
+ riskEl.style.color = risk > 66 ? 'var(--danger)' : risk > 33 ? 'var(--warning)' : 'var(--success)'
479
  riskBarEl.style.width = risk + '%'
480
+ riskBarEl.style.background = risk > 66 ? 'var(--danger)' : risk > 33 ? 'var(--warning)' : 'var(--success)'
481
  } else {
482
  riskEl.textContent = 'β€”'
483
  riskBarEl.style.width = '0%'
 
649
  ctx = canvas.getContext('2d')
650
  if (!ctx) return
651
 
652
+ initTheme()
653
  running = true
654
  resize()
655
  updateZoomLabel()
index.html CHANGED
@@ -5,6 +5,9 @@
5
  <meta charset="UTF-8" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Coastal Surveillance Simulator</title>
 
 
 
8
  <link rel="stylesheet" href="style.css" />
9
  </head>
10
 
@@ -16,8 +19,33 @@
16
  <h1>Coastal Surveillance Sim</h1>
17
  <span class="sub">// PTZ camera model</span>
18
  <div class="spacer"></div>
19
- <div class="hstat">SCAN <span id="hdr-scan">β€”</span></div>
20
- <div class="hstat" style="margin-left: 12px">HEADING <span id="hdr-angle">β€”</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  </div>
22
 
23
  <div id="canvas-wrap">
 
5
  <meta charset="UTF-8" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Coastal Surveillance Simulator</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Saira+Condensed:wght@400;500&display=swap" rel="stylesheet">
11
  <link rel="stylesheet" href="style.css" />
12
  </head>
13
 
 
19
  <h1>Coastal Surveillance Sim</h1>
20
  <span class="sub">// PTZ camera model</span>
21
  <div class="spacer"></div>
22
+ <div class="theme-toggle" id="theme-toggle">
23
+ <button type="button" class="theme-btn" data-theme-val="light" title="Light theme" aria-label="Light theme">
24
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
25
+ <circle cx="8" cy="8" r="3" stroke="currentColor" stroke-width="1.5"/>
26
+ <line x1="8" y1="1" x2="8" y2="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
27
+ <line x1="8" y1="13" x2="8" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
28
+ <line x1="1" y1="8" x2="3" y2="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
29
+ <line x1="13" y1="8" x2="15" y2="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
30
+ <line x1="3.05" y1="3.05" x2="4.46" y2="4.46" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
31
+ <line x1="11.54" y1="11.54" x2="12.95" y2="12.95" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
32
+ <line x1="12.95" y1="3.05" x2="11.54" y2="4.46" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
33
+ <line x1="4.46" y1="11.54" x2="3.05" y2="12.95" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
34
+ </svg>
35
+ </button>
36
+ <button type="button" class="theme-btn" data-theme-val="dark" title="Dark theme" aria-label="Dark theme">
37
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
38
+ <path d="M13.5 10.5A6 6 0 0 1 5.5 2.5a6 6 0 1 0 8 8z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
39
+ </svg>
40
+ </button>
41
+ <button type="button" class="theme-btn" data-theme-val="system" title="System theme" aria-label="System theme">
42
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
43
+ <rect x="1" y="2" width="14" height="10" rx="1.5" stroke="currentColor" stroke-width="1.5"/>
44
+ <line x1="5" y1="14" x2="11" y2="14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
45
+ <line x1="8" y1="12" x2="8" y2="14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
46
+ </svg>
47
+ </button>
48
+ </div>
49
  </div>
50
 
51
  <div id="canvas-wrap">
style.css CHANGED
@@ -10,35 +10,84 @@ body {
10
  }
11
 
12
  .coastal-surveillance-sim {
13
- --bg0: #080e14;
14
- --bg1: #0c1520;
15
- --bg2: #111d2c;
16
- --bg3: #162030;
17
- --accent: #4a9eff;
18
- --accent-dim: rgba(74, 158, 255, 0.18);
19
- --accent-glow: rgba(74, 158, 255, 0.08);
20
- --text: #c8daf0;
21
- --text-muted: #4a6a8a;
22
- --text-dim: #2a4a6a;
23
- --border: rgba(74, 158, 255, 0.15);
24
- --border-bright: rgba(74, 158, 255, 0.35);
25
- --danger: #ff4a4a;
26
- --success: #00e5a0;
27
- --warn: #ffcc44;
28
- --font: 'Courier New', monospace;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  box-sizing: border-box;
31
  width: 100%;
32
- min-height: 100%;
33
  height: 100%;
34
- background: var(--bg0);
35
- color: var(--text);
36
  font-family: var(--font);
37
- font-size: 13px;
38
  overflow: hidden;
39
  display: grid;
40
- grid-template-columns: 1fr 252px;
41
- grid-template-rows: 38px 1fr;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
43
 
44
  .coastal-surveillance-sim *,
@@ -47,345 +96,413 @@ body {
47
  box-sizing: border-box;
48
  }
49
 
 
 
50
  #header {
51
  grid-column: 1 / -1;
52
- padding: 0 16px;
53
- border-bottom: 1px solid var(--border);
54
- background: var(--bg1);
55
  display: flex;
56
  align-items: center;
57
- gap: 16px;
58
  }
 
59
  #header h1 {
60
- font-size: 11px;
61
- font-weight: normal;
62
- letter-spacing: 0.18em;
63
- color: var(--accent);
64
  text-transform: uppercase;
 
65
  }
 
66
  #header .sub {
67
- font-size: 10px;
68
- color: var(--text-muted);
69
- letter-spacing: 0.08em;
 
 
70
  }
 
71
  #header .spacer {
72
  flex: 1;
73
  }
74
- #header .hstat {
75
- font-size: 10px;
76
- letter-spacing: 0.1em;
77
- color: var(--text-muted);
78
- display: flex;
79
- align-items: center;
80
- gap: 5px;
81
- }
82
- #header .hstat span {
83
- color: var(--accent);
84
- }
85
  .blink {
86
  display: inline-block;
87
  width: 6px;
88
  height: 6px;
89
  border-radius: 50%;
90
  background: var(--success);
 
91
  animation: blink 1.4s ease-in-out infinite;
92
  }
 
93
  @keyframes blink {
94
- 0%,
95
- 100% {
96
- opacity: 1;
97
- }
98
- 50% {
99
- opacity: 0.15;
100
- }
101
  }
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  #canvas-wrap {
104
  position: relative;
105
- background: var(--bg0);
106
  overflow: hidden;
107
  }
 
108
  #canvas-wrap canvas {
109
  display: block;
110
  width: 100%;
111
  height: 100%;
112
  }
113
 
 
 
114
  .zoom-bar {
115
  position: absolute;
116
- bottom: 10px;
117
- right: 10px;
118
  display: flex;
119
  align-items: center;
120
- gap: 4px;
121
  z-index: 10;
122
  }
 
123
  .zoom-label {
124
  font-family: var(--font);
125
- font-size: 10px;
126
- color: var(--accent);
127
- min-width: 34px;
 
128
  text-align: center;
129
- letter-spacing: 0.08em;
130
  }
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  #panel {
133
- background: var(--bg1);
134
- border-left: 1px solid var(--border);
135
- padding: 10px;
136
  overflow-y: auto;
137
  display: flex;
138
  flex-direction: column;
139
- gap: 8px;
140
  }
141
 
 
 
142
  .section {
143
- border: 1px solid var(--border);
144
- background: var(--bg2);
 
145
  }
 
146
  .section-title {
147
- font-size: 9px;
148
- letter-spacing: 0.2em;
 
149
  text-transform: uppercase;
150
- color: var(--accent);
151
- padding: 5px 8px;
152
- background: rgba(74, 158, 255, 0.05);
153
- border-bottom: 1px solid var(--border);
154
  }
 
155
  .section-body {
156
- padding: 8px;
157
  }
158
 
 
 
159
  .stats-grid {
160
  display: grid;
161
  grid-template-columns: 1fr 1fr;
162
  gap: 1px;
163
- background: var(--border);
164
  }
 
165
  .stat {
166
- background: var(--bg2);
167
- padding: 6px 8px;
168
  }
 
169
  .stat .lbl {
170
- font-size: 8px;
171
- letter-spacing: 0.14em;
 
172
  text-transform: uppercase;
173
- color: var(--text-muted);
174
  margin-bottom: 2px;
175
  }
 
176
  .stat .val {
177
- font-size: 15px;
178
- color: var(--text);
179
- letter-spacing: 0.02em;
180
- }
181
- .stat .val.active {
182
- color: var(--success);
183
- }
184
- .stat .val.inactive {
185
- color: var(--danger);
186
  }
187
 
 
 
 
188
  .stats-extra {
189
  margin-top: 1px;
190
  display: grid;
191
  grid-template-columns: 1fr 1fr;
192
  gap: 1px;
193
- background: var(--border);
194
  }
 
195
  .stats-extra .stat-risk {
196
  grid-column: 1 / -1;
197
- border-top: 1px solid rgba(255, 74, 74, 0.2);
198
- }
199
- .st-sm {
200
- font-size: 13px;
201
  }
 
 
 
202
  .stat-sub {
203
- font-size: 9px;
204
- color: var(--text-muted);
205
- opacity: 0.5;
206
  margin-top: 2px;
207
- letter-spacing: 0.06em;
208
  }
 
209
  .risk-row {
210
  display: flex;
211
  align-items: center;
212
- gap: 8px;
213
- margin-top: 3px;
214
  }
 
215
  .st-risk-val {
216
- font-size: 14px;
217
- min-width: 38px;
218
  }
 
219
  .risk-track {
220
  flex: 1;
221
  height: 3px;
222
- background: rgba(255, 255, 255, 0.06);
 
223
  overflow: hidden;
224
  }
 
225
  .risk-fill {
226
  height: 100%;
227
  width: 0%;
228
- background: #ff4a4a;
229
- transition: width 0.4s;
 
230
  }
231
 
 
 
232
  .ctrl {
233
- margin-bottom: 8px;
234
  }
 
235
  .ctrl:last-child {
236
  margin-bottom: 0;
237
  }
 
238
  .ctrl label {
239
  display: flex;
240
  justify-content: space-between;
241
- font-size: 10px;
242
- color: var(--text-muted);
243
- margin-bottom: 3px;
244
- letter-spacing: 0.06em;
245
  text-transform: uppercase;
246
  }
 
247
  .ctrl label span {
248
- color: var(--text);
249
- font-size: 11px;
 
250
  }
251
 
 
252
  input[type='range'] {
253
  width: 100%;
254
  -webkit-appearance: none;
255
- height: 2px;
256
- background: rgba(74, 158, 255, 0.15);
 
257
  outline: none;
258
  cursor: pointer;
 
259
  }
 
260
  input[type='range']::-webkit-slider-thumb {
261
  -webkit-appearance: none;
262
- width: 10px;
263
- height: 10px;
264
- background: var(--accent);
 
265
  cursor: pointer;
266
  border-radius: 50%;
267
- box-shadow: 0 0 4px rgba(74, 158, 255, 0.6);
268
  }
 
269
  input[type='range']::-moz-range-thumb {
270
- width: 10px;
271
- height: 10px;
272
- background: var(--accent);
273
  cursor: pointer;
274
  border-radius: 50%;
275
  border: none;
276
  }
277
 
 
278
  .dir-toggle {
279
  display: flex;
280
- gap: 4px;
281
- margin-top: 4px;
282
  }
 
283
  .dir-btn {
284
  flex: 1;
285
- padding: 5px 4px;
286
- background: var(--bg3);
287
- border: 1px solid var(--border);
288
- color: var(--text-muted);
 
289
  font-family: var(--font);
290
- font-size: 10px;
291
- letter-spacing: 0.1em;
 
292
  cursor: pointer;
293
  text-align: center;
294
  text-transform: uppercase;
295
- transition: all 0.12s;
296
  }
 
297
  .dir-btn:hover {
298
- border-color: var(--accent);
299
- color: var(--accent);
300
  }
 
301
  .dir-btn.active {
302
- background: var(--accent-dim);
303
- border-color: var(--accent);
304
- color: var(--accent);
305
  }
306
 
 
307
  .hint {
308
- font-size: 9px;
309
- color: var(--text-muted);
310
- letter-spacing: 0.04em;
311
- line-height: 1.7;
312
- opacity: 0.7;
313
- }
314
- .hint-s {
315
- color: #ff6b6b;
316
- }
317
- .hint-e {
318
- color: #ff9944;
319
  }
320
 
 
 
 
 
 
321
  .legend {
322
  display: flex;
323
  flex-direction: column;
324
- gap: 4px;
325
  }
 
326
  .leg-item {
327
  display: flex;
328
  align-items: center;
329
- gap: 7px;
330
- font-size: 10px;
331
- color: var(--text-muted);
 
 
332
  }
 
333
  .leg-dot {
334
- width: 7px;
335
- height: 7px;
336
  border-radius: 50%;
337
  flex-shrink: 0;
338
  }
339
- .leg-dot--cam {
340
- background: #4a9eff;
341
- }
342
- .leg-dot--boat {
343
- background: #ff4a4a;
344
- }
345
- .leg-dot--det {
346
- background: #00e5a0;
347
- }
348
  .leg-line {
349
- width: 14px;
350
  height: 1px;
351
  flex-shrink: 0;
352
  }
353
- .leg-line--rings {
354
- background: rgba(74, 158, 255, 0.4);
355
- }
356
  .leg-line--arc {
357
- background: rgba(74, 158, 255, 0.25);
358
- border-top: 1px dashed rgba(74, 158, 255, 0.4);
359
  height: 0;
360
- padding-top: 1px;
361
  }
362
 
363
- .zoom-btn {
364
- width: 26px;
365
- height: 26px;
366
- background: rgba(8, 14, 20, 0.9);
367
- border: 1px solid var(--border-bright);
368
- color: var(--text);
369
- font-family: var(--font);
370
- font-size: 15px;
371
- cursor: pointer;
372
- display: flex;
373
- align-items: center;
374
- justify-content: center;
375
- transition: border-color 0.12s;
376
- }
377
- .zoom-btn:hover {
378
- border-color: var(--accent);
379
- color: var(--accent);
380
- }
381
 
382
  @media (max-width: 700px) {
383
  .coastal-surveillance-sim {
384
  grid-template-columns: 1fr;
385
- grid-template-rows: 38px 55vw 1fr;
386
  }
387
  #panel {
388
  border-left: none;
389
- border-top: 1px solid var(--border);
390
  }
391
  }
 
10
  }
11
 
12
  .coastal-surveillance-sim {
13
+ /* SEA.AI Design Tokens β€” DARK theme (default) */
14
+ --surface-1: #101214;
15
+ --surface-2: #181B1E;
16
+ --surface-3: #202428;
17
+ --surface-4: #292E33;
18
+ --surface-5: #31373D;
19
+
20
+ --content-hi: #DCDEE0;
21
+ --content-mid: #B9BDC1;
22
+ --content-lo: #6B7177;
23
+
24
+ --primary: #0A67C2;
25
+ --primary-dim: #084C91;
26
+ --primary-content: #CFE5FC;
27
+ --primary-rgb: 10, 103, 194;
28
+
29
+ --danger: #C20A20;
30
+ --danger-dim: #910818;
31
+ --danger-content: #FA9EA9;
32
+ --danger-rgb: 194, 10, 32;
33
+
34
+ --warning: #C2940A;
35
+ --warning-content: #FAE39E;
36
+ --warning-rgb: 194, 148, 10;
37
+
38
+ --success: #14A87A;
39
+ --success-content: #6EDEB9;
40
+ --success-rgb: 20, 168, 122;
41
+
42
+ --stroke: 1px solid rgba(255, 255, 255, 0.07);
43
+
44
+ --radious-s: 4px;
45
+ --radious-m: 8px;
46
+
47
+ --sp-xs: 4px;
48
+ --sp-s: 8px;
49
+ --sp-m: 12px;
50
+ --sp-l: 16px;
51
+ --sp-xl: 24px;
52
+
53
+ --font: 'Saira Condensed', 'Arial Narrow', Arial, sans-serif;
54
 
55
  box-sizing: border-box;
56
  width: 100%;
 
57
  height: 100%;
58
+ background: var(--surface-1);
59
+ color: var(--content-hi);
60
  font-family: var(--font);
61
+ font-size: 14px;
62
  overflow: hidden;
63
  display: grid;
64
+ grid-template-columns: 1fr 264px;
65
+ grid-template-rows: 40px 1fr;
66
+ }
67
+
68
+ /* SEA.AI Design Tokens β€” LIGHT theme */
69
+ .coastal-surveillance-sim[data-theme="light"] {
70
+ --surface-1: #B9BDC1;
71
+ --surface-2: #CBCED1;
72
+ --surface-3: #DCDEE0;
73
+ --surface-4: #EEEFF0;
74
+ --surface-5: #FFFFFF;
75
+
76
+ --content-hi: #101214;
77
+ --content-mid: #292E33;
78
+ --content-lo: #6B7177;
79
+
80
+ --primary-dim: rgba(10, 103, 194, 0.12);
81
+ --primary-content: #0A67C2;
82
+
83
+ --danger-dim: rgba(194, 10, 32, 0.1);
84
+ --danger-content: #C20A20;
85
+
86
+ --warning-content: #9B7508;
87
+
88
+ --success-content: #0A6E50;
89
+
90
+ --stroke: 1px solid rgba(0, 0, 0, 0.1);
91
  }
92
 
93
  .coastal-surveillance-sim *,
 
96
  box-sizing: border-box;
97
  }
98
 
99
+ /* ── Header ─────────────────────────────────────────── */
100
+
101
  #header {
102
  grid-column: 1 / -1;
103
+ padding: 0 var(--sp-l);
104
+ border-bottom: var(--stroke);
105
+ background: var(--surface-2);
106
  display: flex;
107
  align-items: center;
108
+ gap: var(--sp-l);
109
  }
110
+
111
  #header h1 {
112
+ font-size: 16px;
113
+ font-weight: 500;
114
+ letter-spacing: 0.02em;
115
+ color: var(--content-hi);
116
  text-transform: uppercase;
117
+ margin: 0;
118
  }
119
+
120
  #header .sub {
121
+ font-size: 12px;
122
+ font-weight: 400;
123
+ color: var(--content-lo);
124
+ letter-spacing: 0.02em;
125
+ text-transform: uppercase;
126
  }
127
+
128
  #header .spacer {
129
  flex: 1;
130
  }
131
+
132
+
 
 
 
 
 
 
 
 
 
133
  .blink {
134
  display: inline-block;
135
  width: 6px;
136
  height: 6px;
137
  border-radius: 50%;
138
  background: var(--success);
139
+ flex-shrink: 0;
140
  animation: blink 1.4s ease-in-out infinite;
141
  }
142
+
143
  @keyframes blink {
144
+ 0%, 100% { opacity: 1; }
145
+ 50% { opacity: 0.15; }
 
 
 
 
 
146
  }
147
 
148
+ /* Theme toggle */
149
+ .theme-toggle {
150
+ display: flex;
151
+ gap: 1px;
152
+ background: var(--surface-4);
153
+ border-radius: var(--radious-s);
154
+ padding: 2px;
155
+ }
156
+
157
+ .theme-btn {
158
+ width: 28px;
159
+ height: 28px;
160
+ padding: 0;
161
+ background: transparent;
162
+ border: none;
163
+ border-radius: 3px;
164
+ color: var(--content-lo);
165
+ cursor: pointer;
166
+ display: flex;
167
+ align-items: center;
168
+ justify-content: center;
169
+ transition: background 0.2s ease-in-out, color 0.2s ease-in-out;
170
+ }
171
+
172
+ .theme-btn:hover { color: var(--content-hi); }
173
+
174
+ .theme-btn.active {
175
+ background: var(--surface-2);
176
+ color: var(--content-hi);
177
+ }
178
+
179
+ /* ── Canvas ──────────────────────────────────────────── */
180
+
181
  #canvas-wrap {
182
  position: relative;
183
+ background: var(--surface-1);
184
  overflow: hidden;
185
  }
186
+
187
  #canvas-wrap canvas {
188
  display: block;
189
  width: 100%;
190
  height: 100%;
191
  }
192
 
193
+ /* ── Zoom bar ──���─────────────────────────────────────── */
194
+
195
  .zoom-bar {
196
  position: absolute;
197
+ bottom: var(--sp-m);
198
+ right: var(--sp-m);
199
  display: flex;
200
  align-items: center;
201
+ gap: var(--sp-xs);
202
  z-index: 10;
203
  }
204
+
205
  .zoom-label {
206
  font-family: var(--font);
207
+ font-size: 12px;
208
+ font-weight: 500;
209
+ color: var(--content-mid);
210
+ min-width: 36px;
211
  text-align: center;
 
212
  }
213
 
214
+ .zoom-btn {
215
+ width: 32px;
216
+ height: 32px;
217
+ background: var(--surface-3);
218
+ border: 1px solid rgba(255, 255, 255, 0.1);
219
+ border-radius: var(--radious-s);
220
+ color: var(--content-mid);
221
+ font-family: var(--font);
222
+ font-size: 18px;
223
+ font-weight: 400;
224
+ cursor: pointer;
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: center;
228
+ transition: background 0.2s ease-in-out, color 0.2s ease-in-out;
229
+ line-height: 1;
230
+ }
231
+
232
+ .zoom-btn:hover {
233
+ background: var(--surface-4);
234
+ color: var(--content-hi);
235
+ }
236
+
237
+ /* ── Side panel ──────────────────────────────────────── */
238
+
239
  #panel {
240
+ background: var(--surface-2);
241
+ border-left: var(--stroke);
242
+ padding: var(--sp-s);
243
  overflow-y: auto;
244
  display: flex;
245
  flex-direction: column;
246
+ gap: var(--sp-s);
247
  }
248
 
249
+ /* ── Sections ────────────────────────────────────────── */
250
+
251
  .section {
252
+ background: var(--surface-3);
253
+ border-radius: var(--radious-m);
254
+ overflow: hidden;
255
  }
256
+
257
  .section-title {
258
+ font-size: 12px;
259
+ font-weight: 500;
260
+ letter-spacing: 0;
261
  text-transform: uppercase;
262
+ color: var(--content-lo);
263
+ padding: var(--sp-s) var(--sp-m);
 
 
264
  }
265
+
266
  .section-body {
267
+ padding: 0 var(--sp-m) var(--sp-m);
268
  }
269
 
270
+ /* ── Stats grid ──────────────────────────────────────── */
271
+
272
  .stats-grid {
273
  display: grid;
274
  grid-template-columns: 1fr 1fr;
275
  gap: 1px;
276
+ background: var(--surface-2);
277
  }
278
+
279
  .stat {
280
+ background: var(--surface-4);
281
+ padding: var(--sp-s) var(--sp-m);
282
  }
283
+
284
  .stat .lbl {
285
+ font-size: 12px;
286
+ font-weight: 500;
287
+ letter-spacing: 0;
288
  text-transform: uppercase;
289
+ color: var(--content-lo);
290
  margin-bottom: 2px;
291
  }
292
+
293
  .stat .val {
294
+ font-size: 20px;
295
+ font-weight: 400;
296
+ color: var(--content-hi);
297
+ line-height: 1.2;
 
 
 
 
 
298
  }
299
 
300
+ .stat .val.active { color: var(--success-content); }
301
+ .stat .val.inactive { color: var(--danger-content); }
302
+
303
  .stats-extra {
304
  margin-top: 1px;
305
  display: grid;
306
  grid-template-columns: 1fr 1fr;
307
  gap: 1px;
308
+ background: var(--surface-2);
309
  }
310
+
311
  .stats-extra .stat-risk {
312
  grid-column: 1 / -1;
 
 
 
 
313
  }
314
+
315
+ .st-sm { font-size: 16px; }
316
+
317
  .stat-sub {
318
+ font-size: 11px;
319
+ font-weight: 400;
320
+ color: var(--content-lo);
321
  margin-top: 2px;
 
322
  }
323
+
324
  .risk-row {
325
  display: flex;
326
  align-items: center;
327
+ gap: var(--sp-s);
328
+ margin-top: 4px;
329
  }
330
+
331
  .st-risk-val {
332
+ font-size: 16px;
333
+ min-width: 44px;
334
  }
335
+
336
  .risk-track {
337
  flex: 1;
338
  height: 3px;
339
+ background: var(--surface-5);
340
+ border-radius: 2px;
341
  overflow: hidden;
342
  }
343
+
344
  .risk-fill {
345
  height: 100%;
346
  width: 0%;
347
+ background: var(--danger);
348
+ border-radius: 2px;
349
+ transition: width 0.4s ease-in-out;
350
  }
351
 
352
+ /* ── Controls ────────────────────────────────────────── */
353
+
354
  .ctrl {
355
+ margin-bottom: var(--sp-m);
356
  }
357
+
358
  .ctrl:last-child {
359
  margin-bottom: 0;
360
  }
361
+
362
  .ctrl label {
363
  display: flex;
364
  justify-content: space-between;
365
+ font-size: 12px;
366
+ font-weight: 500;
367
+ color: var(--content-lo);
368
+ margin-bottom: var(--sp-xs);
369
  text-transform: uppercase;
370
  }
371
+
372
  .ctrl label span {
373
+ color: var(--content-hi);
374
+ font-size: 14px;
375
+ font-weight: 400;
376
  }
377
 
378
+ /* Range slider */
379
  input[type='range'] {
380
  width: 100%;
381
  -webkit-appearance: none;
382
+ appearance: none;
383
+ height: 3px;
384
+ background: var(--surface-5);
385
  outline: none;
386
  cursor: pointer;
387
+ border-radius: 2px;
388
  }
389
+
390
  input[type='range']::-webkit-slider-thumb {
391
  -webkit-appearance: none;
392
+ appearance: none;
393
+ width: 14px;
394
+ height: 14px;
395
+ background: var(--primary);
396
  cursor: pointer;
397
  border-radius: 50%;
 
398
  }
399
+
400
  input[type='range']::-moz-range-thumb {
401
+ width: 14px;
402
+ height: 14px;
403
+ background: var(--primary);
404
  cursor: pointer;
405
  border-radius: 50%;
406
  border: none;
407
  }
408
 
409
+ /* Direction toggle */
410
  .dir-toggle {
411
  display: flex;
412
+ gap: var(--sp-xs);
413
+ margin-top: var(--sp-xs);
414
  }
415
+
416
  .dir-btn {
417
  flex: 1;
418
+ height: 40px;
419
+ background: var(--surface-4);
420
+ border: 1px solid rgba(255, 255, 255, 0.1);
421
+ border-radius: var(--radious-s);
422
+ color: var(--content-lo);
423
  font-family: var(--font);
424
+ font-size: 14px;
425
+ font-weight: 500;
426
+ letter-spacing: 0.015em;
427
  cursor: pointer;
428
  text-align: center;
429
  text-transform: uppercase;
430
+ transition: background 0.2s ease-in-out, color 0.2s ease-in-out, border-color 0.2s ease-in-out;
431
  }
432
+
433
  .dir-btn:hover {
434
+ background: var(--surface-5);
435
+ color: var(--content-hi);
436
  }
437
+
438
  .dir-btn.active {
439
+ background: var(--primary-dim);
440
+ border-color: var(--primary);
441
+ color: var(--primary-content);
442
  }
443
 
444
+ /* Hint */
445
  .hint {
446
+ font-size: 12px;
447
+ font-weight: 400;
448
+ color: var(--content-lo);
449
+ line-height: 1.5;
 
 
 
 
 
 
 
450
  }
451
 
452
+ .hint-s { color: var(--danger-content); }
453
+ .hint-e { color: var(--warning-content); }
454
+
455
+ /* ── Legend ──────────────────────────────────────────── */
456
+
457
  .legend {
458
  display: flex;
459
  flex-direction: column;
460
+ gap: var(--sp-s);
461
  }
462
+
463
  .leg-item {
464
  display: flex;
465
  align-items: center;
466
+ gap: var(--sp-s);
467
+ font-size: 12px;
468
+ font-weight: 400;
469
+ color: var(--content-mid);
470
+ text-transform: uppercase;
471
  }
472
+
473
  .leg-dot {
474
+ width: 8px;
475
+ height: 8px;
476
  border-radius: 50%;
477
  flex-shrink: 0;
478
  }
479
+
480
+ .leg-dot--cam { background: #0A67C2; }
481
+ .leg-dot--boat { background: #C20A20; }
482
+ .leg-dot--det { background: #14A87A; }
483
+
 
 
 
 
484
  .leg-line {
485
+ width: 16px;
486
  height: 1px;
487
  flex-shrink: 0;
488
  }
489
+
490
+ .leg-line--rings { background: rgba(10, 103, 194, 0.5); }
491
+
492
  .leg-line--arc {
493
+ border-top: 1px dashed rgba(10, 103, 194, 0.5);
 
494
  height: 0;
 
495
  }
496
 
497
+ /* ── Responsive ──────────────────────────────────────── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
 
499
  @media (max-width: 700px) {
500
  .coastal-surveillance-sim {
501
  grid-template-columns: 1fr;
502
+ grid-template-rows: 40px 55vw 1fr;
503
  }
504
  #panel {
505
  border-left: none;
506
+ border-top: var(--stroke);
507
  }
508
  }