diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index cd5d13e7..0e890c02 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -2394,6 +2394,164 @@ filter: drop-shadow(0 0 8px rgba(34, 211, 238, 0.22)); } +/* ── Light Theme Dashboard Overrides ── */ +:global(html.light) .root { + background: + radial-gradient(circle at 22% 12%, rgba(14, 165, 233, 0.16), transparent 28%), + radial-gradient(circle at 78% 0%, rgba(59, 130, 246, 0.12), transparent 24%), + linear-gradient(180deg, #f8fbff 0%, #e7f1fb 100%); +} + +:global(html.light) .root :global(.map) { + background: #dfeefa; + border-color: rgba(36, 68, 104, 0.18); + filter: none; + box-shadow: + 0 28px 80px rgba(31, 64, 104, 0.18), + inset 0 1px 0 rgba(255, 255, 255, 0.86); +} + +:global(html.light) .root :global(.map .leaflet-tile) { + filter: saturate(1.08) contrast(0.98) brightness(1.02); +} + +:global(html.light) .weatherAura { + opacity: 0.22; + mix-blend-mode: multiply; +} + +:global(html.light) .weatherAuraScrim { + background: + linear-gradient( + 180deg, + rgba(248, 252, 255, 0.2) 0%, + rgba(248, 252, 255, 0.08) 42%, + rgba(226, 239, 250, 0.28) 100% + ), + radial-gradient( + circle at 50% 58%, + rgba(14, 165, 233, 0.08) 0%, + rgba(14, 165, 233, 0) 52% + ); +} + +:global(html.light) .root :global(.home-map-title strong), +:global(html.light) .root :global(.home-panel-city h2), +:global(html.light) .root :global(.home-weather-main strong), +:global(html.light) .root :global(.home-weather-stat strong), +:global(html.light) .root :global(.home-deb-card strong), +:global(html.light) .root :global(.home-card-section h3), +:global(html.light) .root :global(.home-summary-card-head strong), +:global(html.light) .root :global(.opportunity-strip-heading strong), +:global(html.light) .root :global(.opportunity-city), +:global(html.light) .root :global(.home-summary-stat b), +:global(html.light) .root :global(.city-item .city-name-text) { + color: #0b1726; +} + +:global(html.light) .root :global(.home-map-title span), +:global(html.light) .root :global(.home-panel-subtitle), +:global(html.light) .root :global(.home-weather-sub), +:global(html.light) .root :global(.home-weather-label), +:global(html.light) .root :global(.home-weather-stat span), +:global(html.light) .root :global(.home-weather-stat small), +:global(html.light) .root :global(.home-deb-card small), +:global(html.light) .root :global(.home-card-section h3 small), +:global(html.light) .root :global(.home-summary-stat span), +:global(html.light) .root :global(.opportunity-strip-heading span), +:global(html.light) .root :global(.opportunity-meta), +:global(html.light) .root :global(.city-group-title), +:global(html.light) .root :global(.home-panel-kicker), +:global(html.light) .root :global(.home-panel-airport) { + color: rgba(66, 84, 108, 0.86); +} + +:global(html.light) .root :global(.header) { + background: rgba(248, 252, 255, 0.9); + border-bottom-color: rgba(36, 68, 104, 0.14); + box-shadow: 0 12px 34px rgba(31, 64, 104, 0.08); +} + +:global(html.light) .root :global(.city-list), +:global(html.light) .root :global(.home-intelligence-panel), +:global(html.light) .root :global(.home-opportunity-strip) { + background: + radial-gradient(circle at 12% 0%, rgba(14, 165, 233, 0.08), transparent 34%), + rgba(248, 252, 255, 0.9); + border-color: rgba(36, 68, 104, 0.16); + box-shadow: 0 28px 70px rgba(31, 64, 104, 0.14); + color: #0b1726; +} + +:global(html.light) .root :global(.home-summary-card), +:global(html.light) .root :global(.opportunity-card), +:global(html.light) .root :global(.home-deb-card), +:global(html.light) .root :global(.home-card-section), +:global(html.light) .root :global(.home-metric-card), +:global(html.light) .root :global(.home-forecast-item), +:global(html.light) .root :global(.home-intraday-reports span) { + background: + radial-gradient(circle at 100% 0%, rgba(14, 165, 233, 0.08), transparent 36%), + rgba(255, 255, 255, 0.92); + border-color: rgba(36, 68, 104, 0.14); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82); + color: #0b1726; +} + +:global(html.light) .root :global(.home-weather-hero) { + background: + linear-gradient(135deg, rgba(238, 246, 255, 0.98), rgba(216, 233, 248, 0.92)); + border-color: rgba(36, 68, 104, 0.14); +} + +:global(html.light) .root :global(.home-card-section.intraday), +:global(html.light) .root :global(.home-card-section.probability) { + background: + radial-gradient(circle at 0% 25%, rgba(14, 165, 233, 0.1), transparent 42%), + rgba(255, 255, 255, 0.92); +} + +:global(html.light) .root :global(.home-intraday-chart line) { + stroke: rgba(66, 84, 108, 0.16); +} + +:global(html.light) .root :global(.home-intraday-reports strong), +:global(html.light) .root :global(.home-probability-threshold), +:global(html.light) .root :global(.home-market-question strong), +:global(html.light) .root :global(.home-market-metrics strong), +:global(html.light) .root :global(.home-forecast-item strong) { + color: #0b1726; +} + +:global(html.light) .root :global(.home-probability-row div), +:global(html.light) .root :global(.home-probability-track) { + background: rgba(37, 57, 82, 0.12); +} + +:global(html.light) .root :global(.city-group) { + background: rgba(255, 255, 255, 0.54); + border-color: rgba(36, 68, 104, 0.12); +} + +:global(html.light) .root :global(.city-group-header:hover), +:global(html.light) .root :global(.city-item:hover) { + background: rgba(221, 235, 250, 0.86); +} + +:global(html.light) .root :global(.city-item.active) { + background: + linear-gradient(90deg, rgba(14, 165, 233, 0.18), rgba(14, 165, 233, 0.06)), + rgba(255, 255, 255, 0.88); + border-color: rgba(14, 165, 233, 0.42); +} + +:global(html.light) .root :global(.leaflet-control-attribution), +:global(html.light) .root :global(.leaflet-control-zoom a) { + background: rgba(248, 252, 255, 0.86) !important; + border-color: rgba(36, 68, 104, 0.16) !important; + color: #0b1726 !important; +} + @keyframes home-panel-enter { from { opacity: 0; diff --git a/frontend/hooks/useLeafletMap.ts b/frontend/hooks/useLeafletMap.ts index dca18654..d76ec555 100644 --- a/frontend/hooks/useLeafletMap.ts +++ b/frontend/hooks/useLeafletMap.ts @@ -34,6 +34,10 @@ const AUTO_NEARBY_MAX_DISTANCE_M = 120000; const AUTO_NEARBY_IDLE_REFRESH_DELAY_MS = 10_000; const AUTO_NEARBY_MIN_REFRESH_INTERVAL_MS = 60_000; const MAP_MAX_ZOOM = 19; +const MAP_TILE_URLS = { + dark: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", + light: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", +} as const; const CITY_MARKER_DISPLAY_OFFSETS: Record< string, { x: number; y: number; zIndexOffset?: number } @@ -52,6 +56,16 @@ function getMarkerDisplayOffset(cityName: string) { }; } +function getMapTileUrl() { + if ( + typeof document !== "undefined" && + document.documentElement.classList.contains("light") + ) { + return MAP_TILE_URLS.light; + } + return MAP_TILE_URLS.dark; +} + function pickMarkerTemperature( snapshot?: Pick | CitySummary, ) { @@ -382,6 +396,7 @@ export function useLeafletMap({ }: UseLeafletMapArgs) { const containerRef = useRef(null); const mapRef = useRef(null); + const tileLayerRef = useRef(null); const markersRef = useRef< Record >({}); @@ -444,18 +459,16 @@ export function useLeafletMap({ }); L.control.zoom({ position: "bottomright" }).addTo(map); - L.tileLayer( - "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", - { - attribution: - '© OSM © CARTO', - maxZoom: 19, - subdomains: "abcd", - }, - ).addTo(map); + const tileLayer = L.tileLayer(getMapTileUrl(), { + attribution: + '© OSM © CARTO', + maxZoom: 19, + subdomains: "abcd", + }).addTo(map); const nearbyLayer = L.layerGroup().addTo(map); mapRef.current = map; + tileLayerRef.current = tileLayer; nearbyLayerRef.current = nearbyLayer; // Track which city we've already moved to for the current selection @@ -515,11 +528,30 @@ export function useLeafletMap({ map.off("click", handleMapClick); map.remove(); mapRef.current = null; + tileLayerRef.current = null; nearbyLayerRef.current = null; markersRef.current = {}; }; }, []); + useEffect(() => { + const tileLayer = tileLayerRef.current; + if (!tileLayer || typeof MutationObserver === "undefined") return; + + const syncMapTheme = () => { + tileLayer.setUrl(getMapTileUrl()); + }; + syncMapTheme(); + + const observer = new MutationObserver(syncMapTheme); + observer.observe(document.documentElement, { + attributeFilter: ["class"], + attributes: true, + }); + + return () => observer.disconnect(); + }, []); + // Handle initial view if cities are loaded useEffect(() => { const map = mapRef.current;