- Token colors wrapped in color-mix so opacity modifiers (bg-surface/90 etc.) generate rules; map overlay panels were rendering see-through. - Light mode: keep white text on saturated fills (primary/danger buttons were remapped to navy); map text-gray-200, bg-gray-800/60, border-gray-600. - Dark mode: lift text-gray-600/700 to gray-500 for contrast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
249 lines
12 KiB
CSS
249 lines
12 KiB
CSS
@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: #77839A;
|
||
|
||
--accent: #3987E5;
|
||
--sev-moderate: #C98500;
|
||
--sev-major: #D03B3B;
|
||
|
||
--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;
|
||
}
|
||
|
||
/* 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 ─────────────────────────────────────────────────────────── */
|
||
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;
|
||
}
|