fix(map): readable incident rail — dark basemap, opaque cards, AA colours (#136)

- "Dark" base layer was bright OSM in prod; invert the raster via
  .drb-tiles-dark so dark-theme overlays sit on a dark ground.
- Rail and mobile incident cards are opaque, not glass over the map.
- Dark --ink-muted #77839A -> #8C98AE, --sev-major #D03B3B -> #F05252
  (both failed AA as small text on surface/raised).
- Card borders used `color + "55"` on CSS var() colours, which is
  invalid CSS — no border rendered. Now color-mix.
- 10px rail labels -> 11px.

Frontend unverified: no Node on the authoring box (#207).

Closes #136

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-25 21:03:08 -04:00
co-authored by Claude Opus 5.5
parent 49166789f4
commit 8099871c68
2 changed files with 24 additions and 14 deletions
+10 -2
View File
@@ -43,11 +43,11 @@
--line-strong: rgba(255,255,255,.17); --line-strong: rgba(255,255,255,.17);
--ink: #E8EBF0; --ink: #E8EBF0;
--ink-2: #A5B0C2; --ink-2: #A5B0C2;
--ink-muted: #77839A; --ink-muted: #8C98AE; /* was #77839A: 4.2:1 on raised, failed AA at the 10-12px it's used at (#136) */
--accent: #3987E5; --accent: #3987E5;
--sev-moderate: #C98500; --sev-moderate: #C98500;
--sev-major: #D03B3B; --sev-major: #F05252; /* was #D03B3B: 3.7:1 as text on surface (#136) */
--map-bg: #0E131B; --map-bg: #0E131B;
--map-block: #161C26; --map-block: #161C26;
@@ -191,6 +191,14 @@ html:not(.dark) .border-indigo-800 { border-color: #a5b4fc !important; }
z-index: 0; 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);
}
/* Pinned 511 camera tiles (MapView DotCameraLayer): strip Leaflet's tooltip /* 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 * padding and let the tile's own theme colours show, so the tile is no bigger
* than the video it holds. */ * than the video it holds. */
+14 -12
View File
@@ -1802,6 +1802,7 @@ function MapView({
<TileLayer <TileLayer
url={MAP_TILE_URL} url={MAP_TILE_URL}
attribution={MAP_TILE_ATTRIBUTION} attribution={MAP_TILE_ATTRIBUTION}
className={MAP_TILE_URL.includes("dark") ? undefined : "drb-tiles-dark"}
/> />
</LayersControl.BaseLayer> </LayersControl.BaseLayer>
<LayersControl.BaseLayer checked={startBase === "light"} name={BASE_NAMES.light}> <LayersControl.BaseLayer checked={startBase === "light"} name={BASE_NAMES.light}>
@@ -2083,7 +2084,7 @@ function MapView({
{/* Gate A / A2 (server-26#46) — the rail's titles, locations and {/* Gate A / A2 (server-26#46) — the rail's titles, locations and
unit counts are pipeline output. Pinned above the scroll area unit counts are pipeline output. Pinned above the scroll area
so it cannot be scrolled off the screen it qualifies. */} so it cannot be scrolled off the screen it qualifies. */}
<div className="bg-surface/90 backdrop-blur-sm border border-line rounded-lg px-2 py-1.5 shrink-0 pointer-events-auto"> <div className="bg-surface border border-line-strong rounded-lg px-2 py-1.5 shrink-0 pointer-events-auto shadow">
{filteredFrom != null && ( {filteredFrom != null && (
<p className="text-[11px] text-ink-2 mb-1"><b className="text-ink font-semibold tabular-nums">{incidents.length} of {filteredFrom}</b> · filtered</p> <p className="text-[11px] text-ink-2 mb-1"><b className="text-ink font-semibold tabular-nums">{incidents.length} of {filteredFrom}</b> · filtered</p>
)} )}
@@ -2104,12 +2105,13 @@ function MapView({
const latest = (callsByIncident.get(inc.incident_id) ?? []) const latest = (callsByIncident.get(inc.incident_id) ?? [])
.filter(hasAudio) .filter(hasAudio)
.reduce<CallRecord | null>((best, c) => (!best || c.started_at > best.started_at ? c : best), null); .reduce<CallRecord | null>((best, c) => (!best || c.started_at > best.started_at ? c : best), null);
const baseClass = `w-full text-left bg-surface/90 backdrop-blur-sm border rounded-lg px-3 py-2 text-xs hover:brightness-110 transition-all ${selected ? "ring-2 ring-accent" : ""}`; // Opaque, not glass (#136): any see-through over a busy basemap put text on map colour.
const baseClass = `w-full text-left bg-surface border shadow rounded-lg px-3 py-2 text-xs hover:brightness-110 transition-all ${selected ? "ring-2 ring-accent" : ""}`;
const cardBody = ( const cardBody = (
<> <>
<div className="flex items-center gap-1.5 mb-0.5"> <div className="flex items-center gap-1.5 mb-0.5">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" dangerouslySetInnerHTML={{ __html: TYPE_GLYPH_PATHS[inc.type === "accident" ? "collision" : (inc.type && TYPE_GLYPH_PATHS[inc.type] ? inc.type : "other")] }} /> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" dangerouslySetInnerHTML={{ __html: TYPE_GLYPH_PATHS[inc.type === "accident" ? "collision" : (inc.type && TYPE_GLYPH_PATHS[inc.type] ? inc.type : "other")] }} />
<span className="uppercase tracking-wide font-semibold text-[10px]" style={{ color }}> <span className="uppercase tracking-wide font-semibold text-[11px]" style={{ color }}>
{isKnownSeverity(inc.severity) ? SEVERITY_LABEL[inc.severity] : "Unknown"} {isKnownSeverity(inc.severity) ? SEVERITY_LABEL[inc.severity] : "Unknown"}
</span> </span>
{live && <span className="ml-auto bg-accent text-white rounded px-1 text-[9px] font-semibold tracking-wide">ON AIR</span>} {live && <span className="ml-auto bg-accent text-white rounded px-1 text-[9px] font-semibold tracking-wide">ON AIR</span>}
@@ -2123,9 +2125,9 @@ function MapView({
{onSelectIncident && units.length > 0 && ( {onSelectIncident && units.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1"> <div className="flex flex-wrap gap-1 mt-1">
{units.slice(0, 4).map((u) => ( {units.slice(0, 4).map((u) => (
<span key={u} className="font-mono text-[10px] px-1 rounded bg-raised text-ink-2">{u}</span> <span key={u} className="font-mono text-[11px] px-1 rounded bg-raised text-ink-2">{u}</span>
))} ))}
{units.length > 4 && <span className="font-mono text-[10px] px-1 rounded bg-raised text-ink-muted">+{units.length - 4}</span>} {units.length > 4 && <span className="font-mono text-[11px] px-1 rounded bg-raised text-ink-2">+{units.length - 4}</span>}
</div> </div>
)} )}
<div className="flex items-center justify-between mt-0.5"> <div className="flex items-center justify-between mt-0.5">
@@ -2137,7 +2139,7 @@ function MapView({
)} )}
</div> </div>
{!inc.location_coords && !onSelectIncident && ( {!inc.location_coords && !onSelectIncident && (
<p className="text-[10px] text-accent mt-1">View details →</p> <p className="text-[11px] text-accent mt-1">View details →</p>
)} )}
</> </>
); );
@@ -2153,7 +2155,7 @@ function MapView({
onClick={() => handleIncidentSelect(inc)} onClick={() => handleIncidentSelect(inc)}
aria-pressed={selected} aria-pressed={selected}
className={`${baseClass} ${latest ? "pr-10" : ""}`} className={`${baseClass} ${latest ? "pr-10" : ""}`}
style={{ borderColor: color + "55" }} style={{ borderColor: `color-mix(in srgb, ${color} 60%, transparent)` }}
> >
{cardBody} {cardBody}
</button> </button>
@@ -2166,7 +2168,7 @@ function MapView({
<a <a
href={`/incidents/${inc.incident_id}`} href={`/incidents/${inc.incident_id}`}
className={`block ${baseClass} ${latest ? "pr-10" : ""}`} className={`block ${baseClass} ${latest ? "pr-10" : ""}`}
style={{ borderColor: color + "55" }} style={{ borderColor: `color-mix(in srgb, ${color} 60%, transparent)` }}
> >
{cardBody} {cardBody}
</a> </a>
@@ -2181,13 +2183,13 @@ function MapView({
<div className="absolute bottom-0 left-0 right-0 z-[1001] md:hidden"> <div className="absolute bottom-0 left-0 right-0 z-[1001] md:hidden">
<button <button
onClick={() => setDrawerOpen((v: boolean) => !v)} onClick={() => setDrawerOpen((v: boolean) => !v)}
className="w-full bg-surface/95 border-t border-line px-4 py-2 text-xs text-ink-2 flex items-center justify-between" className="w-full bg-surface border-t border-line-strong px-4 py-2 text-xs text-ink-2 flex items-center justify-between"
> >
<span>Incidents ({filteredFrom != null ? `${incidents.length} of ${filteredFrom} · filtered` : incidents.length})</span> <span>Incidents ({filteredFrom != null ? `${incidents.length} of ${filteredFrom} · filtered` : incidents.length})</span>
<span>{drawerOpen ? "▼" : "▲"}</span> <span>{drawerOpen ? "▼" : "▲"}</span>
</button> </button>
{drawerOpen && ( {drawerOpen && (
<div className="bg-surface/95 border-t border-line max-h-52 overflow-y-auto px-3 py-2 space-y-1.5"> <div className="bg-surface border-t border-line max-h-52 overflow-y-auto px-3 py-2 space-y-1.5">
{/* Gate A / A2 (server-26#46) */} {/* Gate A / A2 (server-26#46) */}
<MachineOutputNotice variant="inline" className="text-[10px] items-start" /> <MachineOutputNotice variant="inline" className="text-[10px] items-start" />
{railIncidents.map((inc) => { {railIncidents.map((inc) => {
@@ -2210,7 +2212,7 @@ function MapView({
handleIncidentSelect(inc); handleIncidentSelect(inc);
}} }}
className="w-full text-left border rounded px-2 py-1.5 text-xs" className="w-full text-left border rounded px-2 py-1.5 text-xs"
style={{ borderColor: color + "55" }} style={{ borderColor: `color-mix(in srgb, ${color} 60%, transparent)` }}
> >
{label} {label}
</button> </button>
@@ -2221,7 +2223,7 @@ function MapView({
key={inc.incident_id} key={inc.incident_id}
href={`/incidents/${inc.incident_id}`} href={`/incidents/${inc.incident_id}`}
className="block w-full text-left border rounded px-2 py-1.5 text-xs" className="block w-full text-left border rounded px-2 py-1.5 text-xs"
style={{ borderColor: color + "55" }} style={{ borderColor: `color-mix(in srgb, ${color} 60%, transparent)` }}
> >
{label} {label}
</a> </a>