Files
server-26/drb-frontend/app/globals.css
T
Logan CusanoandClaude Opus 5.5 94898d1af1 fix(frontend): issues from the 10-25 Chrome sweep
- Incident detail "← Incidents" used router.back(), so arriving from Live
  or a shared link it went to the map / out of the app. Now a Link.
- Resolved incidents showed "0m elapsed" (manual resolve sets no
  resolved_at, updated_at can predate the last call): end is now the
  latest of resolved_at, updated_at and the newest call.
- Admin: the max-width toggle wrapped the tab bar, so it reflowed on every
  tab switch and left the cursor over e.g. "Close calls". Body only now.
- STT Eval sat on "Loading…" forever when a scanned page had zero matches
  but a next_cursor — the refill effect didn't depend on cursor.
- Trip event blocks used dark-only bg-indigo-950/60: unreadable in light
  mode. Theme tokens now.
- Reassign modal said "connected for the first time" on configured nodes.
- Dark-mode placeholders dimmed + italic; they read as filled-in values.

Frontend unverified locally (no Node, #207); CI build typechecks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 23:12:07 -04:00

287 lines
14 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'leaflet/dist/leaflet.css';
/* ── Design tokens ────────────────────────────────────────────────────────────
* Single source of truth for surface, ink and encoding colour. `:root` is the
* LIGHT theme; `.dark` on <html> (set by ThemeProvider, darkMode: ["class"])
* swaps the same names to their dark values. Tailwind reads these through
* theme.extend.colors, so components say `bg-surface` / `text-ink-muted`
* instead of `bg-gray-900` / `text-gray-400`.
*
* Colour encoding is validated for colour-blindness — see UI_REDESIGN.md §2.3.
* Severity is the ONLY hue channel. Incident type is shape. Node state is value.
* Do not add a hue here for a category; add a glyph.
*/
:root {
--page: #F4F6F9;
--surface: #FFFFFF;
--raised: #F7F9FB;
--line: rgba(11,17,27,.11);
--line-strong: rgba(11,17,27,.22);
--ink: #101620;
--ink-2: #46536A;
--ink-muted: #6B788C;
--accent: #2A78D6;
--sev-moderate: #B07800;
--sev-major: #C0281F;
--map-bg: #E8ECF1;
--map-block: #DFE4EB;
--map-road: #FFFFFF;
--map-water: #D3E2EE;
}
.dark {
--page: #0B0E13;
--surface: #141922;
--raised: #1B2230;
--line: rgba(255,255,255,.09);
--line-strong: rgba(255,255,255,.17);
--ink: #E8EBF0;
--ink-2: #A5B0C2;
--ink-muted: #8C98AE; /* was #77839A: 4.2:1 on raised, failed AA at the 10-12px it's used at (#136) */
--accent: #3987E5;
--sev-moderate: #C98500;
--sev-major: #F05252; /* was #D03B3B: 3.7:1 as text on surface (#136) */
--map-bg: #0E131B;
--map-block: #161C26;
--map-road: #232C3A;
--map-water: #12202E;
}
/* ── Base ─────────────────────────────────────────────────────────────────── */
html, body {
@apply bg-page text-ink;
font-family: var(--font-sans), system-ui, sans-serif;
}
/* Mono is for machine identifiers only — timestamps, talkgroups, unit
* callsigns, node ids, frequencies, incident ids, number columns. */
.font-mono, code, kbd, pre, samp {
font-family: var(--font-mono), ui-monospace, monospace;
}
/* Dark mode: raw gray-600/700 text sits ~2.4:1 on the near-black page — below
* readable. Lift both to gray-500 (~4:1). */
.dark .text-gray-600, .dark .text-gray-700 { color: #6b7280; }
/* ── Light mode overrides ─────────────────────────────────────────────────── */
/*
* The app's components use hardcoded dark-palette Tailwind classes (bg-gray-9xx,
* text-gray-xxx). Rather than adding dark: prefixes everywhere, we remap those
* classes here when the html element doesn't carry the .dark class.
*/
/* Structural backgrounds */
html:not(.dark),
html:not(.dark) body { background-color: #f1f5f9; color: #0f172a; }
html:not(.dark) .bg-gray-950 { background-color: #f1f5f9 !important; }
html:not(.dark) .bg-gray-950\/95 { background-color: rgba(241,245,249,0.95) !important; }
html:not(.dark) .bg-gray-900 { background-color: #ffffff !important; }
html:not(.dark) .bg-gray-900\/60 { background-color: rgba(255,255,255,0.85) !important; }
html:not(.dark) .bg-gray-900\/50 { background-color: rgba(255,255,255,0.75) !important; }
html:not(.dark) .bg-gray-900\/30 { background-color: rgba(255,255,255,0.50) !important; }
html:not(.dark) .bg-gray-800 { background-color: #f1f5f9 !important; }
html:not(.dark) .bg-gray-800\/60 { background-color: rgba(226,232,240,0.70) !important; }
html:not(.dark) .bg-gray-800\/40 { background-color: rgba(241,245,249,0.60) !important; }
html:not(.dark) .bg-gray-800\/30 { background-color: rgba(241,245,249,0.50) !important; }
html:not(.dark) .bg-gray-700 { background-color: #e2e8f0 !important; }
/* Borders */
html:not(.dark) .border-gray-800 { border-color: #e2e8f0 !important; }
html:not(.dark) .border-gray-700 { border-color: #cbd5e1 !important; }
html:not(.dark) .border-gray-600 { border-color: #94a3b8 !important; }
html:not(.dark) .border-gray-800\/60 { border-color: #e2e8f0 !important; }
html:not(.dark) .divide-gray-800 > * + * { border-color: #e2e8f0 !important; }
/* Text */
html:not(.dark) .text-white { color: #0f172a !important; }
html:not(.dark) .text-gray-100 { color: #1e293b !important; }
html:not(.dark) .text-gray-200 { color: #1e293b !important; }
html:not(.dark) .text-gray-300 { color: #334155 !important; }
html:not(.dark) .text-gray-400 { color: #475569 !important; }
html:not(.dark) .text-gray-500 { color: #64748b !important; }
html:not(.dark) .text-gray-600 { color: #94a3b8 !important; }
/* …except on saturated fills (primary/danger/success buttons), where the fill
* stays dark in both themes — remapping to navy made their labels unreadable. */
html:not(.dark) .text-white:is(.bg-accent, .bg-sev-major, .bg-sev-moderate,
.bg-indigo-500, .bg-indigo-600, .bg-indigo-700, .bg-red-600, .bg-red-700,
.bg-green-600, .bg-green-700, .bg-yellow-700, .bg-yellow-800, .bg-gray-600) { color: #ffffff !important; }
/* Hover states */
html:not(.dark) .hover\:bg-gray-900:hover { background-color: #f8fafc !important; }
html:not(.dark) .hover\:bg-gray-900\/50:hover { background-color: rgba(255,255,255,0.75) !important; }
html:not(.dark) .hover\:bg-gray-800:hover { background-color: #f1f5f9 !important; }
html:not(.dark) .hover\:bg-gray-700:hover { background-color: #e2e8f0 !important; }
html:not(.dark) .active\:bg-gray-800:active { background-color: #f1f5f9 !important; }
/* Hover text */
html:not(.dark) .hover\:text-gray-300:hover { color: #334155 !important; }
html:not(.dark) .hover\:text-gray-200:hover { color: #1e293b !important; }
/* ── Accent badge palette (dark → light) ─────────────────────────────────── */
/* Fire / Error */
html:not(.dark) .bg-red-900 { background-color: #fef2f2 !important; }
html:not(.dark) .bg-red-950 { background-color: #fff1f2 !important; }
html:not(.dark) .text-red-300 { color: #b91c1c !important; }
html:not(.dark) .text-red-400 { color: #dc2626 !important; }
html:not(.dark) .border-red-800 { border-color: #fca5a5 !important; }
/* Police */
html:not(.dark) .bg-blue-900 { background-color: #eff6ff !important; }
html:not(.dark) .bg-blue-950 { background-color: #eff6ff !important; }
html:not(.dark) .text-blue-300 { color: #1d4ed8 !important; }
html:not(.dark) .border-blue-800 { border-color: #93c5fd !important; }
/* EMS */
html:not(.dark) .bg-yellow-900 { background-color: #fefce8 !important; }
html:not(.dark) .bg-yellow-950 { background-color: #fefce8 !important; }
html:not(.dark) .text-yellow-300 { color: #a16207 !important; }
html:not(.dark) .text-yellow-400 { color: #ca8a04 !important; }
/* Accident / Recording */
html:not(.dark) .bg-orange-900 { background-color: #fff7ed !important; }
html:not(.dark) .bg-orange-950 { background-color: #fff7ed !important; }
html:not(.dark) .text-orange-300 { color: #c2410c !important; }
html:not(.dark) .text-orange-400 { color: #ea580c !important; }
html:not(.dark) .border-orange-800 { border-color: #fdba74 !important; }
/* Active / Online */
html:not(.dark) .bg-green-900 { background-color: #f0fdf4 !important; }
html:not(.dark) .bg-green-950 { background-color: #f0fdf4 !important; }
html:not(.dark) .text-green-300 { color: #15803d !important; }
html:not(.dark) .text-green-400 { color: #16a34a !important; }
html:not(.dark) .border-green-800 { border-color: #86efac !important; }
/* Unconfigured / Info */
html:not(.dark) .bg-indigo-950 { background-color: #eef2ff !important; }
html:not(.dark) .bg-indigo-900 { background-color: #eef2ff !important; }
html:not(.dark) .text-indigo-300 { color: #4338ca !important; }
html:not(.dark) .text-indigo-400 { color: #6366f1 !important; }
html:not(.dark) .border-indigo-800 { border-color: #a5b4fc !important; }
/* ── Pulsing ring for recording nodes ────────────────────────────────────── */
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 0.85; }
100% { transform: scale(2.4); opacity: 0; }
}
.node-pulse-ring {
animation: pulse-ring 1.8s ease-out infinite;
}
/* ── Leaflet stacking fix ─────────────────────────────────────────────────────
* Leaflet's internal panes (z-index 200–700) and its zoom / layers controls
* (z-index 1000) otherwise paint above the sticky app Nav (z-40) and any modal
* overlay — on Live this put the account dropdown *behind* the map. Pinning the
* map container to its own low stacking context keeps Leaflet's internal layer
* order intact while dropping the whole map (tiles + controls) below the app
* chrome. The map's own overlay UI (legend, incident rail, clock, fit-all) sits
* outside .leaflet-container, so it is unaffected and still renders on top.
*/
.leaflet-container {
position: relative;
z-index: 0;
}
/* "Dark" base layer (#136): the prod tile URL is bright OSM, so dark-theme
* overlay cards sat on a bright, busy map. Inverting the raster keeps the
* keyless OSM source (CARTO's dark_all CDN proved flaky, #117) while giving
* the overlays the dark ground their palette was chosen for. */
.leaflet-tile-pane .drb-tiles-dark {
filter: invert(1) hue-rotate(180deg) brightness(0.9) contrast(0.9) saturate(0.6);
}
/* Leaflet popups stay white in both themes (their body text is a fixed
* gray-900), so the theme's muted ink — light grey in dark mode, ~2.9:1 on
* white — is pinned to a grey that holds AA on white (~6:1). */
.leaflet-popup-content .text-ink-muted {
color: #5B6577;
}
/* Pinned 511 camera tiles (MapView DotCameraLayer): strip Leaflet's tooltip
* padding and let the tile's own theme colours show, so the tile is no bigger
* than the video it holds. */
.leaflet-tooltip.drb-cam-pin {
padding: 0;
border-radius: 6px;
background: var(--surface);
border: 1px solid var(--line-strong);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
white-space: normal;
}
/* ── Form inputs ─────────────────────────────────────────────────────────── */
/* Placeholders must read as hints, not values: Tailwind's default #9ca3af on
* the dark surface looked like filled-in text (e.g. /systems Local Area showed
* "Ossining / Westchester" next to "no area set"). Dimmer + italic in dark. */
.dark input::placeholder,
.dark textarea::placeholder {
color: #5F6B80;
font-style: italic;
}
html:not(.dark) input:not([type="submit"]):not([type="button"]):not([type="reset"]),
html:not(.dark) select,
html:not(.dark) textarea {
color: #0f172a;
}
html:not(.dark) input::placeholder,
html:not(.dark) textarea::placeholder {
color: #94a3b8;
}
/* ── Marketing/product surface additions (2026-08 overhaul) ─────────────────
* Same pattern as above: components use hardcoded dark-palette Tailwind
* classes, remapped here for light mode instead of dark: prefixes.
* Only new classes introduced by the marketing pages / settings shell live
* below — everything else reuses the palette already mapped above.
*/
/* Tinted accent surfaces (plan highlight cards, "included" checks, danger zones) */
html:not(.dark) .bg-indigo-600\/10 { background-color: rgba(79,70,229,0.08) !important; }
html:not(.dark) .border-indigo-600\/40 { border-color: rgba(79,70,229,0.35) !important; }
html:not(.dark) .bg-green-600\/10 { background-color: rgba(22,163,74,0.08) !important; }
html:not(.dark) .bg-red-600\/10 { background-color: rgba(220,38,38,0.08) !important; }
html:not(.dark) .border-red-600\/40 { border-color: rgba(220,38,38,0.35) !important; }
html:not(.dark) .bg-yellow-600\/10 { background-color: rgba(202,138,4,0.08) !important; }
html:not(.dark) .border-yellow-600\/40 { border-color: rgba(202,138,4,0.35) !important; }
/* Marketing hero background — subtle radial glow, brand-neutral in both themes */
.marketing-hero-bg {
background-image: radial-gradient(ellipse 80% 50% at 50% -10%, rgba(99,102,241,0.25), transparent 60%);
}
html:not(.dark) .marketing-hero-bg {
background-image: radial-gradient(ellipse 80% 50% at 50% -10%, rgba(99,102,241,0.12), transparent 60%);
}
/* Skeleton loading shimmer */
@keyframes skeleton-pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.skeleton {
animation: skeleton-pulse 1.6s ease-in-out infinite;
}
/* ── Live ops wall transcript ticker ─────────────────────────────────────────
* The belt holds the items twice, so translating by -50% loops seamlessly.
* Paused on hover/focus; reduced-motion users get a static, scrollable row. */
@keyframes drb-ticker {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.drb-ticker-belt { animation: drb-ticker var(--drb-ticker-duration, 90s) linear infinite; }
.drb-ticker:hover .drb-ticker-belt,
.drb-ticker:focus-within .drb-ticker-belt { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
.drb-ticker-belt { animation: none; }
.drb-ticker { overflow-x: auto; }
}