feat migrate frontend to Bootstrap 5.3

Replace the custom CSS design system with Bootstrap 5.3.3 (CDN). A thin supplemental app.css is retained for project-specific styles that Bootstrap does not provide. Changes: - layouts/default.html.ep: load Bootstrap CSS + JS bundle from CDN; replace custom sticky header and checkbox hamburger with navbar/navbar-expand-md/navbar-toggler - toolbar.html.ep: replace custom breadcrumb with Bootstrap breadcrumb ol - graph-control.html.ep: replace custom timeframe pill control with Bootstrap btn-group / btn-sm buttons - index.html.ep: replace custom status-band grid with Bootstrap row/col - probe-card.html.ep: replace custom card and details/summary menu with Bootstrap card and Bootstrap dropdown - probes.html.ep: replace custom probes-grid with Bootstrap row/col - graph.html.ep: replace custom message-log card/table with Bootstrap card and table-responsive / table-sm / table-hover - status.html.ep: replace custom status card with Bootstrap card; status badge uses Bootstrap badge + custom state classes; dl uses Bootstrap row/col grid - downloads.html.ep: replace custom form controls with Bootstrap form-select, btn-group btn-check radios, form-control date inputs, and btn btn-success submit button - public/css/app.css: trimmed from ~700 to 297 lines; retained only LED animations, status-pill state colours, status-node-marker dot, reading-range gradient and marker, chart-wrap sizing, probes-grid aspect-ratio rules, tab-panel visibility, timeframe-adjustment show/hide, and mobile graph-controls collapse - t/00-homepage.t, t/01-graph-page.t, t/05-status-page.t, t/06-downloads.t: update CSS selectors to match new Bootstrap HTML structure; all data-* behaviour hooks are preserved and unchanged All data-* JavaScript hooks are preserved; no JS files were modified. prove t/: 8 files, 234 tests, all pass.

Commit
e2280d118ccc11b54d03e3a91f60d4ef3e5dafc4
Author
claude-sonnet-4 high <agent@anthropic.com>
Author date
Committer
claude-sonnet-4 high <agent@anthropic.com>
Committer date
Changed files
roles/dashboard/public/css/app.css
index aa2c7163..2a57e366 100644..100644
@@ -1,1477 +1,297 @@
1 1 /* -*- mode: css; -*- */
2 2
3 Removed: :root {
4 Removed: --bg: #f4f7f5;
5 Removed: --card: #ffffff;
6 Removed: --text: #17211b;
7 Removed: --muted: #66736a;
8 Removed: --border: #d8e2dc;
9 Removed: --accent: #276749;
10 Removed: --accent-soft: #e4f3ea;
11 Removed: --danger: #b42318;
12 Removed: --danger-soft: #fee4e2;
13 Removed: --warn: #b54708;
14 Removed: --warn-soft: #fff4d6;
15 Removed: --unknown: #475467;
16 Removed: --unknown-soft: #eef2f6;
17 Removed: }
18 Removed:
19 Removed: * {
20 Removed: box-sizing: border-box;
21 Removed: }
22 Removed:
23 Removed: body {
24 Removed: margin: 0;
25 Removed: font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
26 Removed: background: var(--bg);
27 Removed: color: var(--text);
28 Removed: }
29 Removed:
30 Removed: .container,
31 Removed: .container-wide {
32 Removed: width: calc(100% - 2rem);
33 Removed: margin: 0 auto 2rem;
34 Removed: }
35 Removed:
36 Removed: .container {
37 Removed: max-width: 56rem;
38 Removed: }
39 Removed:
40 Removed: .container-wide {
41 Removed: max-width: 100rem;
42 Removed: }
43 Removed:
44 Removed: .site-footer {
45 Removed: width: min(1100px, calc(100% - 2rem));
46 Removed: margin: 0 auto 2rem;
47 Removed: color: var(--muted);
48 Removed: font-size: 0.88rem;
49 Removed: text-align: center;
50 Removed: }
51 Removed:
52 Removed: .site-footer p {
53 Removed: margin: 0.2rem 0;
54 Removed: }
55 Removed:
56 Removed: .site-footer a {
57 Removed: color: inherit;
58 Removed: }
59 Removed:
60 Removed: .page-header {
61 Removed: position: sticky;
62 Removed: top: 0;
63 Removed: z-index: 20;
64 Removed: display: flex;
65 Removed: align-items: center;
66 Removed: justify-content: flex-start;
67 Removed: gap: 1rem;
68 Removed: width: 100%;
69 Removed: margin: 0 0 0.75rem;
70 Removed: border: 1px solid var(--border);
71 Removed: border-radius: 0 0 0.5rem 0.5rem;
72 Removed: padding: 0.7rem 0.85rem;
73 Removed: background: var(--card);
74 Removed: box-shadow: 0 8px 24px rgba(23, 33, 27, 0.08);
75 Removed: }
76 Removed:
77 Removed: .toolbar {
78 Removed: --toolbar-item-height: 2.75rem;
79 Removed:
80 Removed: display: flex;
81 Removed: align-items: center;
82 Removed: justify-content: space-between;
83 Removed: gap: 1rem;
84 Removed: width: 100%;
85 Removed: margin: 0 0 1rem;
86 Removed: }
87 Removed:
88 Removed: .breadcrumbs {
89 Removed: display: inline-flex;
90 Removed: align-self: flex-start;
91 Removed: align-items: center;
92 Removed: max-width: 100%;
93 Removed: min-height: var(--toolbar-item-height);
94 Removed: min-width: 0;
95 Removed: border: 1px solid var(--border);
96 Removed: border-radius: 999px;
97 Removed: padding: 0.45rem 0.8rem;
98 Removed: background: var(--card);
99 Removed: color: var(--muted);
100 Removed: font-size: 0.88rem;
101 Removed: }
102 Removed:
103 Removed: .breadcrumbs ol {
104 Removed: display: flex;
105 Removed: flex-wrap: wrap;
106 Removed: gap: 0.4rem;
107 Removed: align-items: center;
108 Removed: margin: 0;
109 Removed: padding: 0;
110 Removed: list-style: none;
111 Removed: }
112 Removed:
113 Removed: .breadcrumbs li {
114 Removed: display: inline-flex;
115 Removed: gap: 0.4rem;
116 Removed: align-items: center;
117 Removed: }
118 Removed:
119 Removed: .breadcrumbs li + li::before {
120 Removed: color: var(--muted);
121 Removed: content: "/";
122 Removed: font-weight: 700;
123 Removed: }
124 Removed:
125 Removed: .breadcrumbs a {
126 Removed: color: var(--accent);
127 Removed: font-weight: 700;
128 Removed: text-decoration: none;
129 Removed: }
130 Removed:
131 Removed: .breadcrumbs a:hover {
132 Removed: text-decoration: underline;
133 Removed: }
134 Removed:
135 Removed: .breadcrumbs [aria-current="page"] {
136 Removed: color: var(--text);
137 Removed: font-weight: 700;
138 Removed: }
139 Removed:
140 Removed: .toolbar-items {
141 Removed: display: flex;
142 Removed: flex: 0 1 auto;
143 Removed: align-items: center;
144 Removed: justify-content: flex-end;
145 Removed: min-height: var(--toolbar-item-height);
146 Removed: min-width: 0;
147 Removed: margin-left: auto;
148 Removed: }
149 Removed:
150 Removed: .toolbar .graph-control,
151 Removed: .toolbar .status-pill,
152 Removed: .toolbar .timeframe-control {
153 Removed: min-height: var(--toolbar-item-height);
154 Removed: }
155 Removed:
156 Removed: .toolbar .status-pill {
157 Removed: align-items: center;
158 Removed: justify-content: center;
159 Removed: }
160 Removed:
161 Removed: .page-brand {
162 Removed: color: var(--accent);
163 Removed: font-size: 1.15rem;
164 Removed: font-weight: 700;
165 Removed: text-decoration: none;
166 Removed: }
167 Removed:
168 Removed: .page-brand:hover {
169 Removed: text-decoration: underline;
170 Removed: }
171 Removed:
172 Removed: .nav-links {
173 Removed: display: flex;
174 Removed: flex-wrap: wrap;
175 Removed: justify-content: flex-end;
176 Removed: gap: 0.4rem;
177 Removed: margin-left: auto;
178 Removed: }
179 Removed:
180 Removed: .nav-links a {
181 Removed: border-radius: 999px;
182 Removed: padding: 0.5rem 0.75rem;
183 Removed: color: var(--text);
184 Removed: font-size: 0.9rem;
185 Removed: font-weight: 700;
186 Removed: text-decoration: none;
187 Removed: }
188 Removed:
189 Removed: .nav-links a:hover {
190 Removed: background: var(--accent-soft);
191 Removed: color: var(--accent);
192 Removed: }
193 Removed:
194 Removed: .downloads-form {
195 Removed: display: flex;
196 Removed: flex: 1;
197 Removed: flex-direction: column;
198 Removed: gap: 1rem;
199 Removed: }
200 Removed:
201 Removed: .downloads-card {
202 Removed: display: flex;
203 Removed: flex-direction: column;
204 Removed: min-height: 20rem;
205 Removed: }
206 Removed:
207 Removed: .downloads-card .card-body {
208 Removed: display: flex;
209 Removed: flex: 1;
210 Removed: }
211 Removed:
212 Removed: .download-date-fields {
213 Removed: display: flex;
214 Removed: flex-wrap: wrap;
215 Removed: gap: 1rem;
216 Removed: }
217 Removed:
218 Removed: .download-date-fields[hidden] {
219 Removed: display: none;
220 Removed: }
221 Removed:
222 Removed: .download-date-field {
223 Removed: display: grid;
224 Removed: gap: 0.35rem;
225 Removed: }
226 Removed:
227 Removed: .download-range-fieldset {
228 Removed: display: grid;
229 Removed: gap: 0.35rem;
230 Removed: margin: 0;
231 Removed: border: 0;
232 Removed: padding: 0;
233 Removed: }
234 Removed:
235 Removed: .download-range-fieldset legend {
236 Removed: margin-bottom: 0.35rem;
237 Removed: padding: 0;
238 Removed: font-weight: 700;
239 Removed: }
240 Removed:
241 Removed: .download-range-control {
242 Removed: display: inline-flex;
243 Removed: align-self: flex-start;
244 Removed: overflow: hidden;
245 Removed: border: 1px solid var(--border);
246 Removed: border-radius: 0.4rem;
247 Removed: background: white;
248 Removed: }
249 Removed:
250 Removed: .download-range-control label {
251 Removed: position: relative;
252 Removed: margin: 0;
253 Removed: cursor: pointer;
254 Removed: }
255 Removed:
256 Removed: .download-range-control input {
257 Removed: position: absolute;
258 Removed: width: 1px;
259 Removed: height: 1px;
260 Removed: opacity: 0;
261 Removed: }
262 Removed:
263 Removed: .download-range-control span {
264 Removed: display: inline-flex;
265 Removed: align-items: center;
266 Removed: justify-content: center;
267 Removed: min-height: 2.75rem;
268 Removed: border: 0;
269 Removed: border-left: 1px solid var(--border);
270 Removed: padding: 0.5rem 0.75rem;
271 Removed: background: white;
272 Removed: color: var(--text);
273 Removed: cursor: pointer;
274 Removed: font: inherit;
275 Removed: font-weight: 700;
276 Removed: }
277 Removed:
278 Removed: .download-range-control label:first-child span {
279 Removed: border-left: 0;
280 Removed: }
281 Removed:
282 Removed: .download-range-control label:hover span {
283 Removed: background: var(--accent-soft);
284 Removed: }
285 Removed:
286 Removed: .download-range-control input:checked + span {
287 Removed: background: var(--accent);
288 Removed: color: white;
289 Removed: }
290 Removed:
291 Removed: .download-range-control input:focus-visible + span {
292 Removed: position: relative;
293 Removed: z-index: 1;
294 Removed: outline: 2px solid var(--accent);
295 Removed: outline-offset: -2px;
296 Removed: }
297 Removed:
298 Removed:
299 Removed: .download-probe-field {
300 Removed: display: grid;
301 Removed: gap: 0.35rem;
302 Removed: align-self: flex-start;
303 Removed: }
304 Removed:
305 Removed: .download-date-field label,
306 Removed: .download-probe-field label {
307 Removed: font-weight: 700;
308 Removed: }
309 Removed:
310 Removed: .download-date-field input,
311 Removed: .download-probe-field select,
312 Removed: .downloads-button,
313 Removed: .secondary-button {
314 Removed: min-height: 2.75rem;
315 Removed: border: 1px solid var(--border);
316 Removed: border-radius: 0.4rem;
317 Removed: padding: 0.5rem 0.65rem;
318 Removed: font: inherit;
319 Removed: }
320 Removed:
321 Removed: .download-probe-field select {
322 Removed: min-width: 14rem;
323 Removed: background: white;
324 Removed: color: var(--text);
325 Removed: }
326 Removed:
327 Removed: .downloads-button {
328 Removed: align-self: flex-end;
329 Removed: margin-top: auto;
330 Removed: border-color: var(--accent);
331 Removed: background: var(--accent);
332 Removed: color: white;
333 Removed: font-weight: 700;
334 Removed: cursor: pointer;
335 Removed: }
336 Removed:
337 Removed: .downloads-button:hover {
338 Removed: filter: brightness(0.9);
339 Removed: }
340 Removed:
341 Removed: .secondary-button {
342 Removed: display: inline-flex;
343 Removed: align-items: center;
344 Removed: justify-content: center;
345 Removed: background: white;
346 Removed: color: var(--accent);
347 Removed: font-weight: 700;
348 Removed: cursor: pointer;
349 Removed: text-decoration: none;
350 Removed: }
351 Removed:
352 Removed: .secondary-button:hover {
353 Removed: border-color: var(--accent);
354 Removed: background: var(--accent-soft);
355 Removed: }
356 Removed:
357 Removed: .header-status-link {
358 Removed: position: relative;
359 Removed: display: inline-grid;
360 Removed: flex: 0 0 auto;
361 Removed: place-items: center;
362 Removed: width: 1.35rem;
363 Removed: height: 1.35rem;
364 Removed: border-radius: 50%;
365 Removed: color: inherit;
366 Removed: text-decoration: none;
367 Removed: }
368 Removed:
369 Removed: .header-status-link:focus-visible {
370 Removed: outline: 2px solid var(--accent);
371 Removed: outline-offset: 0.25rem;
372 Removed: }
373 Removed:
374 Removed: .header-status-led {
375 Removed: width: 0.85rem;
376 Removed: height: 0.85rem;
377 Removed: border: 1px solid rgba(23, 33, 27, 0.22);
378 Removed: border-radius: 50%;
379 Removed: background: radial-gradient(circle at 32% 28%, #ffffff 0 8%, #98a2b3 18%, #475467 70%);
380 Removed: box-shadow:
381 Removed: inset 0 -1px 2px rgba(23, 33, 27, 0.45),
382 Removed: 0 0 0.35rem rgba(71, 84, 103, 0.45);
383 Removed: }
384 Removed:
385 Removed: .header-status-link.is-healthy .header-status-led {
386 Removed: background: radial-gradient(circle at 32% 28%, #ffffff 0 8%, #86efac 18%, #16a34a 68%, #14532d 100%);
387 Removed: animation: led-green 1.35s ease-in-out infinite;
388 Removed: }
389 Removed:
390 Removed: .header-status-link.is-warning .header-status-led {
391 Removed: background: radial-gradient(circle at 32% 28%, #ffffff 0 8%, #fca5a5 18%, #dc2626 68%, #7f1d1d 100%);
392 Removed: animation: led-red 1s ease-in-out infinite;
393 Removed: }
394 Removed:
395 Removed: @keyframes led-green {
396 Removed: 0%,
397 Removed: 100% {
398 Removed: box-shadow:
399 Removed: inset 0 -1px 2px rgba(23, 33, 27, 0.45),
400 Removed: 0 0 0.3rem rgba(22, 163, 74, 0.45);
401 Removed: opacity: 0.78;
402 Removed: }
403 Removed:
404 Removed: 50% {
405 Removed: box-shadow:
406 Removed: inset 0 -1px 2px rgba(23, 33, 27, 0.45),
407 Removed: 0 0 0.85rem rgba(22, 163, 74, 0.95),
408 Removed: 0 0 1.25rem rgba(134, 239, 172, 0.45);
409 Removed: opacity: 1;
410 Removed: }
411 Removed: }
412 Removed:
413 Removed: @keyframes led-red {
414 Removed: 0%,
415 Removed: 100% {
416 Removed: box-shadow:
417 Removed: inset 0 -1px 2px rgba(23, 33, 27, 0.45),
418 Removed: 0 0 0.35rem rgba(220, 38, 38, 0.5);
419 Removed: opacity: 0.75;
420 Removed: }
421 Removed:
422 Removed: 50% {
423 Removed: box-shadow:
424 Removed: inset 0 -1px 2px rgba(23, 33, 27, 0.45),
425 Removed: 0 0 0.9rem rgba(220, 38, 38, 1),
426 Removed: 0 0 1.35rem rgba(252, 165, 165, 0.5);
427 Removed: opacity: 1;
428 Removed: }
429 Removed: }
430 Removed:
431 Removed: .nav-toggle {
432 Removed: display: none;
433 Removed: }
434 Removed:
435 Removed: .nav-toggle-button {
436 Removed: display: none;
437 Removed: }
438 Removed:
439 Removed: .status-band {
440 Removed: margin-bottom: 1.25rem;
441 Removed: }
442 Removed:
443 Removed: .section-heading {
444 Removed: display: flex;
445 Removed: justify-content: space-between;
446 Removed: gap: 1rem;
447 Removed: align-items: end;
448 Removed: margin-bottom: 0.75rem;
449 Removed: }
450 Removed:
451 Removed: .section-heading h2 {
452 Removed: margin: 0;
453 Removed: font-size: 1.15rem;
454 Removed: }
455 Removed:
456 Removed: .section-heading p {
457 Removed: margin: 0;
458 Removed: color: var(--muted);
459 Removed: font-size: 0.95rem;
460 Removed: }
461 Removed:
462 Removed: .status-overview {
463 Removed: display: grid;
464 Removed: gap: 0.75rem;
465 Removed: justify-items: start;
466 Removed: }
467 Removed:
468 Removed: .status-pill {
469 Removed: display: inline-flex;
470 Removed: align-items: center;
471 Removed: justify-content: center;
472 Removed: gap: 0.5rem;
473 Removed: min-width: 8.5rem;
474 Removed: border-radius: 999px;
475 Removed: padding: 0.45rem 0.8rem;
476 Removed: font-size: 0.82rem;
477 Removed: font-weight: 700;
478 Removed: white-space: nowrap;
479 Removed: }
480 Removed:
481 Removed: .status-pill[href] {
482 Removed: color: inherit;
483 Removed: text-decoration: none;
484 Removed: }
485 Removed:
486 Removed: .status-pill[href]:hover {
487 Removed: filter: brightness(0.97);
488 Removed: }
489 Removed:
490 Removed: .status-summary {
491 Removed: min-width: 12rem;
492 Removed: }
493 Removed:
494 Removed: .status-pill.is-online {
495 Removed: background: var(--accent-soft);
496 Removed: color: var(--accent);
497 Removed: }
498 Removed:
499 Removed: .status-pill.is-healthy {
500 Removed: background: var(--accent-soft);
501 Removed: color: var(--accent);
502 Removed: }
503 Removed:
504 Removed: .status-pill.is-stale {
505 Removed: background: var(--warn-soft);
506 Removed: color: var(--warn);
507 Removed: }
508 Removed:
509 Removed: .status-pill.is-probe-error {
510 Removed: background: var(--warn-soft);
511 Removed: color: var(--warn);
512 Removed: }
513 Removed:
514 Removed: .status-pill.is-offline {
515 Removed: background: var(--danger-soft);
516 Removed: color: var(--danger);
517 Removed: }
518 Removed:
519 Removed: .status-pill.is-unreachable {
520 Removed: background: var(--danger-soft);
521 Removed: color: var(--danger);
522 Removed: }
523 Removed:
524 Removed: .status-pill.is-unknown {
525 Removed: background: var(--unknown-soft);
526 Removed: color: var(--unknown);
527 Removed: }
528 Removed:
529 Removed: .reading-quick-status {
530 Removed: width: 100%;
531 Removed: margin-top: 0.35rem;
532 Removed: }
533 Removed:
534 Removed: .reading-quick-status h2 {
535 Removed: margin: 0 0 0.7rem;
536 Removed: font-size: 1rem;
537 Removed: }
538 Removed:
539 Removed: .reading-quick-grid {
540 Removed: display: grid;
541 Removed: grid-template-columns: repeat(4, minmax(0, 1fr));
542 Removed: gap: 0.75rem;
543 Removed: }
544 Removed:
545 Removed: .reading-quick-link {
546 Removed: min-width: 0;
547 Removed: color: inherit;
548 Removed: text-decoration: none;
549 Removed: }
550 Removed:
551 Removed: .reading-quick-link:hover .reading-quick-item,
552 Removed: .reading-quick-link:focus-visible .reading-quick-item {
553 Removed: border-color: var(--accent);
554 Removed: }
555 Removed:
556 Removed: .reading-quick-link:focus-visible {
557 Removed: border-radius: 0.5rem;
558 Removed: outline: 2px solid var(--accent);
559 Removed: outline-offset: 2px;
560 Removed: }
561 Removed:
562 Removed: .reading-quick-item {
563 Removed: --warn-low: 27%;
564 Removed: --good-start: 35%;
565 Removed: --good-end: 65%;
566 Removed: --warn-high: 73%;
567 Removed: display: grid;
568 Removed: gap: 0.7rem;
569 Removed: min-width: 0;
570 Removed: height: 100%;
571 Removed: border: 1px solid var(--border);
572 Removed: border-radius: 0.5rem;
573 Removed: padding: 0.85rem;
574 Removed: background: var(--card);
575 Removed: }
576 Removed:
577 Removed: .reading-quick-header {
578 Removed: display: grid;
579 Removed: gap: 0.2rem;
580 Removed: min-height: 3.15rem;
581 Removed: }
582 Removed:
583 Removed: .reading-quick-header h3 {
584 Removed: margin: 0;
585 Removed: font-size: 0.9rem;
586 Removed: }
587 Removed:
588 Removed: .reading-quick-header p {
589 Removed: margin: 0;
590 Removed: color: var(--muted);
591 Removed: font-size: 0.82rem;
592 Removed: line-height: 1.3;
593 Removed: overflow-wrap: anywhere;
594 Removed: }
595 Removed:
596 Removed: .reading-range {
597 Removed: position: relative;
598 Removed: height: 0.85rem;
599 Removed: border-radius: 999px;
600 Removed: background:
601 Removed: linear-gradient(
602 Removed: 90deg,
603 Removed: var(--danger) 0%,
604 Removed: var(--warn) var(--warn-low),
605 Removed: var(--accent) var(--good-start),
606 Removed: var(--accent) var(--good-end),
607 Removed: var(--warn) var(--warn-high),
608 Removed: var(--danger) 100%
609 Removed: );
610 Removed: }
611 Removed:
612 Removed: .reading-range-marker {
613 Removed: position: absolute;
614 Removed: top: 50%;
615 Removed: left: 50%;
616 Removed: width: 1rem;
617 Removed: height: 1rem;
618 Removed: border: 2px solid white;
619 Removed: border-radius: 50%;
620 Removed: background: var(--unknown);
621 Removed: box-shadow: 0 1px 4px rgba(23, 33, 27, 0.35);
622 Removed: transform: translate(-50%, -50%);
623 Removed: }
624 Removed:
625 Removed: .reading-quick-item.is-good .reading-range-marker {
626 Removed: background: var(--accent);
627 Removed: }
628 Removed:
629 Removed: .reading-quick-item.is-low .reading-range-marker,
630 Removed: .reading-quick-item.is-high .reading-range-marker {
631 Removed: background: var(--danger);
632 Removed: }
633 Removed:
634 Removed: .reading-range-labels {
635 Removed: display: grid;
636 Removed: grid-template-columns: 1fr auto 1fr;
637 Removed: gap: 0.35rem;
638 Removed: color: var(--muted);
639 Removed: font-size: 0.72rem;
640 Removed: line-height: 1.25;
641 Removed: }
642 Removed:
643 Removed: .reading-range-labels span:nth-child(2) {
644 Removed: color: var(--accent);
645 Removed: font-weight: 700;
646 Removed: text-align: center;
647 Removed: }
648 Removed:
649 Removed: .reading-range-labels span:last-child {
650 Removed: text-align: right;
651 Removed: }
652 Removed:
653 Removed: .card {
654 Removed: background: var(--card);
655 Removed: border: 1px solid var(--border);
656 Removed: border-radius: 0.5rem;
657 Removed: box-shadow: 0 10px 30px rgba(0, 0, 0, 0.06);
658 Removed: overflow: hidden;
659 Removed: }
660 Removed:
661 Removed: .card + .card {
662 Removed: margin-top: 1rem;
663 Removed: }
664 Removed:
665 Removed: .probes-grid {
666 Removed: display: grid;
667 Removed: grid-template-columns: 1fr;
668 Removed: gap: 1rem;
669 Removed: }
670 Removed:
671 Removed: @media (min-width: 701px) {
672 Removed: .probes-grid {
673 Removed: grid-template-columns: repeat(2, minmax(0, 1fr));
674 Removed: }
675 Removed: }
676 Removed:
677 Removed: @media (min-width: 1100px) {
678 Removed: .probes-grid {
679 Removed: grid-template-columns: repeat(3, minmax(0, 1fr));
680 Removed: }
681 Removed: }
682 Removed:
683 Removed: @media (min-width: 1500px) {
684 Removed: .probes-grid {
685 Removed: grid-template-columns: repeat(4, minmax(0, 1fr));
686 Removed: }
687 Removed: }
688 Removed:
689 Removed: .graph-control {
690 Removed: display: flex;
691 Removed: flex-wrap: wrap;
692 Removed: justify-content: flex-end;
693 Removed: align-items: center;
694 Removed: gap: 0.75rem;
695 Removed: margin: 0;
696 Removed: }
697 Removed:
698 Removed: .graph-controls-disclosure,
699 Removed: .graph-controls-panel {
700 Removed: display: flex;
701 Removed: align-items: center;
702 Removed: }
703 Removed:
704 Removed: .graph-controls-toggle {
705 Removed: display: none;
706 Removed: }
707 Removed:
708 Removed: .graph-control .timeframe-option {
709 Removed: display: flex;
710 Removed: min-width: 4.75rem;
711 Removed: }
712 Removed:
713 Removed: .graph-control .timeframe-option + .timeframe-option {
714 Removed: border-left: 1px solid var(--border);
715 Removed: }
716 Removed:
717 Removed: .graph-control .timeframe-button {
718 Removed: width: 100%;
719 Removed: border-left: 0;
720 Removed: }
721 Removed:
722 Removed: .graph-control .timeframe-option.is-expanded {
723 Removed: min-width: 12rem;
724 Removed: }
725 Removed:
726 Removed: .timeframe-adjustment {
727 Removed: display: none;
728 Removed: min-width: 2.75rem;
729 Removed: border: 0;
730 Removed: padding: 0.45rem 0.75rem;
731 Removed: background: var(--accent-soft);
732 Removed: color: var(--accent);
733 Removed: cursor: pointer;
734 Removed: font: inherit;
735 Removed: font-size: 1.15rem;
736 Removed: font-weight: 700;
737 Removed: }
738 Removed:
739 Removed: .timeframe-option.is-expanded .timeframe-adjustment {
740 Removed: display: block;
741 Removed: }
742 Removed:
743 Removed: .timeframe-adjustment:hover:not(:disabled) {
744 Removed: background: var(--accent);
745 Removed: color: white;
746 Removed: }
747 Removed:
748 Removed: .timeframe-adjustment:disabled {
749 Removed: color: var(--muted);
750 Removed: cursor: not-allowed;
751 Removed: }
752 Removed:
753 Removed: .probes-grid .card + .card {
754 Removed: margin-top: 0;
755 Removed: }
756 Removed:
757 Removed: .probes-grid .card {
758 Removed: display: flex;
759 Removed: flex-direction: column;
760 Removed: aspect-ratio: 1 / 1;
761 Removed: }
762 Removed:
763 Removed: .probes-grid .card-body {
764 Removed: display: flex;
765 Removed: flex: 1;
766 Removed: flex-direction: column;
767 Removed: min-height: 0;
768 Removed: }
769 Removed:
770 Removed: .probes-grid .tab-panel.is-active {
771 Removed: display: flex;
772 Removed: flex: 1;
773 Removed: flex-direction: column;
774 Removed: min-height: 0;
775 Removed: }
776 Removed:
777 Removed: .probes-grid .chart-wrap {
778 Removed: flex: 1;
779 Removed: min-height: 0;
780 Removed: max-height: none;
781 Removed: aspect-ratio: auto;
782 Removed: }
783 Removed:
784 Removed: .tabs {
785 Removed: display: flex;
786 Removed: flex-wrap: wrap;
787 Removed: gap: 0.5rem;
788 Removed: padding: 1rem;
789 Removed: border-bottom: 1px solid var(--border);
790 Removed: background: #fafcfb;
791 Removed: }
792 Removed:
793 Removed: .tab-button {
794 Removed: border: 1px solid var(--border);
795 Removed: border-radius: 999px;
796 Removed: min-height: 2.75rem;
797 Removed: padding: 0.6rem 1rem;
798 Removed: background: white;
799 Removed: color: var(--text);
800 Removed: cursor: pointer;
801 Removed: font: inherit;
802 Removed: }
803 Removed:
804 Removed: .tab-button:hover {
805 Removed: border-color: var(--accent);
806 Removed: }
807 Removed:
808 Removed: .tab-button.is-active {
809 Removed: background: var(--accent);
810 Removed: border-color: var(--accent);
811 Removed: color: white;
812 Removed: }
813 Removed:
814 Removed: .card-heading {
815 Removed: display: flex;
816 Removed: justify-content: space-between;
817 Removed: gap: 1rem;
818 Removed: align-items: flex-start;
819 Removed: padding: 1.25rem;
820 Removed: border-bottom: 1px solid var(--border);
821 Removed: }
822 Removed:
823 Removed: .card-heading h1,
824 Removed: .card-heading h2 {
825 Removed: margin: 0;
826 Removed: font-size: 1.35rem;
827 Removed: }
828 Removed:
829 Removed: .card-heading h1 a {
830 Removed: color: inherit;
831 Removed: text-decoration: none;
832 Removed: }
833 Removed:
834 Removed: .card-heading h1 a:hover {
835 Removed: color: var(--accent);
836 Removed: text-decoration: underline;
837 Removed: }
838 Removed:
839 Removed: .card-heading p {
840 Removed: margin: 0;
841 Removed: color: var(--muted);
842 Removed: }
843 Removed:
844 Removed: .probe-card-title {
845 Removed: min-width: 0;
846 Removed: }
847 Removed:
848 Removed: .probe-card-subtitle {
849 Removed: margin-top: 0.25rem !important;
850 Removed: font-size: 0.9rem;
851 Removed: }
852 Removed:
853 Removed: .probe-card-menu {
854 Removed: position: relative;
855 Removed: flex: 0 0 auto;
856 Removed: }
857 Removed:
858 Removed: .probe-card-menu summary {
859 Removed: display: grid;
860 Removed: place-items: center;
861 Removed: width: 2.5rem;
862 Removed: height: 2.5rem;
863 Removed: border: 1px solid var(--border);
864 Removed: border-radius: 0.5rem;
865 Removed: background: white;
866 Removed: color: var(--text);
867 Removed: cursor: pointer;
868 Removed: list-style: none;
869 Removed: }
870 Removed:
871 Removed: .probe-card-menu summary::-webkit-details-marker {
872 Removed: display: none;
873 Removed: }
874 Removed:
875 Removed: .probe-card-menu summary:hover,
876 Removed: .probe-card-menu[open] summary {
877 Removed: border-color: var(--accent);
878 Removed: background: var(--accent-soft);
879 Removed: color: var(--accent);
880 Removed: }
881 Removed:
882 Removed: .probe-card-menu summary:focus-visible {
883 Removed: outline: 2px solid var(--accent);
884 Removed: outline-offset: 2px;
885 Removed: }
886 Removed:
887 Removed: .probe-card-menu-icon {
888 Removed: width: 1rem;
889 Removed: height: 2px;
890 Removed: background: currentColor;
891 Removed: box-shadow: 0 -0.35rem currentColor, 0 0.35rem currentColor;
892 Removed: }
893 Removed:
894 Removed: .probe-card-menu-content {
895 Removed: position: absolute;
896 Removed: z-index: 10;
897 Removed: top: calc(100% + 0.35rem);
898 Removed: right: 0;
899 Removed: min-width: max-content;
900 Removed: border: 1px solid var(--border);
901 Removed: border-radius: 0.5rem;
902 Removed: padding: 0.35rem;
903 Removed: background: var(--card);
904 Removed: box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
905 Removed: }
906 Removed:
907 Removed: .probe-card-menu-content .secondary-button {
908 Removed: width: 100%;
909 Removed: }
910 Removed:
911 Removed: .card-body {
912 Removed: padding: 1.25rem;
913 Removed: }
914 Removed:
915 Removed: .tab-panel {
916 Removed: display: none;
917 Removed: scroll-margin-top: 5.5rem;
918 Removed: }
919 Removed:
920 Removed: .tab-panel.is-active {
921 Removed: display: block;
922 Removed: }
923 Removed:
924 Removed: .panel-tools {
925 Removed: display: flex;
926 Removed: flex-wrap: wrap;
927 Removed: justify-content: flex-end;
928 Removed: gap: 0.75rem;
929 Removed: align-items: center;
930 Removed: }
931 Removed:
932 Removed: .timeframe-control {
933 Removed: display: inline-flex;
934 Removed: overflow: hidden;
935 Removed: border: 1px solid var(--border);
936 Removed: border-radius: 999px;
937 Removed: background: white;
938 Removed: }
939 Removed:
940 Removed: .timeframe-button {
941 Removed: min-width: 4.75rem;
942 Removed: min-height: 2.75rem;
943 Removed: border: 0;
944 Removed: border-left: 1px solid var(--border);
945 Removed: padding: 0.45rem 0.75rem;
946 Removed: background: transparent;
947 Removed: color: var(--text);
948 Removed: cursor: pointer;
949 Removed: font: inherit;
950 Removed: font-size: 0.88rem;
951 Removed: font-weight: 700;
952 Removed: }
953 Removed:
954 Removed: .timeframe-button:first-child {
955 Removed: border-left: 0;
956 Removed: }
957 Removed:
958 Removed: .timeframe-button:hover {
959 Removed: background: var(--accent-soft);
960 Removed: }
961 Removed:
962 Removed: .timeframe-button.is-active {
963 Removed: background: var(--accent);
964 Removed: color: white;
965 Removed: }
966 Removed:
967 Removed: .chart-wrap {
968 Removed: position: relative;
969 Removed: width: 100%;
970 Removed: max-height: 420px;
971 Removed: aspect-ratio: 1 / 1;
972 Removed: }
973 Removed:
974 Removed: .reading-status {
975 Removed: margin: 1rem 0 0;
976 Removed: color: var(--muted);
977 Removed: font-size: 0.95rem;
978 Removed: }
979 Removed:
980 Removed: .message-log {
981 Removed: margin-top: 1.25rem;
982 Removed: }
983 Removed:
984 Removed: .message-log h2,
985 Removed: .message-log h3 {
986 Removed: margin: 0 0 0.75rem;
987 Removed: font-size: 1rem;
988 Removed: }
989 Removed:
990 Removed: .message-table-wrap {
991 Removed: overflow-x: auto;
992 Removed: -webkit-overflow-scrolling: touch;
993 Removed: border: 1px solid var(--border);
994 Removed: border-radius: 0.5rem;
995 Removed: }
996 Removed:
997 Removed: .message-table {
998 Removed: width: 100%;
999 Removed: min-width: 640px;
1000 Removed: border-collapse: collapse;
1001 Removed: }
1002 Removed:
1003 Removed: .message-table th,
1004 Removed: .message-table td {
1005 Removed: padding: 0.7rem 0.85rem;
1006 Removed: border-bottom: 1px solid var(--border);
1007 Removed: text-align: left;
1008 Removed: vertical-align: middle;
1009 Removed: }
1010 Removed:
1011 Removed: .message-table thead th {
1012 Removed: background: #fafcfb;
1013 Removed: color: var(--muted);
1014 Removed: font-size: 0.78rem;
1015 Removed: font-weight: 700;
1016 Removed: text-transform: uppercase;
1017 Removed: }
1018 Removed:
1019 Removed: .message-table tbody tr:last-child td {
1020 Removed: border-bottom: 0;
1021 Removed: }
1022 Removed:
1023 Removed: .status-probe-groups {
1024 Removed: display: grid;
1025 Removed: gap: 1.5rem;
1026 Removed: }
1027 Removed:
1028 Removed: .status-probe-group h2 {
1029 Removed: margin: 0 0 0.75rem;
1030 Removed: font-size: 1rem;
1031 Removed: }
1032 Removed:
1033 Removed: .status-probe-group h2 a {
1034 Removed: color: inherit;
1035 Removed: text-decoration: none;
1036 Removed: }
1037 Removed:
1038 Removed: .status-probe-group h2 a:hover {
1039 Removed: color: var(--accent);
1040 Removed: text-decoration: underline;
1041 Removed: }
1042 Removed:
1043 Removed: .status-node-sequence {
1044 Removed: margin: 0;
1045 Removed: padding: 0;
1046 Removed: list-style: none;
1047 Removed: }
1048 Removed:
1049 Removed: .status-node {
1050 Removed: position: relative;
1051 Removed: display: grid;
1052 Removed: grid-template-columns: 1.25rem minmax(0, 1fr);
1053 Removed: gap: 0.75rem;
1054 Removed: }
1055 Removed:
1056 Removed: .status-node + .status-node {
1057 Removed: margin-top: 0.75rem;
1058 Removed: }
1059 Removed:
1060 Removed: .status-node + .status-node::before {
1061 Removed: position: absolute;
1062 Removed: top: -0.75rem;
1063 Removed: bottom: calc(100% - 0.75rem);
1064 Removed: left: 0.55rem;
1065 Removed: width: 0.15rem;
1066 Removed: border-radius: 999px;
1067 Removed: background: var(--border);
1068 Removed: content: "";
1069 Removed: }
1070 Removed:
1071 Removed: .status-node-marker {
1072 Removed: z-index: 1;
1073 Removed: width: 1.25rem;
1074 Removed: height: 1.25rem;
1075 Removed: margin-top: 1rem;
1076 Removed: border: 0.3rem solid var(--unknown-soft);
1077 Removed: border-radius: 50%;
1078 Removed: background: var(--unknown);
1079 Removed: }
1080 Removed:
1081 Removed: .status-node-marker.is-online,
1082 Removed: .status-node-marker.is-healthy {
1083 Removed: border-color: var(--accent-soft);
1084 Removed: background: var(--accent);
1085 Removed: }
1086 Removed:
1087 Removed: .status-node-marker.is-stale,
1088 Removed: .status-node-marker.is-probe-error {
1089 Removed: border-color: var(--warn-soft);
1090 Removed: background: var(--warn);
1091 Removed: }
1092 Removed:
1093 Removed: .status-node-marker.is-offline,
1094 Removed: .status-node-marker.is-unreachable {
1095 Removed: border-color: var(--danger-soft);
1096 Removed: background: var(--danger);
1097 Removed: }
1098 Removed:
1099 Removed: .status-node-content {
1100 Removed: min-width: 0;
1101 Removed: border: 1px solid var(--border);
1102 Removed: border-radius: 0.5rem;
1103 Removed: padding: 1rem;
1104 Removed: background: #fafcfb;
1105 Removed: }
1106 Removed:
1107 Removed: .status-node-summary {
1108 Removed: display: flex;
1109 Removed: flex-wrap: wrap;
1110 Removed: align-items: center;
1111 Removed: justify-content: space-between;
1112 Removed: gap: 0.75rem;
1113 Removed: }
1114 Removed:
1115 Removed: .status-node-name {
1116 Removed: min-width: 0;
1117 Removed: margin: 0;
1118 Removed: overflow-wrap: anywhere;
1119 Removed: font-weight: 700;
1120 Removed: }
1121 Removed:
1122 Removed: .status-node-summary .status-pill {
1123 Removed: min-width: 7rem;
1124 Removed: padding-block: 0.3rem;
1125 Removed: }
1126 Removed:
1127 Removed: .status-node-details {
1128 Removed: display: grid;
1129 Removed: grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
1130 Removed: gap: 0.75rem 1rem;
1131 Removed: margin: 1rem 0 0;
1132 Removed: }
1133 Removed:
1134 Removed: .status-node-details div {
1135 Removed: min-width: 0;
1136 Removed: }
1137 Removed:
1138 Removed: .status-node-details dt {
1139 Removed: color: var(--muted);
1140 Removed: font-size: 0.78rem;
1141 Removed: font-weight: 700;
1142 Removed: }
1143 Removed:
1144 Removed: .status-node-details dd {
1145 Removed: margin: 0.25rem 0 0;
1146 Removed: overflow-wrap: anywhere;
1147 Removed: }
1148 Removed:
1149 Removed: @media (max-width: 900px) {
1150 Removed: .reading-quick-grid {
1151 Removed: grid-template-columns: repeat(2, minmax(0, 1fr));
1152 Removed: }
1153 Removed: }
1154 Removed:
1155 Removed: @media (max-width: 700px) {
1156 Removed: .container,
1157 Removed: .container-wide {
1158 Removed: width: min(100% - 1rem, 1100px);
1159 Removed: margin: 0 auto 1rem;
1160 Removed: }
1161 Removed:
1162 Removed: .site-footer {
1163 Removed: width: min(100% - 1rem, 1100px);
1164 Removed: margin-bottom: 1rem;
1165 Removed: }
1166 Removed:
1167 Removed: .toolbar {
1168 Removed: flex-direction: column;
1169 Removed: align-items: stretch;
1170 Removed: width: 100%;
1171 Removed: margin-bottom: 1rem;
1172 Removed: }
1173 Removed:
1174 Removed: .toolbar-items {
1175 Removed: width: 100%;
1176 Removed: margin-left: 0;
1177 Removed: }
1178 Removed:
1179 Removed: .graph-controls-disclosure {
1180 Removed: display: block;
1181 Removed: width: 100%;
1182 Removed: }
1183 Removed:
1184 Removed: .graph-controls-toggle {
1185 Removed: display: flex;
1186 Removed: align-items: center;
1187 Removed: justify-content: space-between;
1188 Removed: width: 100%;
1189 Removed: min-height: var(--toolbar-item-height);
1190 Removed: border: 1px solid var(--border);
1191 Removed: border-radius: 0.5rem;
1192 Removed: padding: 0.5rem 0.75rem;
1193 Removed: background: white;
1194 Removed: color: var(--text);
1195 Removed: cursor: pointer;
1196 Removed: font: inherit;
1197 Removed: font-weight: 700;
1198 Removed: }
1199 Removed:
1200 Removed: .graph-controls-toggle::after {
1201 Removed: width: 0.55rem;
1202 Removed: height: 0.55rem;
1203 Removed: margin: 0 0.2rem 0.25rem 0.75rem;
1204 Removed: border-right: 2px solid currentColor;
1205 Removed: border-bottom: 2px solid currentColor;
1206 Removed: content: "";
1207 Removed: transform: rotate(45deg);
1208 Removed: transition: transform 0.15s ease;
1209 Removed: }
1210 Removed:
1211 Removed: .graph-controls-toggle[aria-expanded="true"]::after {
1212 Removed: margin-top: 0.25rem;
1213 Removed: margin-bottom: 0;
1214 Removed: transform: rotate(225deg);
1215 Removed: }
1216 Removed:
1217 Removed: .graph-controls-toggle:hover {
1218 Removed: border-color: var(--accent);
1219 Removed: background: var(--accent-soft);
1220 Removed: }
1221 Removed:
1222 Removed: .graph-controls-toggle:focus-visible {
1223 Removed: outline: 2px solid var(--accent);
1224 Removed: outline-offset: 2px;
1225 Removed: }
1226 Removed:
1227 Removed: .graph-controls-panel {
1228 Removed: display: none;
1229 Removed: margin-top: 0.5rem;
1230 Removed: border: 1px solid var(--border);
1231 Removed: border-radius: 0.5rem;
1232 Removed: padding: 0.75rem;
1233 Removed: background: var(--card);
1234 Removed: }
1235 Removed:
1236 Removed: .graph-controls-toggle[aria-expanded="true"] + .graph-controls-panel {
1237 Removed: display: block;
1238 Removed: }
1239 Removed:
1240 Removed: .toolbar-items .graph-control {
1241 Removed: width: 100%;
1242 Removed: justify-content: flex-start;
1243 Removed: }
1244 Removed:
1245 Removed: .page-header {
1246 Removed: align-items: center;
1247 Removed: width: 100%;
1248 Removed: margin: 0 0 0.75rem;
1249 Removed: border-right: 0;
1250 Removed: border-left: 0;
1251 Removed: border-radius: 0 0 0.5rem 0.5rem;
1252 Removed: }
1253 Removed:
1254 Removed: .header-status-link {
1255 Removed: order: 3;
1256 Removed: margin-left: 0;
1257 Removed: }
1258 Removed:
1259 Removed: .nav-toggle-button {
1260 Removed: order: 2;
1261 Removed: display: inline-grid;
1262 Removed: place-content: center;
1263 Removed: gap: 0.25rem;
1264 Removed: width: 2.75rem;
1265 Removed: height: 2.75rem;
1266 Removed: margin-left: auto;
1267 Removed: border: 1px solid var(--border);
1268 Removed: border-radius: 0.5rem;
1269 Removed: background: white;
1270 Removed: cursor: pointer;
1271 Removed: }
1272 Removed:
1273 Removed: .nav-toggle {
1274 Removed: position: absolute;
1275 Removed: display: block;
1276 Removed: top: 0.7rem;
1277 Removed: right: 3.95rem;
1278 Removed: width: 2.75rem;
1279 Removed: height: 2.75rem;
1280 Removed: opacity: 0;
1281 Removed: pointer-events: auto;
1282 Removed: cursor: pointer;
1283 Removed: }
1284 Removed:
1285 Removed: .nav-toggle:focus-visible + .nav-toggle-button {
1286 Removed: outline: 2px solid var(--accent);
1287 Removed: outline-offset: 2px;
1288 Removed: }
1289 Removed:
1290 Removed: .nav-toggle-button span {
1291 Removed: display: block;
1292 Removed: width: 1.15rem;
1293 Removed: height: 0.12rem;
1294 Removed: border-radius: 999px;
1295 Removed: background: var(--text);
1296 Removed: }
1297 Removed:
1298 Removed: .nav-links {
1299 Removed: position: absolute;
1300 Removed: top: calc(100% + 0.35rem);
1301 Removed: right: 0;
1302 Removed: left: 0;
1303 Removed: display: none;
1304 Removed: border: 1px solid var(--border);
1305 Removed: border-radius: 0.5rem;
1306 Removed: padding: 0.5rem;
1307 Removed: background: white;
1308 Removed: box-shadow: 0 10px 30px rgba(23, 33, 27, 0.12);
1309 Removed: }
1310 Removed:
1311 Removed: .nav-links a {
1312 Removed: display: block;
1313 Removed: width: 100%;
1314 Removed: border-radius: 0.4rem;
1315 Removed: padding: 0.75rem;
1316 Removed: }
1317 Removed:
1318 Removed: .nav-toggle:checked ~ .nav-links {
1319 Removed: display: block;
1320 Removed: }
1321 Removed:
1322 Removed: .status-overview {
1323 Removed: justify-items: stretch;
1324 Removed: }
1325 Removed:
1326 Removed: .status-summary {
1327 Removed: min-width: 0;
1328 Removed: }
1329 Removed:
1330 Removed: .status-pill {
1331 Removed: flex-wrap: wrap;
1332 Removed: width: 100%;
1333 Removed: min-width: 0;
1334 Removed: min-height: 2.75rem;
1335 Removed: padding-inline: 0.65rem;
1336 Removed: text-align: center;
1337 Removed: white-space: normal;
1338 Removed: }
1339 Removed:
1340 Removed: .status-node-summary .status-pill {
1341 Removed: width: auto;
1342 Removed: min-height: 0;
1343 Removed: }
1344 Removed:
1345 Removed: .status-node-details {
1346 Removed: grid-template-columns: 1fr;
1347 Removed: gap: 0.75rem;
1348 Removed: }
1349 Removed:
1350 Removed: .download-range-control {
1351 Removed: display: grid;
1352 Removed: grid-template-columns: repeat(2, minmax(0, 1fr));
1353 Removed: width: 100%;
1354 Removed: }
1355 Removed:
1356 Removed: .download-range-control span {
1357 Removed: width: 100%;
1358 Removed: border-top: 1px solid var(--border);
1359 Removed: }
1360 Removed:
1361 Removed: .download-range-control label:first-child span,
1362 Removed: .download-range-control label:nth-child(2) span {
1363 Removed: border-top: 0;
1364 Removed: }
1365 Removed:
1366 Removed: .download-range-control label:nth-child(odd) span {
1367 Removed: border-left: 0;
1368 Removed: }
1369 Removed:
1370 Removed: .download-range-control label:last-child {
1371 Removed: grid-column: span 2;
1372 Removed: }
1373 Removed:
1374 Removed: .reading-quick-grid {
1375 Removed: grid-template-columns: 1fr;
1376 Removed: }
1377 Removed:
1378 Removed: .probes-grid {
1379 Removed: grid-template-columns: 1fr;
1380 Removed: }
1381 Removed:
1382 Removed: .reading-quick-header {
1383 Removed: min-height: 0;
1384 Removed: }
1385 Removed:
1386 Removed: .tabs {
1387 Removed: display: grid;
1388 Removed: grid-template-columns: 1fr 1fr;
1389 Removed: gap: 0.45rem;
1390 Removed: padding: 0.75rem;
1391 Removed: }
1392 Removed:
1393 Removed: .tab-button {
1394 Removed: width: 100%;
1395 Removed: border-radius: 0.5rem;
1396 Removed: padding-inline: 0.65rem;
1397 Removed: }
1398 Removed:
1399 Removed: .card-heading {
1400 Removed: display: block;
1401 Removed: }
1402 Removed:
1403 Removed: .probes-grid .card-heading {
1404 Removed: display: flex;
1405 Removed: }
1406 Removed:
1407 Removed: .card-heading,
1408 Removed: .card-body {
1409 Removed: padding: 1rem;
1410 Removed: }
1411 Removed:
1412 Removed: .panel-tools {
1413 Removed: justify-content: flex-start;
1414 Removed: margin-top: 0.75rem;
1415 Removed: }
1416 Removed:
1417 Removed: .timeframe-control {
1418 Removed: display: grid;
1419 Removed: grid-template-columns: 1fr 1fr;
1420 Removed: width: 100%;
1421 Removed: border-radius: 0.5rem;
1422 Removed: }
1423 Removed:
1424 Removed: .graph-control .timeframe-option {
1425 Removed: min-width: 0;
1426 Removed: }
1427 Removed:
1428 Removed: .graph-control .timeframe-option.is-expanded {
1429 Removed: grid-column: span 2;
1430 Removed: min-width: 0;
1431 Removed: }
1432 Removed:
1433 Removed: .timeframe-button {
1434 Removed: border-top: 1px solid var(--border);
1435 Removed: }
1436 Removed:
1437 Removed: .timeframe-option:nth-child(1) .timeframe-button,
1438 Removed: .timeframe-option:nth-child(2) .timeframe-button {
1439 Removed: border-top: 0;
1440 Removed: }
1441 Removed:
1442 Removed: .timeframe-option:nth-child(odd) .timeframe-button {
1443 Removed: border-left: 0;
1444 Removed: }
1445 Removed:
1446 Removed: .section-heading {
1447 Removed: display: block;
1448 Removed: }
1449 Removed:
1450 Removed: .chart-wrap {
1451 Removed: min-height: 320px;
1452 Removed: }
1453 Removed:
1454 Removed: .reading-status {
1455 Removed: overflow-wrap: anywhere;
1456 Removed: }
1457 Removed:
1458 Removed: .message-log h3 {
1459 Removed: font-size: 0.95rem;
1460 Removed: }
1461 Removed:
1462 Removed: .message-table {
1463 Removed: min-width: 560px;
1464 Removed: }
1465 Removed:
1466 Removed: .message-table th,
1467 Removed: .message-table td {
1468 Removed: padding: 0.65rem 0.7rem;
1469 Removed: }
1470 Removed: }
1471 Removed:
1472 Removed: @media (max-width: 380px) {
1473 Removed: .timeframe-button {
1474 Removed: min-width: 0;
1475 Removed: padding-inline: 0.5rem;
3 Added: /* ================================================================
4 Added: Project-specific CSS supplement for Bootstrap 5.3
5 Added: Only styles that Bootstrap does not provide are kept here.
6 Added: ================================================================ */
7 Added:
8 Added: /* ----------------------------------------------------------------
9 Added: Custom colour tokens (used by status classes and reading range)
10 Added: ---------------------------------------------------------------- */
11 Added: :root {
12 Added: --fapg-accent: #276749;
13 Added: --fapg-accent-soft: #e4f3ea;
14 Added: --fapg-danger: #b42318;
15 Added: --fapg-danger-soft: #fee4e2;
16 Added: --fapg-warn: #b54708;
17 Added: --fapg-warn-soft: #fff4d6;
18 Added: --fapg-unknown: #475467;
19 Added: --fapg-unknown-soft: #eef2f6;
20 Added: }
21 Added:
22 Added: /* ----------------------------------------------------------------
23 Added: Header LED status indicator
24 Added: ---------------------------------------------------------------- */
25 Added: .header-status-link {
26 Added: position: relative;
27 Added: display: inline-grid;
28 Added: flex: 0 0 auto;
29 Added: place-items: center;
30 Added: width: 1.35rem;
31 Added: height: 1.35rem;
32 Added: border-radius: 50%;
33 Added: color: inherit;
34 Added: text-decoration: none;
35 Added: }
36 Added:
37 Added: .header-status-link:focus-visible {
38 Added: outline: 2px solid var(--fapg-accent);
39 Added: outline-offset: 0.25rem;
40 Added: }
41 Added:
42 Added: .header-status-led {
43 Added: width: 0.85rem;
44 Added: height: 0.85rem;
45 Added: border: 1px solid rgba(23, 33, 27, 0.22);
46 Added: border-radius: 50%;
47 Added: background: radial-gradient(circle at 32% 28%, #ffffff 0 8%, #98a2b3 18%, #475467 70%);
48 Added: box-shadow:
49 Added: inset 0 -1px 2px rgba(23, 33, 27, 0.45),
50 Added: 0 0 0.35rem rgba(71, 84, 103, 0.45);
51 Added: }
52 Added:
53 Added: .header-status-link.is-healthy .header-status-led {
54 Added: background: radial-gradient(circle at 32% 28%, #ffffff 0 8%, #86efac 18%, #16a34a 68%, #14532d 100%);
55 Added: animation: led-green 1.35s ease-in-out infinite;
56 Added: }
57 Added:
58 Added: .header-status-link.is-warning .header-status-led {
59 Added: background: radial-gradient(circle at 32% 28%, #ffffff 0 8%, #fca5a5 18%, #dc2626 68%, #7f1d1d 100%);
60 Added: animation: led-red 1s ease-in-out infinite;
61 Added: }
62 Added:
63 Added: @keyframes led-green {
64 Added: 0%, 100% {
65 Added: box-shadow:
66 Added: inset 0 -1px 2px rgba(23, 33, 27, 0.45),
67 Added: 0 0 0.3rem rgba(22, 163, 74, 0.45);
68 Added: opacity: 0.78;
69 Added: }
70 Added: 50% {
71 Added: box-shadow:
72 Added: inset 0 -1px 2px rgba(23, 33, 27, 0.45),
73 Added: 0 0 0.85rem rgba(22, 163, 74, 0.95),
74 Added: 0 0 1.25rem rgba(134, 239, 172, 0.45);
75 Added: opacity: 1;
76 Added: }
77 Added: }
78 Added:
79 Added: @keyframes led-red {
80 Added: 0%, 100% {
81 Added: box-shadow:
82 Added: inset 0 -1px 2px rgba(23, 33, 27, 0.45),
83 Added: 0 0 0.35rem rgba(220, 38, 38, 0.5);
84 Added: opacity: 0.75;
85 Added: }
86 Added: 50% {
87 Added: box-shadow:
88 Added: inset 0 -1px 2px rgba(23, 33, 27, 0.45),
89 Added: 0 0 0.9rem rgba(220, 38, 38, 1),
90 Added: 0 0 1.35rem rgba(252, 165, 165, 0.5);
91 Added: opacity: 1;
92 Added: }
93 Added: }
94 Added:
95 Added: /* ----------------------------------------------------------------
96 Added: Status pill — coloured badge for online/offline/unknown states.
97 Added: Bootstrap badge handles shape/padding; we add state colours.
98 Added: ---------------------------------------------------------------- */
99 Added: .status-pill {
100 Added: white-space: nowrap;
101 Added: font-size: 0.78rem;
102 Added: }
103 Added:
104 Added: .status-pill.is-healthy,
105 Added: .status-pill.is-online {
106 Added: background-color: var(--fapg-accent-soft) !important;
107 Added: color: var(--fapg-accent) !important;
108 Added: }
109 Added:
110 Added: .status-pill.is-stale,
111 Added: .status-pill.is-probe-error {
112 Added: background-color: var(--fapg-warn-soft) !important;
113 Added: color: var(--fapg-warn) !important;
114 Added: }
115 Added:
116 Added: .status-pill.is-offline,
117 Added: .status-pill.is-unreachable {
118 Added: background-color: var(--fapg-danger-soft) !important;
119 Added: color: var(--fapg-danger) !important;
120 Added: }
121 Added:
122 Added: .status-pill.is-unknown {
123 Added: background-color: var(--fapg-unknown-soft) !important;
124 Added: color: var(--fapg-unknown) !important;
125 Added: }
126 Added:
127 Added: /* Status summary pill in the toolbar */
128 Added: .status-summary {
129 Added: display: inline-flex;
130 Added: align-items: center;
131 Added: justify-content: center;
132 Added: min-width: 11rem;
133 Added: border-radius: 999px;
134 Added: padding: 0.4rem 0.85rem;
135 Added: font-size: 0.82rem;
136 Added: font-weight: 700;
137 Added: text-decoration: none;
138 Added: }
139 Added:
140 Added: .status-summary[href]:hover {
141 Added: filter: brightness(0.97);
142 Added: }
143 Added:
144 Added: /* ----------------------------------------------------------------
145 Added: Status node marker — LED dot on the status page
146 Added: ---------------------------------------------------------------- */
147 Added: .status-node-marker {
148 Added: width: 1.25rem;
149 Added: height: 1.25rem;
150 Added: border: 0.3rem solid var(--fapg-unknown-soft);
151 Added: border-radius: 50%;
152 Added: background: var(--fapg-unknown);
153 Added: flex-shrink: 0;
154 Added: }
155 Added:
156 Added: .status-node-marker.is-online,
157 Added: .status-node-marker.is-healthy {
158 Added: border-color: var(--fapg-accent-soft);
159 Added: background: var(--fapg-accent);
160 Added: }
161 Added:
162 Added: .status-node-marker.is-stale,
163 Added: .status-node-marker.is-probe-error {
164 Added: border-color: var(--fapg-warn-soft);
165 Added: background: var(--fapg-warn);
166 Added: }
167 Added:
168 Added: .status-node-marker.is-offline,
169 Added: .status-node-marker.is-unreachable {
170 Added: border-color: var(--fapg-danger-soft);
171 Added: background: var(--fapg-danger);
172 Added: }
173 Added:
174 Added: /* ----------------------------------------------------------------
175 Added: Reading range bar (home page quick-reading cards)
176 Added: ---------------------------------------------------------------- */
177 Added: .reading-quick-item {
178 Added: --warn-low: 27%;
179 Added: --good-start: 35%;
180 Added: --good-end: 65%;
181 Added: --warn-high: 73%;
182 Added: }
183 Added:
184 Added: .reading-range {
185 Added: position: relative;
186 Added: height: 0.85rem;
187 Added: border-radius: 999px;
188 Added: background:
189 Added: linear-gradient(
190 Added: 90deg,
191 Added: var(--fapg-danger) 0%,
192 Added: var(--fapg-warn) var(--warn-low),
193 Added: var(--fapg-accent) var(--good-start),
194 Added: var(--fapg-accent) var(--good-end),
195 Added: var(--fapg-warn) var(--warn-high),
196 Added: var(--fapg-danger) 100%
197 Added: );
198 Added: }
199 Added:
200 Added: .reading-range-marker {
201 Added: position: absolute;
202 Added: top: 50%;
203 Added: left: 50%;
204 Added: width: 1rem;
205 Added: height: 1rem;
206 Added: border: 2px solid white;
207 Added: border-radius: 50%;
208 Added: background: var(--fapg-unknown);
209 Added: box-shadow: 0 1px 4px rgba(23, 33, 27, 0.35);
210 Added: transform: translate(-50%, -50%);
211 Added: }
212 Added:
213 Added: .reading-quick-item.is-good .reading-range-marker { background: var(--fapg-accent); }
214 Added: .reading-quick-item.is-low .reading-range-marker,
215 Added: .reading-quick-item.is-high .reading-range-marker { background: var(--fapg-danger); }
216 Added:
217 Added: /* ----------------------------------------------------------------
218 Added: Chart canvas sizing
219 Added: ---------------------------------------------------------------- */
220 Added: .chart-wrap {
221 Added: position: relative;
222 Added: width: 100%;
223 Added: max-height: 420px;
224 Added: aspect-ratio: 1 / 1;
225 Added: }
226 Added:
227 Added: /* Probes grid — square aspect ratio cards with flex charts */
228 Added: .probes-grid .card {
229 Added: aspect-ratio: 1 / 1;
230 Added: }
231 Added:
232 Added: .probes-grid .card-body {
233 Added: display: flex;
234 Added: flex: 1;
235 Added: flex-direction: column;
236 Added: min-height: 0;
237 Added: }
238 Added:
239 Added: .probes-grid .tab-panel.is-active {
240 Added: display: flex;
241 Added: flex: 1;
242 Added: flex-direction: column;
243 Added: min-height: 0;
244 Added: }
245 Added:
246 Added: .probes-grid .chart-wrap {
247 Added: flex: 1;
248 Added: min-height: 0;
249 Added: max-height: none;
250 Added: aspect-ratio: auto;
251 Added: }
252 Added:
253 Added: /* ----------------------------------------------------------------
254 Added: Tab panel visibility (JS-driven)
255 Added: ---------------------------------------------------------------- */
256 Added: .tab-panel {
257 Added: display: none;
258 Added: }
259 Added:
260 Added: .tab-panel.is-active {
261 Added: display: block;
262 Added: }
263 Added:
264 Added: /* ----------------------------------------------------------------
265 Added: Timeframe control adjustments: hide +/− by default,
266 Added: show when option is expanded (JS adds is-expanded)
267 Added: ---------------------------------------------------------------- */
268 Added: .timeframe-adjustment {
269 Added: display: none;
270 Added: }
271 Added:
272 Added: .timeframe-option.is-expanded .timeframe-adjustment {
273 Added: display: inline-flex;
274 Added: align-items: center;
275 Added: justify-content: center;
276 Added: }
277 Added:
278 Added: .timeframe-button.is-active {
279 Added: /* JS toggles this; override Bootstrap outline style */
280 Added: background-color: var(--fapg-accent) !important;
281 Added: border-color: var(--fapg-accent) !important;
282 Added: color: #fff !important;
283 Added: }
284 Added:
285 Added: /* ----------------------------------------------------------------
286 Added: Graph controls — mobile collapse
287 Added: ---------------------------------------------------------------- */
288 Added: @media (max-width: 767px) {
289 Added: .graph-controls-panel {
290 Added: display: none;
291 Added: }
292 Added:
293 Added: .graph-controls-toggle[aria-expanded="true"] + .graph-controls-panel {
294 Added: display: block;
295 Added: margin-top: 0.5rem;
1476 296 }
1477 297 }
roles/dashboard/t/00-homepage.t
index 7bc7e048..fdef4808 100644..100644
@@ -13,21 +13,23 @@
13 13
14 14 $t->get_ok('/')
15 15 ->status_is(200)
16 Added: ->element_exists('body nav.navbar + main')
17 Added: ->text_is( 'a.navbar-brand[href="/"]', 'FAPG' )
18 Added: ->text_is( '.navbar-nav a.nav-link[href="/probes"]', 'Probes' )
19 Added: ->text_is( '.navbar-nav a.nav-link[href="/downloads"]', 'Downloads' )
16 20 ->element_exists(
17 Removed: 'body > header.page-header + main.container > div.toolbar')
18 Removed: ->text_is( 'a.page-brand[href="/"]', 'FAPG' )
19 Removed: ->text_is( 'div.nav-links a[href="/probes"]', 'Probes' )
20 Removed: ->text_is( 'div.nav-links a[href="/downloads"]', 'Downloads' )
21 Removed: ->element_exists(
22 21 'a.header-status-link[href="/status"][data-header-status]')
23 Removed: ->text_is( 'nav.breadcrumbs [aria-current="page"]', 'Home' )
24 22 ->element_exists(
23 Added: 'nav[aria-label="Breadcrumb"] [aria-current="page"]')
24 Added: ->text_is(
25 Added: 'nav[aria-label="Breadcrumb"] [aria-current="page"]', 'Home' )
26 Added: ->element_exists(
25 27 'div.toolbar-items a.status-summary[href="/status"][data-overview-status]'
26 28 )
27 29 ->element_exists(
28 30 'section.reading-quick-status [data-reading-quick-status]')
29 31 ->element_exists(
30 Removed: 'footer.site-footer a[href="https://www.lafermeaquaponique.com/"]');
32 Added: 'footer a[href="https://www.lafermeaquaponique.com/"]');
31 33
32 34 for my $probe (qw(ph do orp ec)) {
33 35 $t->element_exists(
roles/dashboard/t/01-graph-page.t
index 6f9d1865..1d2b3fe7 100644..100644
@@ -13,10 +13,13 @@
13 13
14 14 $t->get_ok('/probes/ph')
15 15 ->status_is(200)
16 Removed: ->text_is( 'nav.breadcrumbs a[href="/"]', 'Home' )
17 Removed: ->text_is( 'nav.breadcrumbs a[href="/probes"]', 'Probes' )
18 Removed: ->text_is( 'nav.breadcrumbs [aria-current="page"]', 'pH' )
19 Removed: ->text_is( 'div.card h1 a[href="/probes/ph"]', 'pH' )
16 Added: ->element_exists(
17 Added: 'nav[aria-label="Breadcrumb"] a[href="/"]')
18 Added: ->text_is( 'nav[aria-label="Breadcrumb"] a[href="/"]', 'Home' )
19 Added: ->text_is( 'nav[aria-label="Breadcrumb"] a[href="/probes"]', 'Probes' )
20 Added: ->text_is(
21 Added: 'nav[aria-label="Breadcrumb"] [aria-current="page"]', 'pH' )
22 Added: ->text_is( '.card-header h1 a[href="/probes/ph"]', 'pH' )
20 23 ->element_exists('section#probe-ph[data-panel="ph"] canvas#chart-ph')
21 24 ->element_exists('[data-status="ph"]')
22 25 ->element_exists(
@@ -41,14 +44,16 @@
41 44
42 45 $t->get_ok('/probes')
43 46 ->status_is(200)
44 Removed: ->text_is( 'nav.breadcrumbs a[href="/"]', 'Home' )
45 Removed: ->text_is( 'nav.breadcrumbs [aria-current="page"]', 'Probes' )
47 Added: ->element_exists(
48 Added: 'nav[aria-label="Breadcrumb"] a[href="/"]')
49 Added: ->text_is(
50 Added: 'nav[aria-label="Breadcrumb"] [aria-current="page"]', 'Probes' )
46 51 ->element_exists('main.container-wide')
47 52 ->element_exists('div.probes-grid')
48 53 ->element_exists('div.toolbar-items div.graph-control');
49 54
50 55 for my $probe (qw(ph do orp ec)) {
51 Removed: $t->element_exists(qq{div.probes-grid h1 a[href="/probes/$probe"]});
56 Added: $t->element_exists(qq{div.probes-grid .card-header h1 a[href="/probes/$probe"]});
52 57 $t->element_exists(
53 58 qq{section#probe-${probe}[data-panel="$probe"] canvas#chart-$probe});
54 59 $t->element_exists(qq{a[data-raw-download][data-probe="$probe"]});
roles/dashboard/t/05-status-page.t
index a7a1da12..c31ec5ab 100644..100644
@@ -13,9 +13,11 @@
13 13
14 14 $t->get_ok('/status')
15 15 ->status_is(200)
16 Removed: ->text_is( 'nav.breadcrumbs a[href="/"]', 'Home' )
17 Removed: ->text_is( 'nav.breadcrumbs [aria-current="page"]', 'Status' )
18 Removed: ->text_is( 'h1', 'DAQ Status' )
16 Added: ->element_exists(
17 Added: 'nav[aria-label="Breadcrumb"] a[href="/"]')
18 Added: ->text_is(
19 Added: 'nav[aria-label="Breadcrumb"] [aria-current="page"]', 'Status' )
20 Added: ->text_is( 'h1', 'DAQ Status' )
19 21 ->element_exists('main.container')
20 22 ->element_exists('[data-status-page-groups]');
21 23
roles/dashboard/t/06-downloads.t
index 59c0adcc..0c00aecb 100644..100644
@@ -13,21 +13,21 @@
13 13
14 14 $t->get_ok('/downloads')
15 15 ->status_is(200)
16 Removed: ->text_is( 'nav.breadcrumbs a[href="/"]', 'Home' )
17 Removed: ->text_is( 'nav.breadcrumbs [aria-current="page"]', 'Downloads' )
18 Removed: ->text_is( 'h1', 'Downloads' )
16 Added: ->element_exists(
17 Added: 'nav[aria-label="Breadcrumb"] a[href="/"]')
18 Added: ->text_is(
19 Added: 'nav[aria-label="Breadcrumb"] [aria-current="page"]', 'Downloads' )
20 Added: ->text_is( 'h1', 'Downloads' )
19 21 ->element_exists('form.downloads-form[action="/downloads/readings.csv"]')
20 22 ->text_is(
21 23 'select#download-probe[name="probe"] option[value=""][selected]',
22 24 'All probes' )
23 Removed: ->text_is( 'fieldset.download-range-fieldset legend', 'Date range' )
24 25 ->element_exists('input[name="date_range"][value="week"][checked]')
25 26 ->element_exists(
26 Removed: 'fieldset.download-range-fieldset [data-custom-date-fields][hidden] input[name="from"][type="date"]'
27 Removed: )
27 Added: '[data-custom-date-fields][hidden] input[name="from"][type="date"]')
28 28 ->element_exists(
29 Removed: 'fieldset.download-range-fieldset [data-custom-date-fields][hidden] input[name="to"][type="date"]'
30 Removed: )->text_is( 'button.downloads-button[type="submit"]', 'Download CSV' );
29 Added: '[data-custom-date-fields][hidden] input[name="to"][type="date"]')
30 Added: ->text_is( 'button.downloads-button[type="submit"]', 'Download CSV' );
31 31
32 32 for my $probe (qw(ph do orp ec)) {
33 33 $t->element_exists(qq{select#download-probe option[value="$probe"]});
roles/dashboard/templates/dashboard/downloads.html.ep
index 70068bdd..8c905602 100644..100644
@@ -3,46 +3,60 @@
3 3 % layout 'default';
4 4 % title 'FAPG DAQ Downloads';
5 5
6 Removed: <div class="card downloads-card">
7 Removed: <div class="card-heading">
8 Removed: <div>
9 Removed: <h1>Downloads</h1>
10 Removed: <p>Download recorded DAQ readings as a CSV file.</p>
11 Removed: </div>
6 Added: <div class="card">
7 Added: <div class="card-header">
8 Added: <h1 class="h5 mb-0">Downloads</h1>
9 Added: <p class="text-muted small mb-0 mt-1">Download recorded DAQ readings as a CSV file.</p>
12 10 </div>
13 11
14 12 <div class="card-body">
15 Removed: <form class="downloads-form" action="/downloads/readings.csv" method="get">
16 Removed: <div class="download-probe-field">
17 Removed: <label for="download-probe">Probe</label>
18 Removed: <select id="download-probe" name="probe">
13 Added: <form action="/downloads/readings.csv" method="get" class="downloads-form d-flex flex-column gap-3">
14 Added:
15 Added: <div>
16 Added: <label class="form-label fw-bold" for="download-probe">Probe</label>
17 Added: <select class="form-select w-auto" id="download-probe" name="probe">
19 18 <option value="" selected>All probes</option>
20 19 % for my $probe ($probes->@*) {
21 20 <option value="<%= $probe->{key} %>"><%= $probe->{label} %></option>
22 21 % }
23 22 </select>
24 23 </div>
25 Removed: <fieldset class="download-range-fieldset">
26 Removed: <legend>Date range</legend>
27 Removed: <div class="download-range-control">
28 Removed: <label><input type="radio" name="date_range" value="today" data-download-range data-from="<%= $to %>" data-to="<%= $to %>"><span>Today</span></label>
29 Removed: <label><input type="radio" name="date_range" value="week" data-download-range data-from="<%= $from %>" data-to="<%= $to %>" checked><span>Past week</span></label>
30 Removed: <label><input type="radio" name="date_range" value="month" data-download-range data-from="<%= $month_from %>" data-to="<%= $to %>"><span>Past month</span></label>
31 Removed: <label><input type="radio" name="date_range" value="all" data-download-range data-from="<%= $oldest %>" data-to="<%= $to %>"><span>All time</span></label>
32 Removed: <label><input type="radio" name="date_range" value="custom" data-download-range><span>Custom</span></label>
24 Added:
25 Added: <fieldset>
26 Added: <legend class="fw-bold col-form-label pt-0 mb-2">Date range</legend>
27 Added: <div class="btn-group flex-wrap download-range-control" role="group" aria-label="Date range">
28 Added: <input class="btn-check" type="radio" name="date_range" id="range-today" value="today" data-download-range data-from="<%= $to %>" data-to="<%= $to %>">
29 Added: <label class="btn btn-outline-secondary" for="range-today">Today</label>
30 Added:
31 Added: <input class="btn-check" type="radio" name="date_range" id="range-week" value="week" data-download-range data-from="<%= $from %>" data-to="<%= $to %>" checked>
32 Added: <label class="btn btn-outline-secondary" for="range-week">Past week</label>
33 Added:
34 Added: <input class="btn-check" type="radio" name="date_range" id="range-month" value="month" data-download-range data-from="<%= $month_from %>" data-to="<%= $to %>">
35 Added: <label class="btn btn-outline-secondary" for="range-month">Past month</label>
36 Added:
37 Added: <input class="btn-check" type="radio" name="date_range" id="range-all" value="all" data-download-range data-from="<%= $oldest %>" data-to="<%= $to %>">
38 Added: <label class="btn btn-outline-secondary" for="range-all">All time</label>
39 Added:
40 Added: <input class="btn-check" type="radio" name="date_range" id="range-custom" value="custom" data-download-range>
41 Added: <label class="btn btn-outline-secondary" for="range-custom">Custom</label>
33 42 </div>
34 Removed: <div class="download-date-fields" data-custom-date-fields hidden>
35 Removed: <div class="download-date-field">
36 Removed: <label for="download-from">From</label>
37 Removed: <input id="download-from" name="from" type="date" value="<%= $from %>" required>
43 Added:
44 Added: <div class="d-flex flex-wrap gap-3 mt-3" data-custom-date-fields hidden>
45 Added: <div>
46 Added: <label class="form-label fw-bold" for="download-from">From</label>
47 Added: <input class="form-control" id="download-from" name="from" type="date" value="<%= $from %>" required>
38 48 </div>
39 Removed: <div class="download-date-field">
40 Removed: <label for="download-to">To</label>
41 Removed: <input id="download-to" name="to" type="date" value="<%= $to %>" required>
49 Added: <div>
50 Added: <label class="form-label fw-bold" for="download-to">To</label>
51 Added: <input class="form-control" id="download-to" name="to" type="date" value="<%= $to %>" required>
42 52 </div>
43 53 </div>
44 54 </fieldset>
45 Removed: <button class="downloads-button" type="submit">Download CSV</button>
55 Added:
56 Added: <div>
57 Added: <button class="btn btn-success downloads-button" type="submit">Download CSV</button>
58 Added: </div>
59 Added:
46 60 </form>
47 61 </div>
48 62 </div>
roles/dashboard/templates/dashboard/graph-control.html.ep
index da8a5e56..e9fec41c 100644..100644
@@ -2,29 +2,29 @@
2 2
3 3 <div class="graph-controls-disclosure">
4 4 <button
5 Removed: class="graph-controls-toggle"
5 Added: class="btn btn-outline-secondary d-md-none w-100 mb-2 graph-controls-toggle"
6 6 type="button"
7 7 aria-expanded="false"
8 8 aria-controls="graph-controls-panel"
9 9 data-graph-controls-toggle>Graph controls</button>
10 10 <div class="graph-controls-panel" id="graph-controls-panel">
11 11 <div class="graph-control">
12 Removed: <div class="timeframe-control" role="group" aria-label="<%= $aria_label %>">
12 Added: <div class="btn-group timeframe-control" role="group" aria-label="<%= $aria_label %>">
13 13 % for my $timeframe ( [ hour => 'Hourly' ], [ day => 'Daily' ], [ week => 'Weekly' ], [ month => 'Monthly' ], [ year => 'Yearly' ] ) {
14 Removed: <div class="timeframe-option" data-timeframe-option="<%= $timeframe->[0] %>">
14 Added: <div class="timeframe-option d-inline-flex" data-timeframe-option="<%= $timeframe->[0] %>">
15 15 <button
16 Removed: class="timeframe-adjustment"
16 Added: class="btn btn-sm btn-outline-secondary timeframe-adjustment"
17 17 type="button"
18 18 data-timeframe-adjustment="decrease"
19 19 data-timeframe="<%= $timeframe->[0] %>"
20 20 aria-label="Decrease <%= $timeframe->[0] %> range">−</button>
21 21 <button
22 Removed: class="timeframe-button<%= $timeframe->[0] eq 'day' ? ' is-active' : '' %>"
22 Added: class="btn btn-sm<%= $timeframe->[0] eq 'day' ? ' btn-success' : ' btn-outline-secondary' %> timeframe-button"
23 23 type="button"
24 24 data-timeframe="<%= $timeframe->[0] %>"
25 25 data-timeframe-label="<%= $timeframe->[1] %>"><%= $timeframe->[1] %></button>
26 26 <button
27 Removed: class="timeframe-adjustment"
27 Added: class="btn btn-sm btn-outline-secondary timeframe-adjustment"
28 28 type="button"
29 29 data-timeframe-adjustment="increase"
30 30 data-timeframe="<%= $timeframe->[0] %>"
roles/dashboard/templates/dashboard/graph.html.ep
index 4abbe751..8e977629 100644..100644
@@ -5,14 +5,14 @@
5 5
6 6 %= include 'dashboard/probe-card', probe => $probe
7 7
8 Removed: <section class="card message-log" aria-label="Last ten reading messages for <%= $probe->{label} %>">
9 Removed: <div class="card-heading">
10 Removed: <h2>Latest Readings</h2>
8 Added: <div class="card mt-3" aria-label="Last ten reading messages for <%= $probe->{label} %>">
9 Added: <div class="card-header">
10 Added: <h2 class="h6 mb-0">Latest Readings</h2>
11 11 </div>
12 Removed: <div class="card-body">
13 Removed: <div class="message-table-wrap">
14 Removed: <table class="message-table">
15 Removed: <thead>
12 Added: <div class="card-body p-0">
13 Added: <div class="table-responsive">
14 Added: <table class="table table-sm table-hover mb-0 message-table">
15 Added: <thead class="table-light">
16 16 <tr>
17 17 <th scope="col">Received</th>
18 18 <th scope="col">Age</th>
@@ -29,7 +29,7 @@
29 29 </table>
30 30 </div>
31 31 </div>
32 Removed: </section>
32 Added: </div>
33 33
34 34 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
35 35 <script type="module" src="/js/dashboard.js"></script>
roles/dashboard/templates/dashboard/index.html.ep
index 30e7f2fa..abb9da19 100644..100644
@@ -3,42 +3,44 @@
3 3 % layout 'default';
4 4 % title 'FAPG DAQ Dashboard';
5 5
6 Removed: <section class="status-band" aria-label="DAQ status overview">
7 Removed: <div class="status-overview">
8 Removed: % for my $probe ($probes->@*) {
9 Removed: <span
10 Removed: hidden
11 Removed: data-sensor-status="<%= $probe->{key} %>"
12 Removed: data-sensor-label="<%= $probe->{label} %>"></span>
13 Removed: % }
6 Added: <section class="mb-4" aria-label="DAQ status overview">
7 Added: % for my $probe ($probes->@*) {
8 Added: <span
9 Added: hidden
10 Added: data-sensor-status="<%= $probe->{key} %>"
11 Added: data-sensor-label="<%= $probe->{label} %>"></span>
12 Added: % }
14 13
15 Removed: <section class="reading-quick-status" aria-label="Sensor reading quick status">
16 Removed: <div class="reading-quick-grid" data-reading-quick-status>
17 Removed: % for my $probe ($probes->@*) {
18 Removed: <a class="reading-quick-link" href="/probes/<%= $probe->{key} %>">
14 Added: <section class="reading-quick-status" aria-label="Sensor reading quick status">
15 Added: <div class="row g-3" data-reading-quick-status>
16 Added: % for my $probe ($probes->@*) {
17 Added: <div class="col-12 col-sm-6 col-xl-3">
18 Added: <a class="reading-quick-link text-decoration-none text-body d-block h-100" href="/probes/<%= $probe->{key} %>">
19 19 <article
20 Removed: class="reading-quick-item is-unknown"
20 Added: class="card h-100 reading-quick-item is-unknown"
21 21 data-reading-quick-probe="<%= $probe->{key} %>"
22 22 data-reading-quick-label="<%= $probe->{label} %>"
23 23 data-reading-quick-unit="<%= $probe->{unit} %>">
24 Removed: <div class="reading-quick-header">
25 Removed: <h3><%= $probe->{label} %></h3>
26 Removed: <p data-reading-quick-value>Waiting for data</p>
24 Added: <div class="card-body d-flex flex-column gap-2">
25 Added: <div class="reading-quick-header">
26 Added: <h3 class="fs-6 fw-bold mb-0"><%= $probe->{label} %></h3>
27 Added: <p class="text-muted small mb-0" data-reading-quick-value>Waiting for data</p>
28 Added: </div>
29 Added: <div class="reading-range" role="meter" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50">
30 Added: <span class="reading-range-marker" data-reading-quick-marker></span>
31 Added: </div>
32 Added: <div class="reading-range-labels d-grid gap-1 small text-muted" style="grid-template-columns: 1fr auto 1fr">
33 Added: <span data-reading-quick-min></span>
34 Added: <span class="text-success fw-bold text-center" data-reading-quick-target></span>
35 Added: <span class="text-end" data-reading-quick-max></span>
36 Added: </div>
27 37 </div>
28 Removed: <div class="reading-range" role="meter" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50">
29 Removed: <span class="reading-range-marker" data-reading-quick-marker></span>
30 Removed: </div>
31 Removed: <div class="reading-range-labels">
32 Removed: <span data-reading-quick-min></span>
33 Removed: <span data-reading-quick-target></span>
34 Removed: <span data-reading-quick-max></span>
35 Removed: </div>
36 38 </article>
37 39 </a>
38 Removed: % }
39 Removed: </div>
40 Removed: </section>
41 Removed: </div>
40 Added: </div>
41 Added: % }
42 Added: </div>
43 Added: </section>
42 44 </section>
43 45
44 46 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
roles/dashboard/templates/dashboard/probe-card.html.ep
index e0709e8d..0699ec6c 100644..100644
@@ -1,32 +1,41 @@
1 1 <!-- -*- mode: web; -*- -->
2 2
3 3 <div class="card">
4 Removed: <div class="card-heading">
5 Removed: <div class="probe-card-title">
6 Removed: <h1><a href="/probes/<%= $probe->{key} %>"><%= $probe->{label} %></a></h1>
7 Removed: <p class="probe-card-subtitle">
8 Removed: Unit:
9 Removed: <strong><%= $probe->{unit} %></strong>
4 Added: <div class="card-header d-flex justify-content-between align-items-start gap-2">
5 Added: <div class="probe-card-title min-w-0">
6 Added: <h1 class="h5 mb-0">
7 Added: <a class="text-body text-decoration-none" href="/probes/<%= $probe->{key} %>"><%= $probe->{label} %></a>
8 Added: </h1>
9 Added: <p class="text-muted small mb-0 mt-1">
10 Added: Unit: <strong><%= $probe->{unit} %></strong>
10 11 </p>
11 12 </div>
12 Removed: <details class="probe-card-menu">
13 Removed: <summary aria-label="Actions for <%= $probe->{label} %>"><span class="probe-card-menu-icon" aria-hidden="true"></span></summary>
14 Removed: <div class="probe-card-menu-content">
15 Removed: <a
16 Removed: class="secondary-button graph-download-button"
17 Removed: href="/downloads/readings.csv?probe=<%= $probe->{key} %>&amp;timeframe=day&amp;range=1"
18 Removed: data-raw-download
19 Removed: data-probe="<%= $probe->{key} %>">Download raw data</a>
20 Removed: </div>
21 Removed: </details>
13 Added: <div class="dropdown flex-shrink-0">
14 Added: <button
15 Added: class="btn btn-sm btn-outline-secondary"
16 Added: type="button"
17 Added: data-bs-toggle="dropdown"
18 Added: aria-expanded="false"
19 Added: aria-label="Actions for <%= $probe->{label} %>">
20 Added: <span class="probe-card-menu-icon" aria-hidden="true"></span>
21 Added: </button>
22 Added: <ul class="dropdown-menu dropdown-menu-end">
23 Added: <li>
24 Added: <a
25 Added: class="dropdown-item graph-download-button"
26 Added: href="/downloads/readings.csv?probe=<%= $probe->{key} %>&amp;timeframe=day&amp;range=1"
27 Added: data-raw-download
28 Added: data-probe="<%= $probe->{key} %>">Download raw data</a>
29 Added: </li>
30 Added: </ul>
31 Added: </div>
22 32 </div>
23 33 <div class="card-body">
24 34 <section id="probe-<%= $probe->{key} %>" class="tab-panel is-active" data-panel="<%= $probe->{key} %>">
25 35 <div class="chart-wrap">
26 36 <canvas id="chart-<%= $probe->{key} %>"></canvas>
27 37 </div>
28 Removed:
29 Removed: <p class="reading-status" data-status="<%= $probe->{key} %>">
38 Added: <p class="text-muted small mt-2 mb-0 reading-status" data-status="<%= $probe->{key} %>">
30 39 Waiting for data…
31 40 </p>
32 41 </section>
roles/dashboard/templates/dashboard/probes.html.ep
index 2c209b21..7a1894a2 100644..100644
@@ -3,9 +3,11 @@
3 3 % layout 'default';
4 4 % title 'FAPG DAQ Probes';
5 5
6 Removed: <div class="probes-grid">
6 Added: <div class="row g-3 probes-grid">
7 7 % for my $probe ($probes->@*) {
8 Removed: %= include 'dashboard/probe-card', probe => $probe
8 Added: <div class="col-12 col-md-6 col-xl-3">
9 Added: %= include 'dashboard/probe-card', probe => $probe
10 Added: </div>
9 11 % }
10 12 </div>
11 13
roles/dashboard/templates/dashboard/status.html.ep
index 499e4a41..f9d9f376 100644..100644
@@ -3,44 +3,41 @@
3 3 % layout 'default';
4 4 % title 'FAPG DAQ Status';
5 5
6 Removed: <div class="card status-page-card">
7 Removed: <div class="card-heading">
8 Removed: <div>
9 Removed: <h1>DAQ Status</h1>
10 Removed: <p>Current connection status for the hub and sensor probes.</p>
11 Removed: </div>
6 Added: <div class="card">
7 Added: <div class="card-header">
8 Added: <h1 class="h5 mb-0">DAQ Status</h1>
9 Added: <p class="text-muted small mb-0 mt-1">Current connection status for the hub and sensor probes.</p>
12 10 </div>
13 11
14 12 <div class="card-body">
15 Removed: <div class="status-probe-groups" data-status-page-groups>
13 Added: <div class="d-grid gap-4" data-status-page-groups>
16 14 % for my $item ($items->@*) {
17 15 <section
18 Removed: class="status-probe-group"
19 16 data-status-page-probe="<%= $item->{key} %>"
20 17 aria-labelledby="status-probe-<%= $item->{key} %>">
21 Removed: <h2 id="status-probe-<%= $item->{key} %>" data-status-page-label>
18 Added: <h2 id="status-probe-<%= $item->{key} %>" class="h6 fw-semibold mb-2 text-uppercase text-muted" data-status-page-label>
22 19 % if ($item->{key} ne 'hub') {
23 Removed: <a href="/probes/<%= $item->{key} %>"><%= $item->{label} %></a>
20 Added: <a class="text-body text-decoration-none" href="/probes/<%= $item->{key} %>"><%= $item->{label} %></a>
24 21 % } else {
25 22 <%= $item->{label} %>
26 23 % }
27 24 </h2>
28 Removed: <ol class="status-node-sequence" aria-label="<%= $item->{label} %> nodes">
29 Removed: <li class="status-node" data-status-page-item="<%= $item->{key} %>">
30 Removed: <span class="status-node-marker is-unknown" aria-hidden="true"></span>
31 Removed: <div class="status-node-content">
32 Removed: <div class="status-node-summary">
33 Removed: <p class="status-node-name" data-status-page-node>Checking…</p>
34 Removed: <span class="status-pill is-unknown" data-status-page-state>Checking</span>
25 Added: <ol class="list-unstyled mb-0" aria-label="<%= $item->{label} %> nodes">
26 Added: <li class="d-flex gap-3 align-items-start" data-status-page-item="<%= $item->{key} %>">
27 Added: <span class="status-node-marker is-unknown flex-shrink-0 mt-2" aria-hidden="true"></span>
28 Added: <div class="border rounded p-3 flex-grow-1 bg-white">
29 Added: <div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
30 Added: <p class="fw-bold mb-0 status-node-name" data-status-page-node>Checking…</p>
31 Added: <span class="badge status-pill is-unknown" data-status-page-state>Checking</span>
35 32 </div>
36 Removed: <dl class="status-node-details">
37 Removed: <div>
38 Removed: <dt>Last status</dt>
39 Removed: <dd data-status-page-last-seen>Checking…</dd>
33 Added: <dl class="row row-cols-1 row-cols-sm-2 g-2 mb-0 small">
34 Added: <div class="col">
35 Added: <dt class="text-muted text-uppercase fw-semibold" style="font-size:.7rem">Last status</dt>
36 Added: <dd class="mb-0" data-status-page-last-seen>Checking…</dd>
40 37 </div>
41 Removed: <div>
42 Removed: <dt>Message</dt>
43 Removed: <dd data-status-page-message>Checking status…</dd>
38 Added: <div class="col">
39 Added: <dt class="text-muted text-uppercase fw-semibold" style="font-size:.7rem">Message</dt>
40 Added: <dd class="mb-0" data-status-page-message>Checking status…</dd>
44 41 </div>
45 42 </dl>
46 43 </div>
roles/dashboard/templates/dashboard/toolbar.html.ep
index 2ac69c41..310990ed 100644..100644
@@ -1,13 +1,14 @@
1 1 <!-- -*- mode: web; -*- -->
2 2
3 Removed: <div class="toolbar">
4 Removed: <nav class="breadcrumbs" aria-label="Breadcrumb">
5 Removed: <ol>
3 Added: <div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3 toolbar">
4 Added: <nav aria-label="Breadcrumb">
5 Added: <ol class="breadcrumb mb-0 bg-white border rounded-pill px-3 py-2 small fw-bold">
6 6 % for my $index (0 .. $breadcrumbs->$#*) {
7 7 % my $breadcrumb = $breadcrumbs->[$index];
8 Removed: <li>
8 Added: % my $is_last = $index == $breadcrumbs->$#*;
9 Added: <li class="breadcrumb-item<%= $is_last ? ' active' : '' %>">
9 10 % if (defined $breadcrumb->{href}) {
10 Removed: <a href="<%= $breadcrumb->{href} %>"><%= $breadcrumb->{label} %></a>
11 Added: <a class="text-success text-decoration-none fw-bold" href="<%= $breadcrumb->{href} %>"><%= $breadcrumb->{label} %></a>
11 12 % } else {
12 13 <span aria-current="page"><%= $breadcrumb->{label} %></span>
13 14 % }
@@ -16,7 +17,7 @@
16 17 </ol>
17 18 </nav>
18 19 % if (defined $graph_control_aria_label || $show_overview_status) {
19 Removed: <div class="toolbar-items">
20 Added: <div class="toolbar-items d-flex align-items-center gap-2 ms-auto">
20 21 % if (defined $graph_control_aria_label) {
21 22 %= include 'dashboard/graph-control', aria_label => $graph_control_aria_label
22 23 % }
roles/dashboard/templates/layouts/default.html.ep
index 0fe68b4a..d8c6e97d 100644..100644
@@ -6,48 +6,77 @@
6 6 <meta charset="utf-8">
7 7 <title><%= title %></title>
8 8 <meta name="viewport" content="width=device-width, initial-scale=1">
9 Removed: <link rel="preconnect" href="https://fonts.googleapis.com">
10 Removed: <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11 Removed: <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
9 Added: <link
10 Added: rel="stylesheet"
11 Added: href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
12 Added: integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
13 Added: crossorigin="anonymous">
12 14 <link rel="stylesheet" href="/css/app.css">
13 15 </head>
14 Removed: <body>
15 Removed: <header class="page-header" aria-label="Dashboard navigation">
16 Removed: <a class="page-brand" href="/">FAPG</a>
17 Removed: <input class="nav-toggle" id="nav-links-toggle" type="checkbox" aria-label="Toggle navigation">
18 Removed: <label class="nav-toggle-button" for="nav-links-toggle" aria-hidden="true">
19 Removed: <span></span>
20 Removed: <span></span>
21 Removed: <span></span>
22 Removed: </label>
23 Removed: <div class="nav-links">
24 Removed: <a href="/probes">Probes</a>
25 Removed: <a href="/downloads">Downloads</a>
16 Added: <body class="bg-light text-dark">
17 Added:
18 Added: <nav class="navbar navbar-expand-md bg-white border-bottom shadow-sm sticky-top mb-3" aria-label="Dashboard navigation">
19 Added: <div class="container-fluid px-3">
20 Added:
21 Added: <a class="navbar-brand fw-bold text-success" href="/">FAPG</a>
22 Added:
23 Added: <div class="d-flex align-items-center gap-2 ms-auto order-md-last">
24 Added: <a
25 Added: class="header-status-link is-unknown"
26 Added: href="/status"
27 Added: data-header-status
28 Added: aria-label="DAQ status unknown"
29 Added: title="Checking DAQ status">
30 Added: <span class="header-status-led"></span>
31 Added: </a>
32 Added: % for my $item (status_items()->@*) {
33 Added: <span
34 Added: hidden
35 Added: data-header-status-item="<%= $item->{key} %>"
36 Added: data-header-status-label="<%= $item->{label} %>"></span>
37 Added: % }
38 Added:
39 Added: <button
40 Added: class="navbar-toggler"
41 Added: type="button"
42 Added: data-bs-toggle="collapse"
43 Added: data-bs-target="#navbarLinks"
44 Added: aria-controls="navbarLinks"
45 Added: aria-expanded="false"
46 Added: aria-label="Toggle navigation">
47 Added: <span class="navbar-toggler-icon"></span>
48 Added: </button>
49 Added: </div>
50 Added:
51 Added: <div class="collapse navbar-collapse" id="navbarLinks">
52 Added: <ul class="navbar-nav ms-auto mb-2 mb-md-0">
53 Added: <li class="nav-item">
54 Added: <a class="nav-link fw-semibold" href="/probes">Probes</a>
55 Added: </li>
56 Added: <li class="nav-item">
57 Added: <a class="nav-link fw-semibold" href="/downloads">Downloads</a>
58 Added: </li>
59 Added: </ul>
60 Added: </div>
61 Added:
26 62 </div>
27 Removed: <a
28 Removed: class="header-status-link is-unknown"
29 Removed: href="/status"
30 Removed: data-header-status
31 Removed: aria-label="DAQ status unknown"
32 Removed: title="Checking DAQ status">
33 Removed: <span class="header-status-led"></span>
34 Removed: </a>
35 Removed: % for my $item (status_items()->@*) {
36 Removed: <span
37 Removed: hidden
38 Removed: data-header-status-item="<%= $item->{key} %>"
39 Removed: data-header-status-label="<%= $item->{label} %>"></span>
40 Removed: % }
41 Removed: </header>
63 Added: </nav>
64 Added:
42 65 <main class="<%= stash('container_class') // 'container' %>">
43 66 %= include 'dashboard/toolbar', breadcrumbs => (stash('breadcrumbs') // []), graph_control_aria_label => stash('graph_control_aria_label'), show_overview_status => stash('show_overview_status')
44 67 <%= content %>
45 68 </main>
46 Removed: <footer class="site-footer">
47 Removed: <p>
48 Removed: <a href="https://www.lafermeaquaponique.com/">lafermeaquaponique.com</a>
69 Added:
70 Added: <footer class="site-footer text-center text-muted small py-3 mt-4">
71 Added: <p class="mb-0">
72 Added: <a class="text-muted" href="https://www.lafermeaquaponique.com/">lafermeaquaponique.com</a>
49 73 </p>
50 Removed: <p>Copyright &copy; 2026 Marius Peter</p>
74 Added: <p class="mb-0">Copyright &copy; 2026 Marius Peter</p>
51 75 </footer>
76 Added:
77 Added: <script
78 Added: src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
79 Added: integrity="sha384-YvpcrYf0tY3lHB60NNkmXc4s9bIOgUxi8T/jzmDPtGBsYjRfXpjIIl0gEiDIVrqf"
80 Added: crossorigin="anonymous"></script>
52 81 </body>
53 82 </html>