Author SHA1 Message Date
Logan CusanoandClaude Opus 5.5 36fdfb2758 map: mount without waiting on the 500-call window; legend collapsible, hidden under open layers list
The Live map was held behind useCalls(500), measured at ~9.2s to first map
paint on prod; it now mounts as soon as the saved view is known (nodes are
awaited only when there is no view). The legend painted over the expanded
Leaflet layers list (z-1001 vs the map's z-0 stacking context): it is now a
<details> that hides while the list is open and starts folded on short
viewports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:40:57 -05:00
Logan CusanoandClaude Opus 5.5 7cd289874a frontend: Iris palette — graphite neutrals + indigo-violet accent
Tokens in globals.css, Tailwind gray/indigo scales remapped so the ~350
legacy raw-class uses follow the palette, light-mode override sheet moved
off slate, map accent hexes updated. Severity hues retuned for AA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:38:59 -05:00
4 changed files with 110 additions and 75 deletions
+77 -68
View File
@@ -4,6 +4,8 @@
@import 'leaflet/dist/leaflet.css'; @import 'leaflet/dist/leaflet.css';
/* Palette: "Iris" (2026-11) — graphite neutrals + one indigo-violet accent.
* Tailwind's gray/indigo scales are remapped to match in tailwind.config.ts. */
/* ── Design tokens ──────────────────────────────────────────────────────────── /* ── Design tokens ────────────────────────────────────────────────────────────
* Single source of truth for surface, ink and encoding colour. `:root` is the * Single source of truth for surface, ink and encoding colour. `:root` is the
* LIGHT theme; `.dark` on <html> (set by ThemeProvider, darkMode: ["class"]) * LIGHT theme; `.dark` on <html> (set by ThemeProvider, darkMode: ["class"])
@@ -16,43 +18,43 @@
* Do not add a hue here for a category; add a glyph. * Do not add a hue here for a category; add a glyph.
*/ */
:root { :root {
--page: #F4F6F9; --page: #F7F7F8;
--surface: #FFFFFF; --surface: #FFFFFF;
--raised: #F7F9FB; --raised: #F2F2F5;
--line: rgba(11,17,27,.11); --line: rgba(24,24,27,.10);
--line-strong: rgba(11,17,27,.22); --line-strong: rgba(24,24,27,.20);
--ink: #101620; --ink: #18181B;
--ink-2: #46536A; --ink-2: #52525B;
--ink-muted: #6B788C; --ink-muted: #6B6B76;
--accent: #2A78D6; --accent: #5E6AD2;
--sev-moderate: #B07800; --sev-moderate: #B54708;
--sev-major: #C0281F; --sev-major: #D92D20;
--map-bg: #E8ECF1; --map-bg: #ECECF0;
--map-block: #DFE4EB; --map-block: #E1E1E8;
--map-road: #FFFFFF; --map-road: #FFFFFF;
--map-water: #D3E2EE; --map-water: #DADDF2;
} }
.dark { .dark {
--page: #0B0E13; --page: #08090A;
--surface: #141922; --surface: #111214;
--raised: #1B2230; --raised: #1A1B1F;
--line: rgba(255,255,255,.09); --line: rgba(255,255,255,.08);
--line-strong: rgba(255,255,255,.17); --line-strong: rgba(255,255,255,.16);
--ink: #E8EBF0; --ink: #EDEDEF;
--ink-2: #A5B0C2; --ink-2: #A9A9B3;
--ink-muted: #8C98AE; /* was #77839A: 4.2:1 on raised, failed AA at the 10-12px it's used at (#136) */ --ink-muted: #8B8B96; /* 5.3:1 on raised — holds AA at 10-12px (#136) */
--accent: #3987E5; --accent: #8B93F0;
--sev-moderate: #C98500; --sev-moderate: #FDB022;
--sev-major: #F05252; /* was #D03B3B: 3.7:1 as text on surface (#136) */ --sev-major: #F97066; /* 6.6:1 as text on surface (#136) */
--map-bg: #0E131B; --map-bg: #0D0E11;
--map-block: #161C26; --map-block: #16171B;
--map-road: #232C3A; --map-road: #26272D;
--map-water: #12202E; --map-water: #141729;
} }
/* ── Base ─────────────────────────────────────────────────────────────────── */ /* ── Base ─────────────────────────────────────────────────────────────────── */
@@ -69,7 +71,7 @@ html, body {
/* Dark mode: raw gray-600/700 text sits ~2.4:1 on the near-black page — below /* 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). */ * readable. Lift both to gray-500 (~4:1). */
.dark .text-gray-600, .dark .text-gray-700 { color: #6b7280; } .dark .text-gray-600, .dark .text-gray-700 { color: #7A7A85; }
/* ── Light mode overrides ─────────────────────────────────────────────────── */ /* ── Light mode overrides ─────────────────────────────────────────────────── */
/* /*
@@ -80,34 +82,34 @@ html, body {
/* Structural backgrounds */ /* Structural backgrounds */
html:not(.dark), html:not(.dark),
html:not(.dark) body { background-color: #f1f5f9; color: #0f172a; } html:not(.dark) body { background-color: #f2f2f5; color: #18181b; }
html:not(.dark) .bg-gray-950 { background-color: #f1f5f9 !important; } html:not(.dark) .bg-gray-950 { background-color: #f2f2f5 !important; }
html:not(.dark) .bg-gray-950\/95 { background-color: rgba(241,245,249,0.95) !important; } html:not(.dark) .bg-gray-950\/95 { background-color: rgba(242,242,245,0.95) !important; }
html:not(.dark) .bg-gray-900 { background-color: #ffffff !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\/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\/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-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 { background-color: #f2f2f5 !important; }
html:not(.dark) .bg-gray-800\/60 { background-color: rgba(226,232,240,0.70) !important; } html:not(.dark) .bg-gray-800\/60 { background-color: rgba(228,228,231,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\/40 { background-color: rgba(242,242,245,0.60) !important; }
html:not(.dark) .bg-gray-800\/30 { background-color: rgba(241,245,249,0.50) !important; } html:not(.dark) .bg-gray-800\/30 { background-color: rgba(242,242,245,0.50) !important; }
html:not(.dark) .bg-gray-700 { background-color: #e2e8f0 !important; } html:not(.dark) .bg-gray-700 { background-color: #e4e4e7 !important; }
/* Borders */ /* Borders */
html:not(.dark) .border-gray-800 { border-color: #e2e8f0 !important; } html:not(.dark) .border-gray-800 { border-color: #e4e4e7 !important; }
html:not(.dark) .border-gray-700 { border-color: #cbd5e1 !important; } html:not(.dark) .border-gray-700 { border-color: #d4d4d8 !important; }
html:not(.dark) .border-gray-600 { border-color: #94a3b8 !important; } html:not(.dark) .border-gray-600 { border-color: #a1a1aa !important; }
html:not(.dark) .border-gray-800\/60 { border-color: #e2e8f0 !important; } html:not(.dark) .border-gray-800\/60 { border-color: #e4e4e7 !important; }
html:not(.dark) .divide-gray-800 > * + * { border-color: #e2e8f0 !important; } html:not(.dark) .divide-gray-800 > * + * { border-color: #e4e4e7 !important; }
/* Text */ /* Text */
html:not(.dark) .text-white { color: #0f172a !important; } html:not(.dark) .text-white { color: #18181b !important; }
html:not(.dark) .text-gray-100 { color: #1e293b !important; } html:not(.dark) .text-gray-100 { color: #27272a !important; }
html:not(.dark) .text-gray-200 { color: #1e293b !important; } html:not(.dark) .text-gray-200 { color: #27272a !important; }
html:not(.dark) .text-gray-300 { color: #334155 !important; } html:not(.dark) .text-gray-300 { color: #3f3f46 !important; }
html:not(.dark) .text-gray-400 { color: #475569 !important; } html:not(.dark) .text-gray-400 { color: #52525b !important; }
html:not(.dark) .text-gray-500 { color: #64748b !important; } html:not(.dark) .text-gray-500 { color: #6b6b76 !important; }
html:not(.dark) .text-gray-600 { color: #94a3b8 !important; } html:not(.dark) .text-gray-600 { color: #a1a1aa !important; }
/* …except on saturated fills (primary/danger/success buttons), where the fill /* …except on saturated fills (primary/danger/success buttons), where the fill
* stays dark in both themes — remapping to navy made their labels unreadable. */ * stays dark in both themes — remapping to navy made their labels unreadable. */
@@ -116,15 +118,15 @@ html:not(.dark) .text-white:is(.bg-accent, .bg-sev-major, .bg-sev-moderate,
.bg-green-600, .bg-green-700, .bg-yellow-700, .bg-yellow-800, .bg-gray-600) { color: #ffffff !important; } .bg-green-600, .bg-green-700, .bg-yellow-700, .bg-yellow-800, .bg-gray-600) { color: #ffffff !important; }
/* Hover states */ /* Hover states */
html:not(.dark) .hover\:bg-gray-900:hover { background-color: #f8fafc !important; } html:not(.dark) .hover\:bg-gray-900:hover { background-color: #fafafa !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-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-800:hover { background-color: #f2f2f5 !important; }
html:not(.dark) .hover\:bg-gray-700:hover { background-color: #e2e8f0 !important; } html:not(.dark) .hover\:bg-gray-700:hover { background-color: #e4e4e7 !important; }
html:not(.dark) .active\:bg-gray-800:active { background-color: #f1f5f9 !important; } html:not(.dark) .active\:bg-gray-800:active { background-color: #f2f2f5 !important; }
/* Hover text */ /* Hover text */
html:not(.dark) .hover\:text-gray-300:hover { color: #334155 !important; } html:not(.dark) .hover\:text-gray-300:hover { color: #3f3f46 !important; }
html:not(.dark) .hover\:text-gray-200:hover { color: #1e293b !important; } html:not(.dark) .hover\:text-gray-200:hover { color: #27272a !important; }
/* ── Accent badge palette (dark → light) ─────────────────────────────────── */ /* ── Accent badge palette (dark → light) ─────────────────────────────────── */
@@ -162,11 +164,11 @@ html:not(.dark) .text-green-400 { color: #16a34a !important; }
html:not(.dark) .border-green-800 { border-color: #86efac !important; } html:not(.dark) .border-green-800 { border-color: #86efac !important; }
/* Unconfigured / Info */ /* Unconfigured / Info */
html:not(.dark) .bg-indigo-950 { background-color: #eef2ff !important; } html:not(.dark) .bg-indigo-950 { background-color: #f0f1fd !important; }
html:not(.dark) .bg-indigo-900 { background-color: #eef2ff !important; } html:not(.dark) .bg-indigo-900 { background-color: #f0f1fd !important; }
html:not(.dark) .text-indigo-300 { color: #4338ca !important; } html:not(.dark) .text-indigo-300 { color: #4e58b8 !important; }
html:not(.dark) .text-indigo-400 { color: #6366f1 !important; } html:not(.dark) .text-indigo-400 { color: #5e6ad2 !important; }
html:not(.dark) .border-indigo-800 { border-color: #a5b4fc !important; } html:not(.dark) .border-indigo-800 { border-color: #c8ccf6 !important; }
/* ── Pulsing ring for recording nodes ────────────────────────────────────── */ /* ── Pulsing ring for recording nodes ────────────────────────────────────── */
@keyframes pulse-ring { @keyframes pulse-ring {
@@ -191,6 +193,13 @@ html:not(.dark) .border-indigo-800 { border-color: #a5b4fc !important; }
z-index: 0; z-index: 0;
} }
/* The legend floats above the map's stacking context, so it would cover the
* expanded layers list (both live in the right-hand column). Hide it while the
* list is open. */
.relative:has(.leaflet-control-layers-expanded) > .drb-legend {
visibility: hidden;
}
/* "Dark" base layer (#136): the prod tile URL is bright OSM, so dark-theme /* "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 * 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 * keyless OSM source (CARTO's dark_all CDN proved flaky, #117) while giving
@@ -203,7 +212,7 @@ html:not(.dark) .border-indigo-800 { border-color: #a5b4fc !important; }
* gray-900), so the theme's muted ink — light grey in dark mode, ~2.9:1 on * 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). */ * white — is pinned to a grey that holds AA on white (~6:1). */
.leaflet-popup-content .text-ink-muted { .leaflet-popup-content .text-ink-muted {
color: #5B6577; color: #5F5F69;
} }
/* Pinned 511 camera tiles (MapView DotCameraLayer): strip Leaflet's tooltip /* Pinned 511 camera tiles (MapView DotCameraLayer): strip Leaflet's tooltip
@@ -224,17 +233,17 @@ html:not(.dark) .border-indigo-800 { border-color: #a5b4fc !important; }
* "Ossining / Westchester" next to "no area set"). Dimmer + italic in dark. */ * "Ossining / Westchester" next to "no area set"). Dimmer + italic in dark. */
.dark input::placeholder, .dark input::placeholder,
.dark textarea::placeholder { .dark textarea::placeholder {
color: #5F6B80; color: #6E6E78;
font-style: italic; font-style: italic;
} }
html:not(.dark) input:not([type="submit"]):not([type="button"]):not([type="reset"]), html:not(.dark) input:not([type="submit"]):not([type="button"]):not([type="reset"]),
html:not(.dark) select, html:not(.dark) select,
html:not(.dark) textarea { html:not(.dark) textarea {
color: #0f172a; color: #18181b;
} }
html:not(.dark) input::placeholder, html:not(.dark) input::placeholder,
html:not(.dark) textarea::placeholder { html:not(.dark) textarea::placeholder {
color: #94a3b8; color: #a1a1aa;
} }
/* ── Marketing/product surface additions (2026-08 overhaul) ───────────────── /* ── Marketing/product surface additions (2026-08 overhaul) ─────────────────
@@ -245,8 +254,8 @@ html:not(.dark) textarea::placeholder {
*/ */
/* Tinted accent surfaces (plan highlight cards, "included" checks, danger zones) */ /* 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) .bg-indigo-600\/10 { background-color: rgba(94,106,210,0.08) !important; }
html:not(.dark) .border-indigo-600\/40 { border-color: rgba(79,70,229,0.35) !important; } html:not(.dark) .border-indigo-600\/40 { border-color: rgba(94,106,210,0.35) !important; }
html:not(.dark) .bg-green-600\/10 { background-color: rgba(22,163,74,0.08) !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) .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) .border-red-600\/40 { border-color: rgba(220,38,38,0.35) !important; }
@@ -255,10 +264,10 @@ html:not(.dark) .border-yellow-600\/40 { border-color: rgba(202,138,4,0.35) !imp
/* Marketing hero background — subtle radial glow, brand-neutral in both themes */ /* Marketing hero background — subtle radial glow, brand-neutral in both themes */
.marketing-hero-bg { .marketing-hero-bg {
background-image: radial-gradient(ellipse 80% 50% at 50% -10%, rgba(99,102,241,0.25), transparent 60%); background-image: radial-gradient(ellipse 80% 50% at 50% -10%, rgba(94,106,210,0.25), transparent 60%);
} }
html:not(.dark) .marketing-hero-bg { html:not(.dark) .marketing-hero-bg {
background-image: radial-gradient(ellipse 80% 50% at 50% -10%, rgba(99,102,241,0.12), transparent 60%); background-image: radial-gradient(ellipse 80% 50% at 50% -10%, rgba(94,106,210,0.12), transparent 60%);
} }
/* Skeleton loading shimmer */ /* Skeleton loading shimmer */
+5 -1
View File
@@ -560,7 +560,11 @@ export function LiveView() {
{/* Degraded → grey the map and its incident rail (the rail lives inside {/* Degraded → grey the map and its incident rail (the rail lives inside
MapView, so it is wrapped here rather than edited). */} MapView, so it is wrapped here rather than edited). */}
<div className={`w-full h-full transition-[filter,opacity] ${health.degraded ? "grayscale opacity-60" : ""}`}> <div className={`w-full h-full transition-[filter,opacity] ${health.degraded ? "grayscale opacity-60" : ""}`}>
{nodesLoading || callsLoading || !mapState ? ( {/* Mount the map as soon as its starting view is known. The 500-call
window took ~9s on prod and used to hold the whole map back;
calls and paths now stream in once it lands. Nodes are only
awaited when there's no saved view, since they pick the centre. */}
{!mapState || (!mapState.view && nodesLoading) ? (
<div className="w-full h-full flex items-center justify-center text-ink-muted text-sm">Loading map…</div> <div className="w-full h-full flex items-center justify-center text-ink-muted text-sm">Loading map…</div>
) : ( ) : (
<MapView <MapView
+15 -4
View File
@@ -887,7 +887,7 @@ function CameraViewshedLayer({ highlightIds }: { highlightIds: Set<string> }) {
const hot = highlightIds.has(c.id); const hot = highlightIds.has(c.id);
return ( return (
<Polygon key={`vs-${c.id}`} positions={wedge} <Polygon key={`vs-${c.id}`} positions={wedge}
pathOptions={{ color: hot ? "#3987E5" : "#A5B0C2", weight: hot ? 1 : 0.5, opacity: hot ? 0.8 : 0.4, fillOpacity: hot ? 0.18 : 0.06 }}> pathOptions={{ color: hot ? "#8B93F0" : "#A9A9B3", weight: hot ? 1 : 0.5, opacity: hot ? 0.8 : 0.4, fillOpacity: hot ? 0.18 : 0.06 }}>
<Tooltip sticky>{c.name} — approximate view (from the road&apos;s direction of travel, not a surveyed heading)</Tooltip> <Tooltip sticky>{c.name} — approximate view (from the road&apos;s direction of travel, not a surveyed heading)</Tooltip>
</Polygon> </Polygon>
); );
@@ -902,7 +902,7 @@ function CameraViewshedLayer({ highlightIds }: { highlightIds: Set<string> }) {
function nearCameraIcon(): L.DivIcon { function nearCameraIcon(): L.DivIcon {
return L.divIcon({ return L.divIcon({
className: "", className: "",
html: `<div style="width:20px;height:16px;border-radius:4px;background:#0b1f3a;border:2px solid #3987E5;display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 3px rgba(57,135,229,.25)"><svg width="12" height="9" viewBox="0 0 16 12"><rect x="0.5" y="1.5" width="11" height="9" rx="2" fill="none" stroke="#8DB9F0" stroke-width="1.5"/><polygon points="12,4 15.5,2 15.5,10 12,8" fill="#8DB9F0"/></svg></div>`, html: `<div style="width:20px;height:16px;border-radius:4px;background:#1C1F42;border:2px solid #8B93F0;display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 3px rgba(57,135,229,.25)"><svg width="12" height="9" viewBox="0 0 16 12"><rect x="0.5" y="1.5" width="11" height="9" rx="2" fill="none" stroke="#8DB9F0" stroke-width="1.5"/><polygon points="12,4 15.5,2 15.5,10 12,8" fill="#8DB9F0"/></svg></div>`,
iconSize: [20, 16], iconSize: [20, 16],
iconAnchor: [10, 8], iconAnchor: [10, 8],
}); });
@@ -917,7 +917,7 @@ function NearCameraLayer({ cameras, head, onOpenCamera }: {
<> <>
{head && ( {head && (
<Circle center={head} radius={NEAR_CAMERA_KM * 1000} interactive={false} <Circle center={head} radius={NEAR_CAMERA_KM * 1000} interactive={false}
pathOptions={{ color: "#3987E5", weight: 1, opacity: 0.6, fill: false, dashArray: "2 5" }} /> pathOptions={{ color: "#8B93F0", weight: 1, opacity: 0.6, fill: false, dashArray: "2 5" }} />
)} )}
{cameras.map(({ cam, km: d }) => ( {cameras.map(({ cam, km: d }) => (
<Marker key={`near-${cam.id}`} position={[cam.lat, cam.lon]} icon={nearCameraIcon()} zIndexOffset={600} <Marker key={`near-${cam.id}`} position={[cam.lat, cam.lon]} icon={nearCameraIcon()} zIndexOffset={600}
@@ -1620,6 +1620,9 @@ function MapView({
const [wxShown, setWxShown] = useState(() => startLayers.has("wx")); const [wxShown, setWxShown] = useState(() => startLayers.has("wx"));
const [radarLoop, setRadarLoop] = useState(false); // off by default (#237); not persisted const [radarLoop, setRadarLoop] = useState(false); // off by default (#237); not persisted
const loopClock = useMemo(createLoopClock, []); const loopClock = useMemo(createLoopClock, []);
// Short viewports (laptops, the 352px incident-page map) start with the
// legend folded to a chip so it doesn't cover the right side of the map.
const [legendOpenAtStart] = useState(() => typeof window === "undefined" || window.innerHeight >= 760);
// The altitude key only belongs in the legend while the opt-in Aircraft // The altitude key only belongs in the legend while the opt-in Aircraft
// overlay is actually on (server-26#185); same for the outage key (#184). // overlay is actually on (server-26#185); same for the outage key (#184).
@@ -1931,7 +1934,14 @@ function MapView({
</div> </div>
{/* ── Legend — shape-first, both themes. Never a bare colour swatch. ──── */} {/* ── Legend — shape-first, both themes. Never a bare colour swatch. ──── */}
<div className="absolute bottom-8 right-3 z-[1001] bg-surface/90 border border-line rounded-lg px-3 py-2.5 text-xs pointer-events-none space-y-2 max-h-[calc(100%-4rem)] overflow-y-auto"> {/* Collapsible (one click to a "Legend" chip) and hidden while the Leaflet
layers list is expanded: the map sits in a z-0 stacking context, so the
legend at z-1001 used to paint over the bottom of that list. */}
<details open={legendOpenAtStart} className="drb-legend group/legend absolute bottom-8 right-3 z-[1001] bg-surface/90 border border-line rounded-lg text-xs pointer-events-none max-h-[calc(100%-4rem)] overflow-y-auto">
<summary className="pointer-events-auto cursor-pointer list-none select-none px-3 py-1.5 text-ink-muted font-medium text-[10px] uppercase tracking-wide flex items-center gap-1.5">
<span className="group-open/legend:hidden">▸</span><span className="hidden group-open/legend:inline">▾</span> Legend
</summary>
<div className="px-3 pb-2.5 space-y-2">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-ink-muted font-medium text-[10px] uppercase tracking-wide">Severity</p> <p className="text-ink-muted font-medium text-[10px] uppercase tracking-wide">Severity</p>
{(["major", "moderate", "minor", "routine"] as Severity[]).map((sev) => ( {(["major", "moderate", "minor", "routine"] as Severity[]).map((sev) => (
@@ -2071,6 +2081,7 @@ function MapView({
))} ))}
</div> </div>
</div> </div>
</details>
{/* ── Incident overlay panel ───────────────────────────────────────────── */} {/* ── Incident overlay panel ───────────────────────────────────────────── */}
{incidents.length > 0 && ( {incidents.length > 0 && (
+12 -1
View File
@@ -22,6 +22,17 @@ const config: Config = {
// work — a bare var() hex gives Tailwind nothing to apply alpha to, and the // work — a bare var() hex gives Tailwind nothing to apply alpha to, and the
// class silently generates no rule (map overlays rendered see-through). // class silently generates no rule (map overlays rendered see-through).
colors: { colors: {
// "Iris" palette: the legacy raw gray-*/indigo-* classes (~350 uses)
// resolve to graphite + the brand accent instead of Tailwind's slate-blue
// defaults, so un-migrated components match the tokens above.
gray: {
50: "#F7F7F8", 100: "#EDEDEF", 200: "#E4E4E7", 300: "#C9C9D0", 400: "#A1A1AA",
500: "#7A7A85", 600: "#4A4B53", 700: "#2A2B31", 800: "#1A1B1F", 900: "#111214", 950: "#08090A",
},
indigo: {
50: "#F0F1FD", 100: "#E2E4FB", 200: "#C8CCF6", 300: "#A8AEF1", 400: "#8B93F0",
500: "#6F79E0", 600: "#5E6AD2", 700: "#4E58B8", 800: "#3F4794", 900: "#313770", 950: "#1C1F42",
},
page: tok("page"), page: tok("page"),
surface: tok("surface"), surface: tok("surface"),
raised: tok("raised"), raised: tok("raised"),
@@ -51,7 +62,7 @@ const config: Config = {
boxShadow: { boxShadow: {
card: "0 1px 2px 0 rgb(0 0 0 / 0.4), 0 1px 3px 0 rgb(0 0 0 / 0.3)", card: "0 1px 2px 0 rgb(0 0 0 / 0.4), 0 1px 3px 0 rgb(0 0 0 / 0.3)",
"card-hover": "0 4px 12px 0 rgb(0 0 0 / 0.45), 0 2px 4px 0 rgb(0 0 0 / 0.3)", "card-hover": "0 4px 12px 0 rgb(0 0 0 / 0.45), 0 2px 4px 0 rgb(0 0 0 / 0.3)",
glow: "0 0 0 1px rgb(99 102 241 / 0.4), 0 0 24px 0 rgb(99 102 241 / 0.25)", glow: "0 0 0 1px rgb(94 106 210 / 0.4), 0 0 24px 0 rgb(94 106 210 / 0.25)",
}, },
animation: { animation: {
"fade-in": "fade-in 0.4s ease-out", "fade-in": "fade-in 0.4s ease-out",