diff --git a/src/components/MapView.jsx b/src/components/MapView.jsx
index a49143e..5b8eb3c 100644
--- a/src/components/MapView.jsx
+++ b/src/components/MapView.jsx
@@ -1247,10 +1247,10 @@ function removeBlmTrails(map) {
}
-/** Add boundary polygon layers with computed accent color (MapLibre rejects CSS vars in paint) */
+/** Add boundary polygon layers using theme-aware highlight config */
const BOUNDARY_FILL_LAYER = 'boundary-fill-layer'
-function addBoundaryLayer(map) {
+function addBoundaryLayer(map, themeId) {
if (!map || map.getLayer(BOUNDARY_LAYER)) return
if (!map.getSource(BOUNDARY_SOURCE)) {
map.addSource(BOUNDARY_SOURCE, {
@@ -1258,7 +1258,14 @@ function addBoundaryLayer(map) {
data: { type: "FeatureCollection", features: [] },
})
}
- const accentColor = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() || "#7a9a6b"
+ // Get highlight config from theme overlay
+ const highlight = getOverlayConfig(themeId, "highlight") || {}
+ const lineColor = highlight.lineColor || "#7a9a6b"
+ const lineWidth = highlight.lineWidth || 2
+ const lineDash = highlight.lineDash || [4, 4]
+ const lineOpacity = highlight.lineOpacity || 0.8
+ const fillColor = highlight.fillColor || lineColor
+ const fillOpacity = highlight.fillOpacity || 0.08
// Find first symbol layer to insert boundary layers below labels
const layers = map.getStyle().layers
@@ -1276,8 +1283,8 @@ function addBoundaryLayer(map) {
type: "fill",
source: BOUNDARY_SOURCE,
paint: {
- "fill-color": accentColor,
- "fill-opacity": 0.05,
+ "fill-color": fillColor,
+ "fill-opacity": fillOpacity,
},
}, firstSymbolId)
@@ -1287,10 +1294,10 @@ function addBoundaryLayer(map) {
type: "line",
source: BOUNDARY_SOURCE,
paint: {
- "line-color": accentColor,
- "line-width": 2,
- "line-opacity": 0.7,
- "line-dasharray": [3, 2],
+ "line-color": lineColor,
+ "line-width": lineWidth,
+ "line-opacity": lineOpacity,
+ "line-dasharray": lineDash,
},
}, firstSymbolId)
}
@@ -1498,7 +1505,14 @@ const MapView = forwardRef(function MapView(_, ref) {
type: "geojson",
data: { type: "FeatureCollection", features: [] },
})
- const accentColor = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() || "#7a9a6b"
+ // Get highlight config from theme overlay
+ const highlight = getOverlayConfig(themeId, "highlight") || {}
+ const lineColor = highlight.lineColor || "#7a9a6b"
+ const lineWidth = highlight.lineWidth || 2
+ const lineDash = highlight.lineDash || [4, 4]
+ const lineOpacity = highlight.lineOpacity || 0.8
+ const fillColor = highlight.fillColor || lineColor
+ const fillOpacity = highlight.fillOpacity || 0.08
map.addLayer({
id: MEASURE_LINE_LAYER,
type: "line",
@@ -2124,7 +2138,7 @@ const MapView = forwardRef(function MapView(_, ref) {
// Boundary polygon layer for selected places
if (!map.getLayer(BOUNDARY_LAYER)) {
- addBoundaryLayer(map)
+ addBoundaryLayer(map, currentThemeRef.current)
}
// Apply improved base label styling for readability
@@ -2333,7 +2347,7 @@ const MapView = forwardRef(function MapView(_, ref) {
// Boundary polygon layer
if (!map.getLayer(BOUNDARY_LAYER)) {
- addBoundaryLayer(map)
+ addBoundaryLayer(map, currentThemeRef.current)
}
// Apply improved base label styling for readability
diff --git a/src/themes/clean.js b/src/themes/clean.js
index b13e52d..2ed3b87 100644
--- a/src/themes/clean.js
+++ b/src/themes/clean.js
@@ -1,369 +1,379 @@
-/**
- * Clean Theme for Navi
- *
- * A plain, familiar, Google Maps-inspired style focused on maximum usability.
- * Clean, neutral, utilitarian. White/light gray land, soft pastel green parks,
- * gentle blue water, classic gray→yellow→orange road hierarchy. No strong
- * personality — everything serves readability and wayfinding.
- *
- * The theme equivalent of a rental car: nothing exciting, nothing wrong.
- */
-
-// ═══════════════════════════════════════════════════════════════════════════
-// PALETTE
-// ═══════════════════════════════════════════════════════════════════════════
-//
-// base: #f5f5f5 ← land, app background
-// surface: #ffffff ← panels, cards, modals
-// surfaceAlt: #f8f9fa ← secondary panels, hover states
-// border: #dadce0 ← Google's standard border gray
-// text: #202124 ← primary text (Google dark)
-// textSecondary: #5f6368 ← secondary text
-// textMuted: #9aa0a6 ← placeholders, hints
-// accent: #1a73e8 ← Google blue — links, active states
-// accentHover: #1557b0 ← darker blue hover
-// success: #34a853 ← Google green
-// warning: #fbbc04 ← Google yellow
-// danger: #ea4335 ← Google red
-// water: #aadaff ← soft sky blue (Google's water)
-// waterDark: #73b3e8 ← water labels
-// vegetation: #c3ecb2 ← pastel green parks
-// forest: #a8dda0 ← slightly deeper green
-// road: #ffffff ← minor roads — white
-// roadPrimary: #fbc02d ← yellow
-// roadMotorway: #f9a825 ← deeper yellow-orange
-// roadCasing: #e0e0e0 ← light gray casing
-// building: #e8e4de ← warm light gray
-// contour: #c8b8a0 ← subtle warm brown
-//
-// ═══════════════════════════════════════════════════════════════════════════
-
-/**
- * Map flavor colors - protomaps-themes-base schema
- * All 73 flat keys + pois + landcover nested objects
- */
-const cleanColors = {
- // Background & earth
- background: '#e8e8e8',
- earth: '#f5f5f5',
-
- // Land use areas
- park_a: '#d4ecd0',
- park_b: '#c3ecb2',
- hospital: '#fde8e8',
- industrial: '#ebeff1',
- school: '#fff3e0',
- wood_a: '#d8ecd4',
- wood_b: '#a8dda0',
- pedestrian: '#f0f0f0',
- scrub_a: '#dcecd8',
- scrub_b: '#c8e4c0',
- glacier: '#f8fcff',
- sand: '#f5f0e0',
- beach: '#fef8e0',
- aerodrome: '#eaecef',
- runway: '#d0d0d0',
- water: '#aadaff',
- zoo: '#d8e8d8',
- military: '#e8e8e8',
-
- // Tunnels
- tunnel_other_casing: '#d8d8d8',
- tunnel_minor_casing: '#d8d8d8',
- tunnel_link_casing: '#d8d8d8',
- tunnel_major_casing: '#d8d8d8',
- tunnel_highway_casing: '#d8d8d8',
- tunnel_other: '#e8e8e8',
- tunnel_minor: '#e8e8e8',
- tunnel_link: '#f0e0a0',
- tunnel_major: '#f0e0a0',
- tunnel_highway: '#f0d080',
-
- // Pier & buildings
- pier: '#e0e0e0',
- buildings: '#e8e4de',
-
- // Roads & casings
- minor_service_casing: '#e0e0e0',
- minor_casing: '#e0e0e0',
- link_casing: '#d8c080',
- major_casing_late: '#d8c080',
- highway_casing_late: '#d8a860',
- other: '#f0f0f0',
- minor_service: '#ffffff',
- minor_a: '#ffffff',
- minor_b: '#ffffff',
- link: '#fbc02d',
- major_casing_early: '#d8c080',
- major: '#fbc02d',
- highway_casing_early: '#d8a860',
- highway: '#f9a825',
- railway: '#a0a0a0',
- boundaries: '#c0c0c0',
-
- // Waterway label
- waterway_label: '#73b3e8',
-
- // Bridges
- bridges_other_casing: '#d0d0d0',
- bridges_minor_casing: '#d0d0d0',
- bridges_link_casing: '#d8c080',
- bridges_major_casing: '#d8c080',
- bridges_highway_casing: '#d8a860',
- bridges_other: '#f0f0f0',
- bridges_minor: '#ffffff',
- bridges_link: '#fbc02d',
- bridges_major: '#fbc02d',
- bridges_highway: '#f9a825',
-
- // Labels
- roads_label_minor: '#5f6368',
- roads_label_minor_halo: '#ffffff',
- roads_label_major: '#5f6368',
- roads_label_major_halo: '#ffffff',
- ocean_label: '#73b3e8',
- peak_label: '#5f6368',
- subplace_label: '#5f6368',
- subplace_label_halo: '#ffffff',
- city_label: '#202124',
- city_label_halo: '#ffffff',
- state_label: '#9aa0a6',
- state_label_halo: '#ffffff',
- country_label: '#5f6368',
- address_label: '#5f6368',
- address_label_halo: '#ffffff',
-
- // POI icon colors
- pois: {
- blue: '#1a73e8',
- green: '#34a853',
- lapis: '#4285f4',
- pink: '#e91e63',
- red: '#ea4335',
- slategray: '#5f6368',
- tangerine: '#f9a825',
- turquoise: '#00bcd4',
- },
-
- // Landcover fill colors
- landcover: {
- grassland: 'rgba(200, 232, 192, 1)',
- barren: 'rgba(240, 235, 220, 1)',
- urban_area: 'rgba(235, 235, 235, 1)',
- farmland: 'rgba(216, 240, 210, 1)',
- glacier: 'rgba(250, 252, 255, 1)',
- scrub: 'rgba(220, 236, 216, 1)',
- forest: 'rgba(180, 224, 176, 1)',
- },
-}
-
-/**
- * UI CSS custom properties - app chrome styling
- * Clean Google-inspired white panels with standard gray text
- */
-const cleanUI = {
- // Fonts
- '--font-sans': "'Inter', system-ui, -apple-system, sans-serif",
- '--font-mono': "'JetBrains Mono', ui-monospace, monospace",
+/**
+ * Clean Theme for Navi
+ *
+ * A plain, familiar, Google Maps-inspired style focused on maximum usability.
+ * Clean, neutral, utilitarian. White/light gray land, soft pastel green parks,
+ * gentle blue water, classic gray→yellow→orange road hierarchy. No strong
+ * personality — everything serves readability and wayfinding.
+ *
+ * The theme equivalent of a rental car: nothing exciting, nothing wrong.
+ */
+
+// ═══════════════════════════════════════════════════════════════════════════
+// PALETTE
+// ═══════════════════════════════════════════════════════════════════════════
+//
+// base: #f5f5f5 ← land, app background
+// surface: #ffffff ← panels, cards, modals
+// surfaceAlt: #f8f9fa ← secondary panels, hover states
+// border: #dadce0 ← Google's standard border gray
+// text: #202124 ← primary text (Google dark)
+// textSecondary: #5f6368 ← secondary text
+// textMuted: #9aa0a6 ← placeholders, hints
+// accent: #1a73e8 ← Google blue — links, active states
+// accentHover: #1557b0 ← darker blue hover
+// success: #34a853 ← Google green
+// warning: #fbbc04 ← Google yellow
+// danger: #ea4335 ← Google red
+// water: #aadaff ← soft sky blue (Google's water)
+// waterDark: #73b3e8 ← water labels
+// vegetation: #c3ecb2 ← pastel green parks
+// forest: #a8dda0 ← slightly deeper green
+// road: #ffffff ← minor roads — white
+// roadPrimary: #fbc02d ← yellow
+// roadMotorway: #f9a825 ← deeper yellow-orange
+// roadCasing: #e0e0e0 ← light gray casing
+// building: #e8e4de ← warm light gray
+// contour: #c8b8a0 ← subtle warm brown
+//
+// ═══════════════════════════════════════════════════════════════════════════
+
+/**
+ * Map flavor colors - protomaps-themes-base schema
+ * All 73 flat keys + pois + landcover nested objects
+ */
+const cleanColors = {
+ // Background & earth
+ background: '#e8e8e8',
+ earth: '#f5f5f5',
+
+ // Land use areas
+ park_a: '#d4ecd0',
+ park_b: '#c3ecb2',
+ hospital: '#fde8e8',
+ industrial: '#ebeff1',
+ school: '#fff3e0',
+ wood_a: '#d8ecd4',
+ wood_b: '#a8dda0',
+ pedestrian: '#f0f0f0',
+ scrub_a: '#dcecd8',
+ scrub_b: '#c8e4c0',
+ glacier: '#f8fcff',
+ sand: '#f5f0e0',
+ beach: '#fef8e0',
+ aerodrome: '#eaecef',
+ runway: '#d0d0d0',
+ water: '#aadaff',
+ zoo: '#d8e8d8',
+ military: '#e8e8e8',
+
+ // Tunnels
+ tunnel_other_casing: '#d8d8d8',
+ tunnel_minor_casing: '#d8d8d8',
+ tunnel_link_casing: '#d8d8d8',
+ tunnel_major_casing: '#d8d8d8',
+ tunnel_highway_casing: '#d8d8d8',
+ tunnel_other: '#e8e8e8',
+ tunnel_minor: '#e8e8e8',
+ tunnel_link: '#f0e0a0',
+ tunnel_major: '#f0e0a0',
+ tunnel_highway: '#f0d080',
+
+ // Pier & buildings
+ pier: '#e0e0e0',
+ buildings: '#e8e4de',
+
+ // Roads & casings
+ minor_service_casing: '#e0e0e0',
+ minor_casing: '#e0e0e0',
+ link_casing: '#d8c080',
+ major_casing_late: '#d8c080',
+ highway_casing_late: '#d8a860',
+ other: '#f0f0f0',
+ minor_service: '#ffffff',
+ minor_a: '#ffffff',
+ minor_b: '#ffffff',
+ link: '#fbc02d',
+ major_casing_early: '#d8c080',
+ major: '#fbc02d',
+ highway_casing_early: '#d8a860',
+ highway: '#f9a825',
+ railway: '#a0a0a0',
+ boundaries: '#c0c0c0',
+
+ // Waterway label
+ waterway_label: '#73b3e8',
+
+ // Bridges
+ bridges_other_casing: '#d0d0d0',
+ bridges_minor_casing: '#d0d0d0',
+ bridges_link_casing: '#d8c080',
+ bridges_major_casing: '#d8c080',
+ bridges_highway_casing: '#d8a860',
+ bridges_other: '#f0f0f0',
+ bridges_minor: '#ffffff',
+ bridges_link: '#fbc02d',
+ bridges_major: '#fbc02d',
+ bridges_highway: '#f9a825',
+
+ // Labels
+ roads_label_minor: '#5f6368',
+ roads_label_minor_halo: '#ffffff',
+ roads_label_major: '#5f6368',
+ roads_label_major_halo: '#ffffff',
+ ocean_label: '#73b3e8',
+ peak_label: '#5f6368',
+ subplace_label: '#5f6368',
+ subplace_label_halo: '#ffffff',
+ city_label: '#202124',
+ city_label_halo: '#ffffff',
+ state_label: '#9aa0a6',
+ state_label_halo: '#ffffff',
+ country_label: '#5f6368',
+ address_label: '#5f6368',
+ address_label_halo: '#ffffff',
+
+ // POI icon colors
+ pois: {
+ blue: '#1a73e8',
+ green: '#34a853',
+ lapis: '#4285f4',
+ pink: '#e91e63',
+ red: '#ea4335',
+ slategray: '#5f6368',
+ tangerine: '#f9a825',
+ turquoise: '#00bcd4',
+ },
+
+ // Landcover fill colors
+ landcover: {
+ grassland: 'rgba(200, 232, 192, 1)',
+ barren: 'rgba(240, 235, 220, 1)',
+ urban_area: 'rgba(235, 235, 235, 1)',
+ farmland: 'rgba(216, 240, 210, 1)',
+ glacier: 'rgba(250, 252, 255, 1)',
+ scrub: 'rgba(220, 236, 216, 1)',
+ forest: 'rgba(180, 224, 176, 1)',
+ },
+}
+
+/**
+ * UI CSS custom properties - app chrome styling
+ * Clean Google-inspired white panels with standard gray text
+ */
+const cleanUI = {
+ // Fonts
+ '--font-sans': "'Inter', system-ui, -apple-system, sans-serif",
+ '--font-mono': "'JetBrains Mono', ui-monospace, monospace",
'--font-heading': "'Inter', system-ui, -apple-system, sans-serif",
- // Backgrounds
- '--bg-base': '#f5f5f5',
- '--bg-raised': '#ffffff',
- '--bg-overlay': '#ffffff',
- '--bg-input': '#ffffff',
- '--bg-inset': '#f0f0f0',
- '--bg-muted': '#f8f9fa',
- // Text
- '--text-primary': '#202124',
- '--text-secondary': '#5f6368',
- '--text-tertiary': '#9aa0a6',
- '--text-inverse': '#ffffff',
- // Borders
- '--border': '#dadce0',
- '--border-subtle': '#e8eaed',
- // Accent
- '--accent': '#1a73e8',
- '--accent-hover': '#1557b0',
- '--accent-muted': '#e8f0fe',
- // Tan
- '--tan': '#f9a825',
- '--tan-muted': '#fef7e0',
- // Pins
- '--pin-origin': '#34a853',
- '--pin-destination': '#ea4335',
- '--pin-intermediate': '#5f6368',
- '--pin-stroke': '#ffffff',
- // Status
- '--status-success': '#34a853',
- '--status-warning': '#fbbc04',
- '--status-danger': '#ea4335',
- '--success': '#34a853',
- '--warning': '#fbbc04',
- '--warning-muted': '#fef7e0',
- // Route
- '--route-line': '#1a73e8',
- // Shadows
- '--shadow': '0 1px 3px rgba(60, 64, 67, 0.15), 0 1px 2px rgba(60, 64, 67, 0.1)',
- '--shadow-lg': '0 2px 6px rgba(60, 64, 67, 0.2), 0 1px 3px rgba(60, 64, 67, 0.15)',
-}
-
-/**
- * Overlay configuration overrides
- * Light shadow hillshade, warm brown contours, standard public lands
- */
-const cleanOverlay = {
- // Hillshade - light and natural
- hillshade: {
- exaggeration: 0.4,
- illuminationDirection: 315,
- shadowColor: '#000000',
- highlightColor: '#ffffff',
- },
-
- // Contours - warm brown, subtle
- contours: {
- opacityMod: 0.9,
- minorColor: '#c8b8a0',
- minorOpacity: 0.35,
- minorWidth: { z11: 0.5, z14: 0.8 },
- intermediateColor: '#c8b8a0',
- intermediateOpacity: 0.55,
- intermediateWidth: { z8: 0.7, z14: 1.0 },
- indexColor: '#a89878',
- indexOpacity: 0.75,
- indexWidth: { z4: 1.0, z14: 1.5 },
- labelColor: '#8a7a60',
- labelHaloColor: '#ffffff',
- labelHaloWidth: 1.5,
- labelOpacity: 0.8,
- labelSize: 10,
- labelFont: ['Noto Sans Regular'],
- },
-
- // Contours Test - blue variant
- contoursTest: {
- minorColor: '#5a9ab8',
- intermediateColor: '#5a9ab8',
- indexColor: '#3a7a98',
- labelColor: '#3a6a88',
- },
-
- // Contours Test 10ft - green variant
- contoursTest10ft: {
- minorColor: '#4a9a5f',
- intermediateColor: '#4a9a5f',
- indexColor: '#2a7a4a',
- labelColor: '#2a5a40',
- },
-
- // Public Lands - standard green tints with dark labels
- publicLands: {
- opacityMod: 0.9,
- // Fill colors per category
- fillWA: '#8a7a40',
- fillNPS: '#4a8030',
- fillUSFS: '#6a9040',
- fillBLM: '#d4b880',
- fillFWS: '#5a9068',
- fillSTAT: '#6aa088',
- fillLOC: '#9ab8a8',
- fillDefault: '#b0b0b0',
- // Fill opacities
- fillOpacityWA: 0.25,
- fillOpacityNPS: 0.25,
- fillOpacityUSFS: 0.20,
- fillOpacityBLM: 0.18,
- fillOpacitySTAT: 0.22,
- fillOpacityLOC: 0.18,
- fillOpacityDefault: 0.12,
- // Outline colors
- outlineWA: '#6a5a28',
- outlineNPS: '#2a5018',
- outlineUSFS: '#4a6828',
- outlineBLM: '#9a8050',
- outlineFWS: '#3a6848',
- outlineSTAT: '#4a7060',
- outlineLOC: '#6a8070',
- outlineDefault: '#808080',
- // Outline opacities
- outlineOpacityNPS: 0.65,
- outlineOpacityUSFS: 0.55,
- outlineOpacityDefault: 0.45,
- // Outline width
- outlineWidth: { z4: 0.3, z8: 0.8, z12: 1.2 },
- // Labels - dark for readability
- labelColor: '#2a3a28',
- labelHaloColor: '#ffffff',
- labelHaloWidth: 1.5,
- labelOpacity: 0.85,
- labelSize: { z10: 10, z14: 13 },
- labelFont: ['Noto Sans Regular'],
- },
-
- // USFS Trails - standard trail colors
- usfsTrails: {
- roadsColor: '#c09050',
- roadsOpacity: 0.85,
- roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 },
- trailsMotorized: '#e07030',
- trailsBicycle: '#d0a030',
- trailsHiker: '#50b040',
- trailsDefault: '#b09050',
- trailsOpacity: 0.85,
- trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
- trailsDash: [2, 1.5],
- roadsLabelColor: '#5a4a30',
- roadsLabelHaloColor: '#ffffff',
- roadsLabelHaloWidth: 1.5,
- roadsLabelOpacity: 0.85,
- roadsLabelSize: 11,
- trailsLabelColor: '#4a3a28',
- trailsLabelHaloColor: '#ffffff',
- trailsLabelHaloWidth: 1.5,
- trailsLabelOpacity: 0.85,
- trailsLabelSize: 11,
- labelFont: ['Noto Sans Regular'],
- hitWidth: 14,
- },
-
- // BLM Trails - standard route colors
- blmTrails: {
- color4wdHigh: '#e07030',
- color4wdLow: '#d0a030',
- colorAtv: '#d03030',
- colorMotoSingle: '#a060b0',
- color2wdLow: '#e0c060',
- colorNonMech: '#50b040',
- colorDefault: '#b09050',
- colorSnow: '#6090c0',
- lineOpacity: 0.85,
- lineOpacityOther: 0.80,
- lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
- dashImproved: [4, 2],
- dashAggregate: [1, 2],
- dashSnow: [4, 2, 1, 2],
- dashOther: [4, 2, 1, 2, 1, 2],
- labelColor: '#4a3a28',
- labelHaloColor: '#ffffff',
- labelHaloWidth: 1.5,
- labelOpacity: 0.85,
- labelSize: 11,
- labelFont: ['Noto Sans Regular'],
- hitWidth: 14,
- },
-}
-
-/**
- * Clean theme configuration
- */
-const cleanTheme = {
- id: 'clean',
- name: 'Clean',
- dark: false,
- colors: cleanColors,
- satellite: null, // No adjustments — default clear view
- overlay: cleanOverlay,
- ui: cleanUI,
-}
-
-export default cleanTheme
+ // Backgrounds
+ '--bg-base': '#f5f5f5',
+ '--bg-raised': '#ffffff',
+ '--bg-overlay': '#ffffff',
+ '--bg-input': '#ffffff',
+ '--bg-inset': '#f0f0f0',
+ '--bg-muted': '#f8f9fa',
+ // Text
+ '--text-primary': '#202124',
+ '--text-secondary': '#5f6368',
+ '--text-tertiary': '#9aa0a6',
+ '--text-inverse': '#ffffff',
+ // Borders
+ '--border': '#dadce0',
+ '--border-subtle': '#e8eaed',
+ // Accent
+ '--accent': '#1a73e8',
+ '--accent-hover': '#1557b0',
+ '--accent-muted': '#e8f0fe',
+ // Tan
+ '--tan': '#f9a825',
+ '--tan-muted': '#fef7e0',
+ // Pins
+ '--pin-origin': '#34a853',
+ '--pin-destination': '#ea4335',
+ '--pin-intermediate': '#5f6368',
+ '--pin-stroke': '#ffffff',
+ // Status
+ '--status-success': '#34a853',
+ '--status-warning': '#fbbc04',
+ '--status-danger': '#ea4335',
+ '--success': '#34a853',
+ '--warning': '#fbbc04',
+ '--warning-muted': '#fef7e0',
+ // Route
+ '--route-line': '#1a73e8',
+ // Shadows
+ '--shadow': '0 1px 3px rgba(60, 64, 67, 0.15), 0 1px 2px rgba(60, 64, 67, 0.1)',
+ '--shadow-lg': '0 2px 6px rgba(60, 64, 67, 0.2), 0 1px 3px rgba(60, 64, 67, 0.15)',
+}
+
+/**
+ * Overlay configuration overrides
+ * Light shadow hillshade, warm brown contours, standard public lands
+ */
+const cleanOverlay = {
+ // Hillshade - light and natural
+ hillshade: {
+ exaggeration: 0.4,
+ illuminationDirection: 315,
+ shadowColor: '#000000',
+ highlightColor: '#ffffff',
+ },
+
+ // Contours - warm brown, subtle
+ contours: {
+ opacityMod: 0.9,
+ minorColor: '#c8b8a0',
+ minorOpacity: 0.35,
+ minorWidth: { z11: 0.5, z14: 0.8 },
+ intermediateColor: '#c8b8a0',
+ intermediateOpacity: 0.55,
+ intermediateWidth: { z8: 0.7, z14: 1.0 },
+ indexColor: '#a89878',
+ indexOpacity: 0.75,
+ indexWidth: { z4: 1.0, z14: 1.5 },
+ labelColor: '#8a7a60',
+ labelHaloColor: '#ffffff',
+ labelHaloWidth: 1.5,
+ labelOpacity: 0.8,
+ labelSize: 10,
+ labelFont: ['Noto Sans Regular'],
+ },
+
+ // Contours Test - blue variant
+ contoursTest: {
+ minorColor: '#5a9ab8',
+ intermediateColor: '#5a9ab8',
+ indexColor: '#3a7a98',
+ labelColor: '#3a6a88',
+ },
+
+ // Contours Test 10ft - green variant
+ contoursTest10ft: {
+ minorColor: '#4a9a5f',
+ intermediateColor: '#4a9a5f',
+ indexColor: '#2a7a4a',
+ labelColor: '#2a5a40',
+ },
+
+ // Public Lands - standard green tints with dark labels
+ publicLands: {
+ opacityMod: 0.9,
+ // Fill colors per category
+ fillWA: '#8a7a40',
+ fillNPS: '#4a8030',
+ fillUSFS: '#6a9040',
+ fillBLM: '#d4b880',
+ fillFWS: '#5a9068',
+ fillSTAT: '#6aa088',
+ fillLOC: '#9ab8a8',
+ fillDefault: '#b0b0b0',
+ // Fill opacities
+ fillOpacityWA: 0.25,
+ fillOpacityNPS: 0.25,
+ fillOpacityUSFS: 0.20,
+ fillOpacityBLM: 0.18,
+ fillOpacitySTAT: 0.22,
+ fillOpacityLOC: 0.18,
+ fillOpacityDefault: 0.12,
+ // Outline colors
+ outlineWA: '#6a5a28',
+ outlineNPS: '#2a5018',
+ outlineUSFS: '#4a6828',
+ outlineBLM: '#9a8050',
+ outlineFWS: '#3a6848',
+ outlineSTAT: '#4a7060',
+ outlineLOC: '#6a8070',
+ outlineDefault: '#808080',
+ // Outline opacities
+ outlineOpacityNPS: 0.65,
+ outlineOpacityUSFS: 0.55,
+ outlineOpacityDefault: 0.45,
+ // Outline width
+ outlineWidth: { z4: 0.3, z8: 0.8, z12: 1.2 },
+ // Labels - dark for readability
+ labelColor: '#2a3a28',
+ labelHaloColor: '#ffffff',
+ labelHaloWidth: 1.5,
+ labelOpacity: 0.85,
+ labelSize: { z10: 10, z14: 13 },
+ labelFont: ['Noto Sans Regular'],
+ },
+
+ // USFS Trails - standard trail colors
+ usfsTrails: {
+ roadsColor: '#c09050',
+ roadsOpacity: 0.85,
+ roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 },
+ trailsMotorized: '#e07030',
+ trailsBicycle: '#d0a030',
+ trailsHiker: '#50b040',
+ trailsDefault: '#b09050',
+ trailsOpacity: 0.85,
+ trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
+ trailsDash: [2, 1.5],
+ roadsLabelColor: '#5a4a30',
+ roadsLabelHaloColor: '#ffffff',
+ roadsLabelHaloWidth: 1.5,
+ roadsLabelOpacity: 0.85,
+ roadsLabelSize: 11,
+ trailsLabelColor: '#4a3a28',
+ trailsLabelHaloColor: '#ffffff',
+ trailsLabelHaloWidth: 1.5,
+ trailsLabelOpacity: 0.85,
+ trailsLabelSize: 11,
+ labelFont: ['Noto Sans Regular'],
+ hitWidth: 14,
+ },
+
+ // BLM Trails - standard route colors
+ blmTrails: {
+ color4wdHigh: '#e07030',
+ color4wdLow: '#d0a030',
+ colorAtv: '#d03030',
+ colorMotoSingle: '#a060b0',
+ color2wdLow: '#e0c060',
+ colorNonMech: '#50b040',
+ colorDefault: '#b09050',
+ colorSnow: '#6090c0',
+ lineOpacity: 0.85,
+ lineOpacityOther: 0.80,
+ lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
+ dashImproved: [4, 2],
+ dashAggregate: [1, 2],
+ dashSnow: [4, 2, 1, 2],
+ dashOther: [4, 2, 1, 2, 1, 2],
+ labelColor: '#4a3a28',
+ labelHaloColor: '#ffffff',
+ labelHaloWidth: 1.5,
+ labelOpacity: 0.85,
+ labelSize: 11,
+ labelFont: ['Noto Sans Regular'],
+ hitWidth: 14,
+ },
+
+ // ── Highlight (boundary/selection) ────────────────────────────────────────
+ highlight: {
+ lineColor: "#1a73e8", // Google blue for selection
+ lineWidth: 2,
+ lineDash: [4, 4],
+ lineOpacity: 0.7,
+ fillColor: "#1a73e8",
+ fillOpacity: 0.06,
+ },
+}
+
+/**
+ * Clean theme configuration
+ */
+const cleanTheme = {
+ id: 'clean',
+ name: 'Clean',
+ dark: false,
+ colors: cleanColors,
+ satellite: null, // No adjustments — default clear view
+ overlay: cleanOverlay,
+ ui: cleanUI,
+}
+
+export default cleanTheme
diff --git a/src/themes/cyberpunk.js b/src/themes/cyberpunk.js
index 86a8e92..d5e09c4 100644
--- a/src/themes/cyberpunk.js
+++ b/src/themes/cyberpunk.js
@@ -1,403 +1,413 @@
-/**
- * Cyberpunk Theme for Navi
- *
- * Inspired by Mapbox's "Terminal" cyberpunk style, Blade Runner, and Ghost in
- * the Shell. A tactical display in a neon-lit command center. Near-black base
- * with deep blue-purple undertones. Roads glow in hot magenta and electric cyan.
- * Water is inky dark. Vegetation is barely there — dark teal hints. Labels are
- * cool white with colored halos.
- *
- * The whole thing should feel like you're navigating Night City.
- *
- * CUSTOM FONTS:
- * - Heading: "Orbitron" — geometric, futuristic display font
- * - Body: "Share Tech Mono" — monospaced terminal feel for entire UI
- */
-
-// ═══════════════════════════════════════════════════════════════════════════
-// PALETTE
-// ═══════════════════════════════════════════════════════════════════════════
-//
-// base: #0a0a14 ← near-black with blue-purple undertone
-// surface: #10101e ← panels, cards
-// surfaceAlt: #161628 ← secondary surfaces, hover states
-// border: #1e1e3a ← subtle purple edges
-// text: #d0d0e8 ← cool white text
-// textSecondary: #8888aa ← lavender-gray
-// textMuted: #5a5a7a ← dark purple-gray
-// textInverse: #0a0a14 ← text on neon backgrounds
-// accent: #ff2d6b ← hot pink/magenta — primary actions
-// accentHover: #ff4d8b ← lighter magenta
-// accentAlt: #00f0ff ← electric cyan — secondary accent
-// success: #00ff88 ← neon green
-// warning: #ffaa00 ← amber
-// danger: #ff3333 ← neon red
-// water: #06061a ← deep dark blue-black
-// waterLabel: #3a6a8a ← muted blue for water labels
-// vegetation: #0a1a12 ← barely-there dark teal-green
-// forest: #0e1e14 ← slightly deeper
-// road: #1a1a3a ← ghost purple minor roads
-// roadSecondary: #2a2a5a
-// roadPrimary: #8833aa ← purple for primary
-// roadMotorway: #ff2d6b ← hot magenta for motorways
-// roadCasing: #0a0a14 ← dark casing
-// building: #141428 ← dark purple-gray buildings
-// contour: #1e1e3e ← dark lines, just visible
-// contourLabel: #5a5a7a
-//
-// ═══════════════════════════════════════════════════════════════════════════
-
-/**
- * Map flavor colors - protomaps-themes-base schema
- * All 73 flat keys + pois + landcover nested objects
- */
-const cyberpunkColors = {
- // Background & earth
- background: '#08080f',
- earth: '#0a0a14',
-
- // Land use areas - dark with slight purple undertones
- park_a: '#0a1a14',
- park_b: '#0e1e18',
- hospital: '#1a1020',
- industrial: '#0e0e1a',
- school: '#14101e',
- wood_a: '#0a1a12',
- wood_b: '#0e1e14',
- pedestrian: '#0c0c18',
- scrub_a: '#0a1410',
- scrub_b: '#0c1812',
- glacier: '#101020',
- sand: '#12101a',
- beach: '#14121c',
- aerodrome: '#0a0a16',
- runway: '#1a1a30',
- water: '#06061a',
- zoo: '#0c1614',
- military: '#100a14',
-
- // Tunnels - dark purple casings
- tunnel_other_casing: '#0a0a14',
- tunnel_minor_casing: '#0a0a14',
- tunnel_link_casing: '#0a0a14',
- tunnel_major_casing: '#0a0a14',
- tunnel_highway_casing: '#0a0a14',
- tunnel_other: '#161628',
- tunnel_minor: '#161628',
- tunnel_link: '#2a2050',
- tunnel_major: '#4a2870',
- tunnel_highway: '#801848',
-
- // Pier & buildings
- pier: '#1a1a30',
- buildings: '#141428',
-
- // Roads & casings - glowing neon progression
- minor_service_casing: '#0a0a14',
- minor_casing: '#0a0a14',
- link_casing: '#0a0a14',
- major_casing_late: '#0a0a14',
- highway_casing_late: '#0a0a14',
- other: '#1a1a3a',
- minor_service: '#1a1a3a',
- minor_a: '#2a2a5a',
- minor_b: '#1a1a3a',
- link: '#5a3888',
- major_casing_early: '#0a0a14',
- major: '#8833aa',
- highway_casing_early: '#0a0a14',
- highway: '#ff2d6b',
- railway: '#2a2050',
- boundaries: '#4a4a6a',
-
- // Waterway label
- waterway_label: '#3a6a8a',
-
- // Bridges - same neon colors
- bridges_other_casing: '#0c0c18',
- bridges_minor_casing: '#0a0a14',
- bridges_link_casing: '#0a0a14',
- bridges_major_casing: '#0a0a14',
- bridges_highway_casing: '#0a0a14',
- bridges_other: '#1a1a3a',
- bridges_minor: '#2a2a5a',
- bridges_link: '#5a3888',
- bridges_major: '#8833aa',
- bridges_highway: '#ff2d6b',
-
- // Labels - cool white with DARK halos
- roads_label_minor: '#8888aa',
- roads_label_minor_halo: '#0a0a14',
- roads_label_major: '#a0a0c0',
- roads_label_major_halo: '#0a0a14',
- ocean_label: '#3a6a8a',
- peak_label: '#8888aa',
- subplace_label: '#8888aa',
- subplace_label_halo: '#0a0a14',
- city_label: '#d0d0e8',
- city_label_halo: '#0a0a14',
- state_label: '#5a5a7a',
- state_label_halo: '#0a0a14',
- country_label: '#7a7a9a',
- address_label: '#8888aa',
- address_label_halo: '#0a0a14',
-
- // POI icon colors - neon palette
- pois: {
- blue: '#00a0ff',
- green: '#00ff88',
- lapis: '#6060ff',
- pink: '#ff2d6b',
- red: '#ff3333',
- slategray: '#8888aa',
- tangerine: '#ffaa00',
- turquoise: '#00f0ff',
- },
-
- // Landcover fill colors - very dark, barely visible
- landcover: {
- grassland: 'rgba(10, 26, 18, 1)',
- barren: 'rgba(18, 16, 26, 1)',
- urban_area: 'rgba(14, 14, 26, 1)',
- farmland: 'rgba(12, 24, 16, 1)',
- glacier: 'rgba(16, 16, 32, 1)',
- scrub: 'rgba(12, 20, 16, 1)',
- forest: 'rgba(14, 30, 20, 1)',
- },
-}
-
-/**
- * UI CSS custom properties - neon command center aesthetic
- * Dark translucent panels with magenta/cyan accents
- */
-const cyberpunkUI = {
- // Fonts - monospace terminal feel
- '--font-sans': "'Share Tech Mono', monospace",
- '--font-mono': "'Share Tech Mono', monospace",
- '--font-heading': "'Orbitron', sans-serif",
- // Backgrounds - dark with blue-purple undertone
- '--bg-base': '#0a0a14',
- '--bg-raised': '#10101e',
- '--bg-overlay': '#161628',
- '--bg-input': '#0c0c18',
- '--bg-inset': '#08080f',
- '--bg-muted': '#12121e',
- // Text - cool white spectrum
- '--text-primary': '#d0d0e8',
- '--text-secondary': '#8888aa',
- '--text-tertiary': '#5a5a7a',
- '--text-inverse': '#0a0a14',
- // Borders - subtle purple edges
- '--border': '#1e1e3a',
- '--border-subtle': '#141428',
- // Accent - hot magenta
- '--accent': '#ff2d6b',
- '--accent-hover': '#ff4d8b',
- '--accent-muted': '#3a1828',
- // Tan becomes cyan in this theme
- '--tan': '#00f0ff',
- '--tan-muted': '#0a2830',
- // Pins - neon colors
- '--pin-origin': '#ff2d6b',
- '--pin-destination': '#00f0ff',
- '--pin-intermediate': '#8833aa',
- '--pin-stroke': '#0a0a14',
- // Status - neon signals
- '--status-success': '#00ff88',
- '--status-warning': '#ffaa00',
- '--status-danger': '#ff3333',
- '--success': '#00ff88',
- '--warning': '#ffaa00',
- '--warning-muted': '#2a2010',
- // Route - cyan for contrast with magenta UI
- '--route-line': '#00f0ff',
- // Shadows - subtle magenta glow
- '--shadow': '0 2px 8px rgba(255, 45, 107, 0.25)',
- '--shadow-lg': '0 4px 16px rgba(255, 45, 107, 0.35)',
-}
-
-/**
- * Overlay configuration - subtle, muted for dark theme
- */
-const cyberpunkOverlay = {
- // Hillshade - dramatic shadows
- hillshade: {
- exaggeration: 0.6,
- illuminationDirection: 315,
- shadowColor: '#000000',
- highlightColor: '#2a2a4a',
- },
-
- // Contours - very subtle dark purple-gray
- contours: {
- opacityMod: 0.5,
- minorColor: '#1e1e3e',
- minorOpacity: 0.3,
- minorWidth: { z11: 0.4, z14: 0.8 },
- intermediateColor: '#2a2a4a',
- intermediateOpacity: 0.4,
- intermediateWidth: { z8: 0.6, z14: 1.0 },
- indexColor: '#3a3a5a',
- indexOpacity: 0.5,
- indexWidth: { z4: 0.8, z14: 1.2 },
- labelColor: '#5a5a7a',
- labelHaloColor: '#0a0a14',
- labelHaloWidth: 1.5,
- labelOpacity: 0.6,
- labelSize: 10,
- labelFont: ['Noto Sans Regular'],
- },
-
- // Contours Test - cyan variant
- contoursTest: {
- minorColor: '#1a3a4a',
- intermediateColor: '#2a4a5a',
- indexColor: '#3a5a6a',
- labelColor: '#5a8a9a',
- },
-
- // Contours Test 10ft - purple variant
- contoursTest10ft: {
- minorColor: '#2a1a4a',
- intermediateColor: '#3a2a5a',
- indexColor: '#4a3a6a',
- labelColor: '#7a6a9a',
- },
-
- // Public Lands - very muted fills
- publicLands: {
- opacityMod: 0.5,
- // Fill colors - dark teal/purple tints
- fillWA: '#1a2a20',
- fillNPS: '#0a2a1a',
- fillUSFS: '#102820',
- fillBLM: '#1a2828',
- fillFWS: '#0a2a2a',
- fillSTAT: '#102028',
- fillLOC: '#182028',
- fillDefault: '#1a1a2a',
- // Fill opacities - very low
- fillOpacityWA: 0.25,
- fillOpacityNPS: 0.25,
- fillOpacityUSFS: 0.20,
- fillOpacityBLM: 0.15,
- fillOpacitySTAT: 0.20,
- fillOpacityLOC: 0.15,
- fillOpacityDefault: 0.10,
- // Outline colors - subtle
- outlineWA: '#2a3a30',
- outlineNPS: '#1a3a2a',
- outlineUSFS: '#203830',
- outlineBLM: '#2a3838',
- outlineFWS: '#1a3a3a',
- outlineSTAT: '#203038',
- outlineLOC: '#283038',
- outlineDefault: '#2a2a3a',
- // Outline opacities
- outlineOpacityNPS: 0.5,
- outlineOpacityUSFS: 0.4,
- outlineOpacityDefault: 0.3,
- // Outline width
- outlineWidth: { z4: 0.3, z8: 0.6, z12: 1.0 },
- // Labels - muted teal
- labelColor: '#5a8a8a',
- labelHaloColor: '#0a0a14',
- labelHaloWidth: 1.5,
- labelOpacity: 0.7,
- labelSize: { z10: 10, z14: 12 },
- labelFont: ['Noto Sans Regular'],
- },
-
- // USFS Trails - purple/magenta/cyan family instead of earthy browns
- usfsTrails: {
- // Roads - purple
- roadsColor: '#8833aa',
- roadsOpacity: 0.85,
- roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 },
- // Trails - neon colors by use type
- trailsMotorized: '#ff2d6b',
- trailsBicycle: '#ffaa00',
- trailsHiker: '#00ff88',
- trailsDefault: '#8833aa',
- trailsOpacity: 0.85,
- trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
- trailsDash: [2, 1.5],
- // Road labels
- roadsLabelColor: '#a080c0',
- roadsLabelHaloColor: '#0a0a14',
- roadsLabelHaloWidth: 1.5,
- roadsLabelOpacity: 0.85,
- roadsLabelSize: 11,
- // Trail labels
- trailsLabelColor: '#a080c0',
- trailsLabelHaloColor: '#0a0a14',
- trailsLabelHaloWidth: 1.5,
- trailsLabelOpacity: 0.85,
- trailsLabelSize: 11,
- labelFont: ['Noto Sans Regular'],
- // Hit layer
- hitWidth: 14,
- },
-
- // BLM Trails - purple/cyan/magenta family
- blmTrails: {
- // Route colors - neon family
- color4wdHigh: '#ff2d6b',
- color4wdLow: '#cc2288',
- colorAtv: '#ff3333',
- colorMotoSingle: '#aa44cc',
- color2wdLow: '#8833aa',
- colorNonMech: '#00ff88',
- colorDefault: '#6644aa',
- colorSnow: '#00f0ff',
- lineOpacity: 0.85,
- lineOpacityOther: 0.75,
- lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
- // Dash patterns
- dashImproved: [4, 2],
- dashAggregate: [1, 2],
- dashSnow: [4, 2, 1, 2],
- dashOther: [4, 2, 1, 2, 1, 2],
- // Labels
- labelColor: '#a080c0',
- labelHaloColor: '#0a0a14',
- labelHaloWidth: 1.5,
- labelOpacity: 0.85,
- labelSize: 11,
- labelFont: ['Noto Sans Regular'],
- // Hit layer
- hitWidth: 14,
- },
-}
-
-/**
- * Satellite adjustments - dark, desaturated, purple-shifted
- */
-const cyberpunkSatellite = {
- opacity: 0.8,
- brightnessMin: 0.0,
- brightnessMax: 0.30,
- contrast: 0.15,
- saturation: -0.6,
- hueRotate: 280,
-}
-
-/**
- * Cyberpunk theme configuration
- */
-const cyberpunkTheme = {
- id: 'cyberpunk',
- name: 'Cyberpunk',
- dark: true,
- swatch: ['#0a0a14', '#ff2d6b', '#00f0ff'],
- fontImports: [
- 'https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&display=swap',
- 'https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap',
- ],
- colors: cyberpunkColors,
- satellite: cyberpunkSatellite,
- overlay: cyberpunkOverlay,
- ui: cyberpunkUI,
-}
-
-export default cyberpunkTheme
+/**
+ * Cyberpunk Theme for Navi
+ *
+ * Inspired by Mapbox's "Terminal" cyberpunk style, Blade Runner, and Ghost in
+ * the Shell. A tactical display in a neon-lit command center. Near-black base
+ * with deep blue-purple undertones. Roads glow in hot magenta and electric cyan.
+ * Water is inky dark. Vegetation is barely there — dark teal hints. Labels are
+ * cool white with colored halos.
+ *
+ * The whole thing should feel like you're navigating Night City.
+ *
+ * CUSTOM FONTS:
+ * - Heading: "Orbitron" — geometric, futuristic display font
+ * - Body: "Share Tech Mono" — monospaced terminal feel for entire UI
+ */
+
+// ═══════════════════════════════════════════════════════════════════════════
+// PALETTE
+// ═══════════════════════════════════════════════════════════════════════════
+//
+// base: #0a0a14 ← near-black with blue-purple undertone
+// surface: #10101e ← panels, cards
+// surfaceAlt: #161628 ← secondary surfaces, hover states
+// border: #1e1e3a ← subtle purple edges
+// text: #d0d0e8 ← cool white text
+// textSecondary: #8888aa ← lavender-gray
+// textMuted: #5a5a7a ← dark purple-gray
+// textInverse: #0a0a14 ← text on neon backgrounds
+// accent: #ff2d6b ← hot pink/magenta — primary actions
+// accentHover: #ff4d8b ← lighter magenta
+// accentAlt: #00f0ff ← electric cyan — secondary accent
+// success: #00ff88 ← neon green
+// warning: #ffaa00 ← amber
+// danger: #ff3333 ← neon red
+// water: #06061a ← deep dark blue-black
+// waterLabel: #3a6a8a ← muted blue for water labels
+// vegetation: #0a1a12 ← barely-there dark teal-green
+// forest: #0e1e14 ← slightly deeper
+// road: #1a1a3a ← ghost purple minor roads
+// roadSecondary: #2a2a5a
+// roadPrimary: #8833aa ← purple for primary
+// roadMotorway: #ff2d6b ← hot magenta for motorways
+// roadCasing: #0a0a14 ← dark casing
+// building: #141428 ← dark purple-gray buildings
+// contour: #1e1e3e ← dark lines, just visible
+// contourLabel: #5a5a7a
+//
+// ═══════════════════════════════════════════════════════════════════════════
+
+/**
+ * Map flavor colors - protomaps-themes-base schema
+ * All 73 flat keys + pois + landcover nested objects
+ */
+const cyberpunkColors = {
+ // Background & earth
+ background: '#08080f',
+ earth: '#0a0a14',
+
+ // Land use areas - dark with slight purple undertones
+ park_a: '#0a1a14',
+ park_b: '#0e1e18',
+ hospital: '#1a1020',
+ industrial: '#0e0e1a',
+ school: '#14101e',
+ wood_a: '#0a1a12',
+ wood_b: '#0e1e14',
+ pedestrian: '#0c0c18',
+ scrub_a: '#0a1410',
+ scrub_b: '#0c1812',
+ glacier: '#101020',
+ sand: '#12101a',
+ beach: '#14121c',
+ aerodrome: '#0a0a16',
+ runway: '#1a1a30',
+ water: '#06061a',
+ zoo: '#0c1614',
+ military: '#100a14',
+
+ // Tunnels - dark purple casings
+ tunnel_other_casing: '#0a0a14',
+ tunnel_minor_casing: '#0a0a14',
+ tunnel_link_casing: '#0a0a14',
+ tunnel_major_casing: '#0a0a14',
+ tunnel_highway_casing: '#0a0a14',
+ tunnel_other: '#161628',
+ tunnel_minor: '#161628',
+ tunnel_link: '#2a2050',
+ tunnel_major: '#4a2870',
+ tunnel_highway: '#801848',
+
+ // Pier & buildings
+ pier: '#1a1a30',
+ buildings: '#141428',
+
+ // Roads & casings - glowing neon progression
+ minor_service_casing: '#0a0a14',
+ minor_casing: '#0a0a14',
+ link_casing: '#0a0a14',
+ major_casing_late: '#0a0a14',
+ highway_casing_late: '#0a0a14',
+ other: '#1a1a3a',
+ minor_service: '#1a1a3a',
+ minor_a: '#2a2a5a',
+ minor_b: '#1a1a3a',
+ link: '#5a3888',
+ major_casing_early: '#0a0a14',
+ major: '#8833aa',
+ highway_casing_early: '#0a0a14',
+ highway: '#ff2d6b',
+ railway: '#2a2050',
+ boundaries: '#4a4a6a',
+
+ // Waterway label
+ waterway_label: '#3a6a8a',
+
+ // Bridges - same neon colors
+ bridges_other_casing: '#0c0c18',
+ bridges_minor_casing: '#0a0a14',
+ bridges_link_casing: '#0a0a14',
+ bridges_major_casing: '#0a0a14',
+ bridges_highway_casing: '#0a0a14',
+ bridges_other: '#1a1a3a',
+ bridges_minor: '#2a2a5a',
+ bridges_link: '#5a3888',
+ bridges_major: '#8833aa',
+ bridges_highway: '#ff2d6b',
+
+ // Labels - cool white with DARK halos
+ roads_label_minor: '#8888aa',
+ roads_label_minor_halo: '#0a0a14',
+ roads_label_major: '#a0a0c0',
+ roads_label_major_halo: '#0a0a14',
+ ocean_label: '#3a6a8a',
+ peak_label: '#8888aa',
+ subplace_label: '#8888aa',
+ subplace_label_halo: '#0a0a14',
+ city_label: '#d0d0e8',
+ city_label_halo: '#0a0a14',
+ state_label: '#5a5a7a',
+ state_label_halo: '#0a0a14',
+ country_label: '#7a7a9a',
+ address_label: '#8888aa',
+ address_label_halo: '#0a0a14',
+
+ // POI icon colors - neon palette
+ pois: {
+ blue: '#00a0ff',
+ green: '#00ff88',
+ lapis: '#6060ff',
+ pink: '#ff2d6b',
+ red: '#ff3333',
+ slategray: '#8888aa',
+ tangerine: '#ffaa00',
+ turquoise: '#00f0ff',
+ },
+
+ // Landcover fill colors - very dark, barely visible
+ landcover: {
+ grassland: 'rgba(10, 26, 18, 1)',
+ barren: 'rgba(18, 16, 26, 1)',
+ urban_area: 'rgba(14, 14, 26, 1)',
+ farmland: 'rgba(12, 24, 16, 1)',
+ glacier: 'rgba(16, 16, 32, 1)',
+ scrub: 'rgba(12, 20, 16, 1)',
+ forest: 'rgba(14, 30, 20, 1)',
+ },
+}
+
+/**
+ * UI CSS custom properties - neon command center aesthetic
+ * Dark translucent panels with magenta/cyan accents
+ */
+const cyberpunkUI = {
+ // Fonts - monospace terminal feel
+ '--font-sans': "'Share Tech Mono', monospace",
+ '--font-mono': "'Share Tech Mono', monospace",
+ '--font-heading': "'Orbitron', sans-serif",
+ // Backgrounds - dark with blue-purple undertone
+ '--bg-base': '#0a0a14',
+ '--bg-raised': '#10101e',
+ '--bg-overlay': '#161628',
+ '--bg-input': '#0c0c18',
+ '--bg-inset': '#08080f',
+ '--bg-muted': '#12121e',
+ // Text - cool white spectrum
+ '--text-primary': '#d0d0e8',
+ '--text-secondary': '#8888aa',
+ '--text-tertiary': '#5a5a7a',
+ '--text-inverse': '#0a0a14',
+ // Borders - subtle purple edges
+ '--border': '#1e1e3a',
+ '--border-subtle': '#141428',
+ // Accent - hot magenta
+ '--accent': '#ff2d6b',
+ '--accent-hover': '#ff4d8b',
+ '--accent-muted': '#3a1828',
+ // Tan becomes cyan in this theme
+ '--tan': '#00f0ff',
+ '--tan-muted': '#0a2830',
+ // Pins - neon colors
+ '--pin-origin': '#ff2d6b',
+ '--pin-destination': '#00f0ff',
+ '--pin-intermediate': '#8833aa',
+ '--pin-stroke': '#0a0a14',
+ // Status - neon signals
+ '--status-success': '#00ff88',
+ '--status-warning': '#ffaa00',
+ '--status-danger': '#ff3333',
+ '--success': '#00ff88',
+ '--warning': '#ffaa00',
+ '--warning-muted': '#2a2010',
+ // Route - cyan for contrast with magenta UI
+ '--route-line': '#00f0ff',
+ // Shadows - subtle magenta glow
+ '--shadow': '0 2px 8px rgba(255, 45, 107, 0.25)',
+ '--shadow-lg': '0 4px 16px rgba(255, 45, 107, 0.35)',
+}
+
+/**
+ * Overlay configuration - subtle, muted for dark theme
+ */
+const cyberpunkOverlay = {
+ // Hillshade - dramatic shadows
+ hillshade: {
+ exaggeration: 0.6,
+ illuminationDirection: 315,
+ shadowColor: '#000000',
+ highlightColor: '#2a2a4a',
+ },
+
+ // Contours - very subtle dark purple-gray
+ contours: {
+ opacityMod: 0.5,
+ minorColor: '#1e1e3e',
+ minorOpacity: 0.3,
+ minorWidth: { z11: 0.4, z14: 0.8 },
+ intermediateColor: '#2a2a4a',
+ intermediateOpacity: 0.4,
+ intermediateWidth: { z8: 0.6, z14: 1.0 },
+ indexColor: '#3a3a5a',
+ indexOpacity: 0.5,
+ indexWidth: { z4: 0.8, z14: 1.2 },
+ labelColor: '#5a5a7a',
+ labelHaloColor: '#0a0a14',
+ labelHaloWidth: 1.5,
+ labelOpacity: 0.6,
+ labelSize: 10,
+ labelFont: ['Noto Sans Regular'],
+ },
+
+ // Contours Test - cyan variant
+ contoursTest: {
+ minorColor: '#1a3a4a',
+ intermediateColor: '#2a4a5a',
+ indexColor: '#3a5a6a',
+ labelColor: '#5a8a9a',
+ },
+
+ // Contours Test 10ft - purple variant
+ contoursTest10ft: {
+ minorColor: '#2a1a4a',
+ intermediateColor: '#3a2a5a',
+ indexColor: '#4a3a6a',
+ labelColor: '#7a6a9a',
+ },
+
+ // Public Lands - very muted fills
+ publicLands: {
+ opacityMod: 0.5,
+ // Fill colors - dark teal/purple tints
+ fillWA: '#1a2a20',
+ fillNPS: '#0a2a1a',
+ fillUSFS: '#102820',
+ fillBLM: '#1a2828',
+ fillFWS: '#0a2a2a',
+ fillSTAT: '#102028',
+ fillLOC: '#182028',
+ fillDefault: '#1a1a2a',
+ // Fill opacities - very low
+ fillOpacityWA: 0.25,
+ fillOpacityNPS: 0.25,
+ fillOpacityUSFS: 0.20,
+ fillOpacityBLM: 0.15,
+ fillOpacitySTAT: 0.20,
+ fillOpacityLOC: 0.15,
+ fillOpacityDefault: 0.10,
+ // Outline colors - subtle
+ outlineWA: '#2a3a30',
+ outlineNPS: '#1a3a2a',
+ outlineUSFS: '#203830',
+ outlineBLM: '#2a3838',
+ outlineFWS: '#1a3a3a',
+ outlineSTAT: '#203038',
+ outlineLOC: '#283038',
+ outlineDefault: '#2a2a3a',
+ // Outline opacities
+ outlineOpacityNPS: 0.5,
+ outlineOpacityUSFS: 0.4,
+ outlineOpacityDefault: 0.3,
+ // Outline width
+ outlineWidth: { z4: 0.3, z8: 0.6, z12: 1.0 },
+ // Labels - muted teal
+ labelColor: '#5a8a8a',
+ labelHaloColor: '#0a0a14',
+ labelHaloWidth: 1.5,
+ labelOpacity: 0.7,
+ labelSize: { z10: 10, z14: 12 },
+ labelFont: ['Noto Sans Regular'],
+ },
+
+ // USFS Trails - purple/magenta/cyan family instead of earthy browns
+ usfsTrails: {
+ // Roads - purple
+ roadsColor: '#8833aa',
+ roadsOpacity: 0.85,
+ roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 },
+ // Trails - neon colors by use type
+ trailsMotorized: '#ff2d6b',
+ trailsBicycle: '#ffaa00',
+ trailsHiker: '#00ff88',
+ trailsDefault: '#8833aa',
+ trailsOpacity: 0.85,
+ trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
+ trailsDash: [2, 1.5],
+ // Road labels
+ roadsLabelColor: '#a080c0',
+ roadsLabelHaloColor: '#0a0a14',
+ roadsLabelHaloWidth: 1.5,
+ roadsLabelOpacity: 0.85,
+ roadsLabelSize: 11,
+ // Trail labels
+ trailsLabelColor: '#a080c0',
+ trailsLabelHaloColor: '#0a0a14',
+ trailsLabelHaloWidth: 1.5,
+ trailsLabelOpacity: 0.85,
+ trailsLabelSize: 11,
+ labelFont: ['Noto Sans Regular'],
+ // Hit layer
+ hitWidth: 14,
+ },
+
+ // BLM Trails - purple/cyan/magenta family
+ blmTrails: {
+ // Route colors - neon family
+ color4wdHigh: '#ff2d6b',
+ color4wdLow: '#cc2288',
+ colorAtv: '#ff3333',
+ colorMotoSingle: '#aa44cc',
+ color2wdLow: '#8833aa',
+ colorNonMech: '#00ff88',
+ colorDefault: '#6644aa',
+ colorSnow: '#00f0ff',
+ lineOpacity: 0.85,
+ lineOpacityOther: 0.75,
+ lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
+ // Dash patterns
+ dashImproved: [4, 2],
+ dashAggregate: [1, 2],
+ dashSnow: [4, 2, 1, 2],
+ dashOther: [4, 2, 1, 2, 1, 2],
+ // Labels
+ labelColor: '#a080c0',
+ labelHaloColor: '#0a0a14',
+ labelHaloWidth: 1.5,
+ labelOpacity: 0.85,
+ labelSize: 11,
+ labelFont: ['Noto Sans Regular'],
+ // Hit layer
+ hitWidth: 14,
+ },
+
+ // ── Highlight (boundary/selection) ────────────────────────────────────────
+ highlight: {
+ lineColor: "#00f0ff", // Electric cyan for selection
+ lineWidth: 2,
+ lineDash: [4, 4],
+ lineOpacity: 0.9,
+ fillColor: "#00f0ff",
+ fillOpacity: 0.1,
+ },
+}
+
+/**
+ * Satellite adjustments - dark, desaturated, purple-shifted
+ */
+const cyberpunkSatellite = {
+ opacity: 0.8,
+ brightnessMin: 0.0,
+ brightnessMax: 0.30,
+ contrast: 0.15,
+ saturation: -0.6,
+ hueRotate: 280,
+}
+
+/**
+ * Cyberpunk theme configuration
+ */
+const cyberpunkTheme = {
+ id: 'cyberpunk',
+ name: 'Cyberpunk',
+ dark: true,
+ swatch: ['#0a0a14', '#ff2d6b', '#00f0ff'],
+ fontImports: [
+ 'https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&display=swap',
+ 'https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap',
+ ],
+ colors: cyberpunkColors,
+ satellite: cyberpunkSatellite,
+ overlay: cyberpunkOverlay,
+ ui: cyberpunkUI,
+}
+
+export default cyberpunkTheme
diff --git a/src/themes/registry.js b/src/themes/registry.js
index cc48c8f..7f6b74c 100644
--- a/src/themes/registry.js
+++ b/src/themes/registry.js
@@ -1,652 +1,672 @@
-/**
- * Theme Registry for Navi
- *
- * Provides a centralized registry for map themes, supporting both built-in
- * protomaps themes (light/dark) and custom themes with full flavor objects.
- *
- * Theme config structure:
- * id: string - unique identifier (used in store, data-theme attr)
- * name: string - display name for UI
- * dark: boolean - true if dark theme (affects overlay styling, sprite fallback)
- * colors: object|null - null for built-in themes, full flavor object for custom
- * satellite: object|null - raster adjustments when satellite layer is present
- * overlay: object - overlay layer styling configuration
- * ui: object - CSS custom properties for UI elements
- * swatch: string[3] - 3 hex colors for theme picker preview
- * fontImports: string[] - URLs for font CSS imports (empty for system fonts)
- */
-
-import { namedTheme } from 'protomaps-themes-base'
-import cleanTheme from './clean.js'
+/**
+ * Theme Registry for Navi
+ *
+ * Provides a centralized registry for map themes, supporting both built-in
+ * protomaps themes (light/dark) and custom themes with full flavor objects.
+ *
+ * Theme config structure:
+ * id: string - unique identifier (used in store, data-theme attr)
+ * name: string - display name for UI
+ * dark: boolean - true if dark theme (affects overlay styling, sprite fallback)
+ * colors: object|null - null for built-in themes, full flavor object for custom
+ * satellite: object|null - raster adjustments when satellite layer is present
+ * overlay: object - overlay layer styling configuration
+ * ui: object - CSS custom properties for UI elements
+ * swatch: string[3] - 3 hex colors for theme picker preview
+ * fontImports: string[] - URLs for font CSS imports (empty for system fonts)
+ */
+
+import { namedTheme } from 'protomaps-themes-base'
+import cleanTheme from './clean.js'
import cyberpunkTheme from './cyberpunk.js'
-
-// ═══════════════════════════════════════════════════════════════════════════
-// UI CSS CUSTOM PROPERTIES
-// ═══════════════════════════════════════════════════════════════════════════
-
-/**
- * Dark theme UI configuration
- * All CSS custom properties for dark theme UI
- */
-const darkUI = {
- // Fonts
- '--font-sans': "'Inter', system-ui, -apple-system, sans-serif",
- '--font-mono': "'JetBrains Mono', ui-monospace, monospace",
+
+// ═══════════════════════════════════════════════════════════════════════════
+// UI CSS CUSTOM PROPERTIES
+// ═══════════════════════════════════════════════════════════════════════════
+
+/**
+ * Dark theme UI configuration
+ * All CSS custom properties for dark theme UI
+ */
+const darkUI = {
+ // Fonts
+ '--font-sans': "'Inter', system-ui, -apple-system, sans-serif",
+ '--font-mono': "'JetBrains Mono', ui-monospace, monospace",
'--font-heading': "'Inter', system-ui, -apple-system, sans-serif",
- // Backgrounds
- '--bg-base': '#1c1917',
- '--bg-raised': '#252220',
- '--bg-overlay': '#2e2a27',
- '--bg-input': '#201d1a',
- '--bg-inset': '#181614',
- '--bg-muted': '#2a2725',
- // Text
- '--text-primary': '#dde3dc',
- '--text-secondary': '#8f9a8e',
- '--text-tertiary': '#5e6b5d',
- '--text-inverse': '#1c1917',
- // Borders
- '--border': '#3a3530',
- '--border-subtle': '#2a2624',
- // Accent
- '--accent': '#7a9a6b',
- '--accent-hover': '#8fad7f',
- '--accent-muted': '#3d4d36',
- // Tan
- '--tan': '#b8a88a',
- '--tan-muted': '#4a4235',
- // Pins
- '--pin-origin': '#6b8f5e',
- '--pin-destination': '#a67c52',
- '--pin-intermediate': '#6b7268',
- '--pin-stroke': '#1c1917',
- // Status
- '--status-success': '#6b8f5e',
- '--status-warning': '#b89a4a',
- '--status-danger': '#a65c52',
- '--success': '#6b8f5e',
- '--warning': '#b89a4a',
- '--warning-muted': '#4a4235',
- // Route
- '--route-line': '#7a9a6b',
- // Shadows
- '--shadow': '0 2px 8px rgba(0, 0, 0, 0.4)',
- '--shadow-lg': '0 4px 16px rgba(0, 0, 0, 0.5)',
-}
-
-/**
- * Light theme UI configuration
- * All CSS custom properties for light theme UI
- */
-const lightUI = {
- // Fonts
- '--font-sans': "'Inter', system-ui, -apple-system, sans-serif",
- '--font-mono': "'JetBrains Mono', ui-monospace, monospace",
+ // Backgrounds
+ '--bg-base': '#1c1917',
+ '--bg-raised': '#252220',
+ '--bg-overlay': '#2e2a27',
+ '--bg-input': '#201d1a',
+ '--bg-inset': '#181614',
+ '--bg-muted': '#2a2725',
+ // Text
+ '--text-primary': '#dde3dc',
+ '--text-secondary': '#8f9a8e',
+ '--text-tertiary': '#5e6b5d',
+ '--text-inverse': '#1c1917',
+ // Borders
+ '--border': '#3a3530',
+ '--border-subtle': '#2a2624',
+ // Accent
+ '--accent': '#7a9a6b',
+ '--accent-hover': '#8fad7f',
+ '--accent-muted': '#3d4d36',
+ // Tan
+ '--tan': '#b8a88a',
+ '--tan-muted': '#4a4235',
+ // Pins
+ '--pin-origin': '#6b8f5e',
+ '--pin-destination': '#a67c52',
+ '--pin-intermediate': '#6b7268',
+ '--pin-stroke': '#1c1917',
+ // Status
+ '--status-success': '#6b8f5e',
+ '--status-warning': '#b89a4a',
+ '--status-danger': '#a65c52',
+ '--success': '#6b8f5e',
+ '--warning': '#b89a4a',
+ '--warning-muted': '#4a4235',
+ // Route
+ '--route-line': '#7a9a6b',
+ // Shadows
+ '--shadow': '0 2px 8px rgba(0, 0, 0, 0.4)',
+ '--shadow-lg': '0 4px 16px rgba(0, 0, 0, 0.5)',
+}
+
+/**
+ * Light theme UI configuration
+ * All CSS custom properties for light theme UI
+ */
+const lightUI = {
+ // Fonts
+ '--font-sans': "'Inter', system-ui, -apple-system, sans-serif",
+ '--font-mono': "'JetBrains Mono', ui-monospace, monospace",
'--font-heading': "'Inter', system-ui, -apple-system, sans-serif",
- // Backgrounds
- '--bg-base': '#ddd2b9',
- '--bg-raised': '#e8dec8',
- '--bg-overlay': '#e3d9c1',
- '--bg-input': '#e8dec8',
- '--bg-inset': '#d5cab2',
- '--bg-muted': '#e0d6c0',
- // Text
- '--text-primary': '#1a1d1a',
- '--text-secondary': '#4f5a49',
- '--text-tertiary': '#7a8674',
- '--text-inverse': '#f5f2ed',
- // Borders
- '--border': '#c4b89e',
- '--border-subtle': '#d5cab2',
- // Accent
- '--accent': '#4a7040',
- '--accent-hover': '#3d5e35',
- '--accent-muted': '#dce8d6',
- // Tan
- '--tan': '#8a7556',
- '--tan-muted': '#f0e8d8',
- // Pins
- '--pin-origin': '#4a7040',
- '--pin-destination': '#8a5c35',
- '--pin-intermediate': '#6b6960',
- '--pin-stroke': '#1a1d1a',
- // Status
- '--status-success': '#4a7040',
- '--status-warning': '#8a7040',
- '--status-danger': '#8a4040',
- '--success': '#4a7040',
- '--warning': '#8a7040',
- '--warning-muted': '#f0e8d8',
- // Route
- '--route-line': '#4a7040',
- // Shadows
- '--shadow': '0 2px 8px rgba(0, 0, 0, 0.08)',
- '--shadow-lg': '0 4px 16px rgba(0, 0, 0, 0.12)',
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// OVERLAY CONFIGURATIONS
-// ═══════════════════════════════════════════════════════════════════════════
-
-/**
- * Dark theme overlay configuration
- * All hardcoded values from overlay add functions extracted here
- */
-const darkOverlay = {
- // ── Hillshade ─────────────────────────────────────────────────────────────
- hillshade: {
- exaggeration: 0.5,
- illuminationDirection: 315,
- shadowColor: '#000000',
- highlightColor: '#ffffff',
- },
-
- // ── Traffic ───────────────────────────────────────────────────────────────
- traffic: {
- opacity: 0.6,
- },
-
- // ── Contours (main, brown/tan scheme) ─────────────────────────────────────
- contours: {
- opacityMod: 0.8,
- minorColor: '#8b6f47',
- minorOpacity: 0.4,
- minorWidth: { z11: 0.5, z14: 1.0 },
- intermediateColor: '#8b6f47',
- intermediateOpacity: 0.7,
- intermediateWidth: { z8: 0.8, z14: 1.2 },
- indexColor: '#6b4f2a',
- indexOpacity: 0.9,
- indexWidth: { z4: 1.2, z14: 1.8 },
- labelColor: '#c0b898',
- labelHaloColor: '#1a1a1a',
- labelHaloWidth: 1.5,
- labelOpacity: 0.85,
- labelSize: 10,
- labelFont: ['Noto Sans Regular'],
- },
-
- // ── Contours Test (blue scheme) ───────────────────────────────────────────
- // Missing keys cascade from contours
- contoursTest: {
- minorColor: '#4a7c9b',
- intermediateColor: '#4a7c9b',
- indexColor: '#2a5a7c',
- labelColor: '#98b8d0',
- },
-
- // ── Contours Test 10ft (green scheme) ─────────────────────────────────────
- // Missing keys cascade from contours
- contoursTest10ft: {
- minorColor: '#3a7c4f',
- intermediateColor: '#3a7c4f',
- indexColor: '#2a5c3a',
- labelColor: '#98c0a8',
- },
-
- // ── Public Lands (PAD-US) ─────────────────────────────────────────────────
- publicLands: {
- opacityMod: 0.7,
- // Fill colors per category
- fillWA: '#7c6b2f',
- fillNPS: '#3d6b1f',
- fillUSFS: '#5a7c2f',
- fillBLM: '#c4a672',
- fillFWS: '#4a7a5a',
- fillSTAT: '#5a8c7c',
- fillLOC: '#8ca694',
- fillDefault: '#a0a0a0',
- // Fill base opacities (multiplied by opacityMod)
- fillOpacityWA: 0.30,
- fillOpacityNPS: 0.30,
- fillOpacityUSFS: 0.25,
- fillOpacityBLM: 0.20,
- fillOpacitySTAT: 0.25,
- fillOpacityLOC: 0.20,
- fillOpacityDefault: 0.15,
- // Outline colors per category
- outlineWA: '#5a4d20',
- outlineNPS: '#2a4a15',
- outlineUSFS: '#3d5520',
- outlineBLM: '#8a7343',
- outlineFWS: '#2d5a3a',
- outlineSTAT: '#3d6055',
- outlineLOC: '#5c6e66',
- outlineDefault: '#707070',
- // Outline opacities
- outlineOpacityNPS: 0.7,
- outlineOpacityUSFS: 0.6,
- outlineOpacityDefault: 0.5,
- // Outline width
- outlineWidth: { z4: 0.3, z8: 0.8, z12: 1.2 },
- // Labels
- labelColor: '#c0c8b8',
- labelHaloColor: '#1a1a1a',
- labelHaloWidth: 1.5,
- labelOpacity: 0.85,
- labelSize: { z10: 10, z14: 13 },
- labelFont: ['Noto Sans Regular'],
- },
-
- // ── USFS Trails ───────────────────────────────────────────────────────────
- usfsTrails: {
- // Roads
- roadsColor: '#d0a060',
- roadsOpacity: 0.9,
- roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 },
- // Trails colors by use type
- trailsMotorized: '#f08040',
- trailsBicycle: '#e0b040',
- trailsHiker: '#60c050',
- trailsDefault: '#c0a060',
- trailsOpacity: 0.9,
- trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
- trailsDash: [2, 1.5],
- // Road labels
- roadsLabelColor: '#d0c0a0',
- roadsLabelHaloColor: '#1a1a1a',
- roadsLabelHaloWidth: 1.5,
- roadsLabelOpacity: 0.9,
- roadsLabelSize: 11,
- // Trail labels
- trailsLabelColor: '#d0b090',
- trailsLabelHaloColor: '#1a1a1a',
- trailsLabelHaloWidth: 1.5,
- trailsLabelOpacity: 0.9,
- trailsLabelSize: 11,
- labelFont: ['Noto Sans Regular'],
- // Hit layer
- hitWidth: 14,
- },
-
- // ── BLM Trails / Roads ────────────────────────────────────────────────────
- blmTrails: {
- // Route colors by use class
- color4wdHigh: '#f08040',
- color4wdLow: '#e0b040',
- colorAtv: '#e04040',
- colorMotoSingle: '#b070c0',
- color2wdLow: '#f0d070',
- colorNonMech: '#60c050',
- colorDefault: '#c0a060',
- colorSnow: '#80b0e0',
- lineOpacity: 0.9,
- lineOpacityOther: 0.85,
- lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
- // Dash patterns by surface type
- dashImproved: [4, 2],
- dashAggregate: [1, 2],
- dashSnow: [4, 2, 1, 2],
- dashOther: [4, 2, 1, 2, 1, 2],
- // Labels
- labelColor: '#d0c0a0',
- labelHaloColor: '#1a1a1a',
- labelHaloWidth: 1.5,
- labelOpacity: 0.9,
- labelSize: 11,
- labelFont: ['Noto Sans Regular'],
- // Hit layer
- hitWidth: 14,
- },
-}
-
-/**
- * Light theme overlay configuration
- * All hardcoded values from overlay add functions extracted here
- */
-const lightOverlay = {
- // ── Hillshade ─────────────────────────────────────────────────────────────
- hillshade: {
- exaggeration: 0.5,
- illuminationDirection: 315,
- shadowColor: '#000000',
- highlightColor: '#ffffff',
- },
-
- // ── Traffic ───────────────────────────────────────────────────────────────
- traffic: {
- opacity: 0.6,
- },
-
- // ── Contours (main, brown/tan scheme) ─────────────────────────────────────
- contours: {
- opacityMod: 1.0,
- minorColor: '#8b6f47',
- minorOpacity: 0.4,
- minorWidth: { z11: 0.5, z14: 1.0 },
- intermediateColor: '#8b6f47',
- intermediateOpacity: 0.7,
- intermediateWidth: { z8: 0.8, z14: 1.2 },
- indexColor: '#6b4f2a',
- indexOpacity: 0.9,
- indexWidth: { z4: 1.2, z14: 1.8 },
- labelColor: '#5a4020',
- labelHaloColor: '#ffffff',
- labelHaloWidth: 1.5,
- labelOpacity: 0.85,
- labelSize: 10,
- labelFont: ['Noto Sans Regular'],
- },
-
- // ── Contours Test (blue scheme) ───────────────────────────────────────────
- // Missing keys cascade from contours
- contoursTest: {
- minorColor: '#4a7c9b',
- intermediateColor: '#4a7c9b',
- indexColor: '#2a5a7c',
- labelColor: '#205080',
- },
-
- // ── Contours Test 10ft (green scheme) ─────────────────────────────────────
- // Missing keys cascade from contours
- contoursTest10ft: {
- minorColor: '#3a7c4f',
- intermediateColor: '#3a7c4f',
- indexColor: '#2a5c3a',
- labelColor: '#2a4030',
- },
-
- // ── Public Lands (PAD-US) ─────────────────────────────────────────────────
- publicLands: {
- opacityMod: 1.0,
- // Fill colors per category
- fillWA: '#7c6b2f',
- fillNPS: '#3d6b1f',
- fillUSFS: '#5a7c2f',
- fillBLM: '#c4a672',
- fillFWS: '#4a7a5a',
- fillSTAT: '#5a8c7c',
- fillLOC: '#8ca694',
- fillDefault: '#a0a0a0',
- // Fill base opacities (multiplied by opacityMod)
- fillOpacityWA: 0.30,
- fillOpacityNPS: 0.30,
- fillOpacityUSFS: 0.25,
- fillOpacityBLM: 0.20,
- fillOpacitySTAT: 0.25,
- fillOpacityLOC: 0.20,
- fillOpacityDefault: 0.15,
- // Outline colors per category
- outlineWA: '#5a4d20',
- outlineNPS: '#2a4a15',
- outlineUSFS: '#3d5520',
- outlineBLM: '#8a7343',
- outlineFWS: '#2d5a3a',
- outlineSTAT: '#3d6055',
- outlineLOC: '#5c6e66',
- outlineDefault: '#707070',
- // Outline opacities
- outlineOpacityNPS: 0.7,
- outlineOpacityUSFS: 0.6,
- outlineOpacityDefault: 0.5,
- // Outline width
- outlineWidth: { z4: 0.3, z8: 0.8, z12: 1.2 },
- // Labels
- labelColor: '#3a4a30',
- labelHaloColor: '#ffffff',
- labelHaloWidth: 1.5,
- labelOpacity: 0.85,
- labelSize: { z10: 10, z14: 13 },
- labelFont: ['Noto Sans Regular'],
- },
-
- // ── USFS Trails ───────────────────────────────────────────────────────────
- usfsTrails: {
- // Roads
- roadsColor: '#c09050',
- roadsOpacity: 0.9,
- roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 },
- // Trails colors by use type
- trailsMotorized: '#e07030',
- trailsBicycle: '#d0a030',
- trailsHiker: '#50b040',
- trailsDefault: '#b09050',
- trailsOpacity: 0.9,
- trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
- trailsDash: [2, 1.5],
- // Road labels
- roadsLabelColor: '#6a5a40',
- roadsLabelHaloColor: '#ffffff',
- roadsLabelHaloWidth: 1.5,
- roadsLabelOpacity: 0.9,
- roadsLabelSize: 11,
- // Trail labels
- trailsLabelColor: '#5a4a30',
- trailsLabelHaloColor: '#ffffff',
- trailsLabelHaloWidth: 1.5,
- trailsLabelOpacity: 0.9,
- trailsLabelSize: 11,
- labelFont: ['Noto Sans Regular'],
- // Hit layer
- hitWidth: 14,
- },
-
- // ── BLM Trails / Roads ────────────────────────────────────────────────────
- blmTrails: {
- // Route colors by use class
- color4wdHigh: '#e07030',
- color4wdLow: '#d0a030',
- colorAtv: '#d03030',
- colorMotoSingle: '#a060b0',
- color2wdLow: '#e0c060',
- colorNonMech: '#50b040',
- colorDefault: '#b09050',
- colorSnow: '#6090c0',
- lineOpacity: 0.9,
- lineOpacityOther: 0.85,
- lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
- // Dash patterns by surface type
- dashImproved: [4, 2],
- dashAggregate: [1, 2],
- dashSnow: [4, 2, 1, 2],
- dashOther: [4, 2, 1, 2, 1, 2],
- // Labels
- labelColor: '#5a4a30',
- labelHaloColor: '#ffffff',
- labelHaloWidth: 1.5,
- labelOpacity: 0.9,
- labelSize: 11,
- labelFont: ['Noto Sans Regular'],
- // Hit layer
- hitWidth: 14,
- },
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// THEME REGISTRY
-// ═══════════════════════════════════════════════════════════════════════════
-
-/**
- * Theme registry - maps theme IDs to theme configurations
- *
- * Built-in themes (light/dark) use colors: null to signal that namedTheme()
- * should be called at render time. Custom themes provide a full flavor object.
- */
-const themes = {
- light: {
- id: 'light',
- name: 'Light',
- dark: false,
- colors: null, // Use namedTheme('light')
- satellite: null,
- overlay: lightOverlay,
- ui: lightUI,
- swatch: ['#ddd2b9', '#4a7040', '#8a7556'],
- fontImports: [],
- },
- dark: {
- id: 'dark',
- name: 'Dark',
- dark: true,
- colors: null, // Use namedTheme('dark')
- satellite: null,
- overlay: darkOverlay,
- ui: darkUI,
- swatch: ['#1c1917', '#7a9a6b', '#b8a88a'],
- fontImports: [],
- },
- clean: {
- ...cleanTheme,
- swatch: ['#f5f5f5', '#1a73e8', '#34a853'],
- fontImports: [],
- },
+ // Backgrounds
+ '--bg-base': '#ddd2b9',
+ '--bg-raised': '#e8dec8',
+ '--bg-overlay': '#e3d9c1',
+ '--bg-input': '#e8dec8',
+ '--bg-inset': '#d5cab2',
+ '--bg-muted': '#e0d6c0',
+ // Text
+ '--text-primary': '#1a1d1a',
+ '--text-secondary': '#4f5a49',
+ '--text-tertiary': '#7a8674',
+ '--text-inverse': '#f5f2ed',
+ // Borders
+ '--border': '#c4b89e',
+ '--border-subtle': '#d5cab2',
+ // Accent
+ '--accent': '#4a7040',
+ '--accent-hover': '#3d5e35',
+ '--accent-muted': '#dce8d6',
+ // Tan
+ '--tan': '#8a7556',
+ '--tan-muted': '#f0e8d8',
+ // Pins
+ '--pin-origin': '#4a7040',
+ '--pin-destination': '#8a5c35',
+ '--pin-intermediate': '#6b6960',
+ '--pin-stroke': '#1a1d1a',
+ // Status
+ '--status-success': '#4a7040',
+ '--status-warning': '#8a7040',
+ '--status-danger': '#8a4040',
+ '--success': '#4a7040',
+ '--warning': '#8a7040',
+ '--warning-muted': '#f0e8d8',
+ // Route
+ '--route-line': '#4a7040',
+ // Shadows
+ '--shadow': '0 2px 8px rgba(0, 0, 0, 0.08)',
+ '--shadow-lg': '0 4px 16px rgba(0, 0, 0, 0.12)',
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// OVERLAY CONFIGURATIONS
+// ═══════════════════════════════════════════════════════════════════════════
+
+/**
+ * Dark theme overlay configuration
+ * All hardcoded values from overlay add functions extracted here
+ */
+const darkOverlay = {
+ // ── Hillshade ─────────────────────────────────────────────────────────────
+ hillshade: {
+ exaggeration: 0.5,
+ illuminationDirection: 315,
+ shadowColor: '#000000',
+ highlightColor: '#ffffff',
+ },
+
+ // ── Traffic ───────────────────────────────────────────────────────────────
+ traffic: {
+ opacity: 0.6,
+ },
+
+ // ── Contours (main, brown/tan scheme) ─────────────────────────────────────
+ contours: {
+ opacityMod: 0.8,
+ minorColor: '#8b6f47',
+ minorOpacity: 0.4,
+ minorWidth: { z11: 0.5, z14: 1.0 },
+ intermediateColor: '#8b6f47',
+ intermediateOpacity: 0.7,
+ intermediateWidth: { z8: 0.8, z14: 1.2 },
+ indexColor: '#6b4f2a',
+ indexOpacity: 0.9,
+ indexWidth: { z4: 1.2, z14: 1.8 },
+ labelColor: '#c0b898',
+ labelHaloColor: '#1a1a1a',
+ labelHaloWidth: 1.5,
+ labelOpacity: 0.85,
+ labelSize: 10,
+ labelFont: ['Noto Sans Regular'],
+ },
+
+ // ── Contours Test (blue scheme) ───────────────────────────────────────────
+ // Missing keys cascade from contours
+ contoursTest: {
+ minorColor: '#4a7c9b',
+ intermediateColor: '#4a7c9b',
+ indexColor: '#2a5a7c',
+ labelColor: '#98b8d0',
+ },
+
+ // ── Contours Test 10ft (green scheme) ─────────────────────────────────────
+ // Missing keys cascade from contours
+ contoursTest10ft: {
+ minorColor: '#3a7c4f',
+ intermediateColor: '#3a7c4f',
+ indexColor: '#2a5c3a',
+ labelColor: '#98c0a8',
+ },
+
+ // ── Public Lands (PAD-US) ─────────────────────────────────────────────────
+ publicLands: {
+ opacityMod: 0.7,
+ // Fill colors per category
+ fillWA: '#7c6b2f',
+ fillNPS: '#3d6b1f',
+ fillUSFS: '#5a7c2f',
+ fillBLM: '#c4a672',
+ fillFWS: '#4a7a5a',
+ fillSTAT: '#5a8c7c',
+ fillLOC: '#8ca694',
+ fillDefault: '#a0a0a0',
+ // Fill base opacities (multiplied by opacityMod)
+ fillOpacityWA: 0.30,
+ fillOpacityNPS: 0.30,
+ fillOpacityUSFS: 0.25,
+ fillOpacityBLM: 0.20,
+ fillOpacitySTAT: 0.25,
+ fillOpacityLOC: 0.20,
+ fillOpacityDefault: 0.15,
+ // Outline colors per category
+ outlineWA: '#5a4d20',
+ outlineNPS: '#2a4a15',
+ outlineUSFS: '#3d5520',
+ outlineBLM: '#8a7343',
+ outlineFWS: '#2d5a3a',
+ outlineSTAT: '#3d6055',
+ outlineLOC: '#5c6e66',
+ outlineDefault: '#707070',
+ // Outline opacities
+ outlineOpacityNPS: 0.7,
+ outlineOpacityUSFS: 0.6,
+ outlineOpacityDefault: 0.5,
+ // Outline width
+ outlineWidth: { z4: 0.3, z8: 0.8, z12: 1.2 },
+ // Labels
+ labelColor: '#c0c8b8',
+ labelHaloColor: '#1a1a1a',
+ labelHaloWidth: 1.5,
+ labelOpacity: 0.85,
+ labelSize: { z10: 10, z14: 13 },
+ labelFont: ['Noto Sans Regular'],
+ },
+
+ // ── USFS Trails ───────────────────────────────────────────────────────────
+ usfsTrails: {
+ // Roads
+ roadsColor: '#d0a060',
+ roadsOpacity: 0.9,
+ roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 },
+ // Trails colors by use type
+ trailsMotorized: '#f08040',
+ trailsBicycle: '#e0b040',
+ trailsHiker: '#60c050',
+ trailsDefault: '#c0a060',
+ trailsOpacity: 0.9,
+ trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
+ trailsDash: [2, 1.5],
+ // Road labels
+ roadsLabelColor: '#d0c0a0',
+ roadsLabelHaloColor: '#1a1a1a',
+ roadsLabelHaloWidth: 1.5,
+ roadsLabelOpacity: 0.9,
+ roadsLabelSize: 11,
+ // Trail labels
+ trailsLabelColor: '#d0b090',
+ trailsLabelHaloColor: '#1a1a1a',
+ trailsLabelHaloWidth: 1.5,
+ trailsLabelOpacity: 0.9,
+ trailsLabelSize: 11,
+ labelFont: ['Noto Sans Regular'],
+ // Hit layer
+ hitWidth: 14,
+ },
+
+ // ── BLM Trails / Roads ────────────────────────────────────────────────────
+ blmTrails: {
+ // Route colors by use class
+ color4wdHigh: '#f08040',
+ color4wdLow: '#e0b040',
+ colorAtv: '#e04040',
+ colorMotoSingle: '#b070c0',
+ color2wdLow: '#f0d070',
+ colorNonMech: '#60c050',
+ colorDefault: '#c0a060',
+ colorSnow: '#80b0e0',
+ lineOpacity: 0.9,
+ lineOpacityOther: 0.85,
+ lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
+ // Dash patterns by surface type
+ dashImproved: [4, 2],
+ dashAggregate: [1, 2],
+ dashSnow: [4, 2, 1, 2],
+ dashOther: [4, 2, 1, 2, 1, 2],
+ // Labels
+ labelColor: '#d0c0a0',
+ labelHaloColor: '#1a1a1a',
+ labelHaloWidth: 1.5,
+ labelOpacity: 0.9,
+ labelSize: 11,
+ labelFont: ['Noto Sans Regular'],
+ // Hit layer
+ hitWidth: 14,
+ },
+
+ // ── Highlight (boundary/selection) ────────────────────────────────────────
+ highlight: {
+ lineColor: "#7a9a6b", // Muted olive-green for dark backgrounds
+ lineWidth: 2,
+ lineDash: [4, 4],
+ lineOpacity: 0.8,
+ fillColor: "#7a9a6b",
+ fillOpacity: 0.08,
+ },
+}
+
+/**
+ * Light theme overlay configuration
+ * All hardcoded values from overlay add functions extracted here
+ */
+const lightOverlay = {
+ // ── Hillshade ─────────────────────────────────────────────────────────────
+ hillshade: {
+ exaggeration: 0.5,
+ illuminationDirection: 315,
+ shadowColor: '#000000',
+ highlightColor: '#ffffff',
+ },
+
+ // ── Traffic ───────────────────────────────────────────────────────────────
+ traffic: {
+ opacity: 0.6,
+ },
+
+ // ── Contours (main, brown/tan scheme) ─────────────────────────────────────
+ contours: {
+ opacityMod: 1.0,
+ minorColor: '#8b6f47',
+ minorOpacity: 0.4,
+ minorWidth: { z11: 0.5, z14: 1.0 },
+ intermediateColor: '#8b6f47',
+ intermediateOpacity: 0.7,
+ intermediateWidth: { z8: 0.8, z14: 1.2 },
+ indexColor: '#6b4f2a',
+ indexOpacity: 0.9,
+ indexWidth: { z4: 1.2, z14: 1.8 },
+ labelColor: '#5a4020',
+ labelHaloColor: '#ffffff',
+ labelHaloWidth: 1.5,
+ labelOpacity: 0.85,
+ labelSize: 10,
+ labelFont: ['Noto Sans Regular'],
+ },
+
+ // ── Contours Test (blue scheme) ───────────────────────────────────────────
+ // Missing keys cascade from contours
+ contoursTest: {
+ minorColor: '#4a7c9b',
+ intermediateColor: '#4a7c9b',
+ indexColor: '#2a5a7c',
+ labelColor: '#205080',
+ },
+
+ // ── Contours Test 10ft (green scheme) ─────────────────────────────────────
+ // Missing keys cascade from contours
+ contoursTest10ft: {
+ minorColor: '#3a7c4f',
+ intermediateColor: '#3a7c4f',
+ indexColor: '#2a5c3a',
+ labelColor: '#2a4030',
+ },
+
+ // ── Public Lands (PAD-US) ─────────────────────────────────────────────────
+ publicLands: {
+ opacityMod: 1.0,
+ // Fill colors per category
+ fillWA: '#7c6b2f',
+ fillNPS: '#3d6b1f',
+ fillUSFS: '#5a7c2f',
+ fillBLM: '#c4a672',
+ fillFWS: '#4a7a5a',
+ fillSTAT: '#5a8c7c',
+ fillLOC: '#8ca694',
+ fillDefault: '#a0a0a0',
+ // Fill base opacities (multiplied by opacityMod)
+ fillOpacityWA: 0.30,
+ fillOpacityNPS: 0.30,
+ fillOpacityUSFS: 0.25,
+ fillOpacityBLM: 0.20,
+ fillOpacitySTAT: 0.25,
+ fillOpacityLOC: 0.20,
+ fillOpacityDefault: 0.15,
+ // Outline colors per category
+ outlineWA: '#5a4d20',
+ outlineNPS: '#2a4a15',
+ outlineUSFS: '#3d5520',
+ outlineBLM: '#8a7343',
+ outlineFWS: '#2d5a3a',
+ outlineSTAT: '#3d6055',
+ outlineLOC: '#5c6e66',
+ outlineDefault: '#707070',
+ // Outline opacities
+ outlineOpacityNPS: 0.7,
+ outlineOpacityUSFS: 0.6,
+ outlineOpacityDefault: 0.5,
+ // Outline width
+ outlineWidth: { z4: 0.3, z8: 0.8, z12: 1.2 },
+ // Labels
+ labelColor: '#3a4a30',
+ labelHaloColor: '#ffffff',
+ labelHaloWidth: 1.5,
+ labelOpacity: 0.85,
+ labelSize: { z10: 10, z14: 13 },
+ labelFont: ['Noto Sans Regular'],
+ },
+
+ // ── USFS Trails ───────────────────────────────────────────────────────────
+ usfsTrails: {
+ // Roads
+ roadsColor: '#c09050',
+ roadsOpacity: 0.9,
+ roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 },
+ // Trails colors by use type
+ trailsMotorized: '#e07030',
+ trailsBicycle: '#d0a030',
+ trailsHiker: '#50b040',
+ trailsDefault: '#b09050',
+ trailsOpacity: 0.9,
+ trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
+ trailsDash: [2, 1.5],
+ // Road labels
+ roadsLabelColor: '#6a5a40',
+ roadsLabelHaloColor: '#ffffff',
+ roadsLabelHaloWidth: 1.5,
+ roadsLabelOpacity: 0.9,
+ roadsLabelSize: 11,
+ // Trail labels
+ trailsLabelColor: '#5a4a30',
+ trailsLabelHaloColor: '#ffffff',
+ trailsLabelHaloWidth: 1.5,
+ trailsLabelOpacity: 0.9,
+ trailsLabelSize: 11,
+ labelFont: ['Noto Sans Regular'],
+ // Hit layer
+ hitWidth: 14,
+ },
+
+ // ── BLM Trails / Roads ────────────────────────────────────────────────────
+ blmTrails: {
+ // Route colors by use class
+ color4wdHigh: '#e07030',
+ color4wdLow: '#d0a030',
+ colorAtv: '#d03030',
+ colorMotoSingle: '#a060b0',
+ color2wdLow: '#e0c060',
+ colorNonMech: '#50b040',
+ colorDefault: '#b09050',
+ colorSnow: '#6090c0',
+ lineOpacity: 0.9,
+ lineOpacityOther: 0.85,
+ lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
+ // Dash patterns by surface type
+ dashImproved: [4, 2],
+ dashAggregate: [1, 2],
+ dashSnow: [4, 2, 1, 2],
+ dashOther: [4, 2, 1, 2, 1, 2],
+ // Labels
+ labelColor: '#5a4a30',
+ labelHaloColor: '#ffffff',
+ labelHaloWidth: 1.5,
+ labelOpacity: 0.9,
+ labelSize: 11,
+ labelFont: ['Noto Sans Regular'],
+ // Hit layer
+ hitWidth: 14,
+ },
+
+ // ── Highlight (boundary/selection) ────────────────────────────────────────
+ highlight: {
+ lineColor: "#4a7040", // Forest green for light backgrounds
+ lineWidth: 2,
+ lineDash: [4, 4],
+ lineOpacity: 0.7,
+ fillColor: "#4a7040",
+ fillOpacity: 0.06,
+ },
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// THEME REGISTRY
+// ═══════════════════════════════════════════════════════════════════════════
+
+/**
+ * Theme registry - maps theme IDs to theme configurations
+ *
+ * Built-in themes (light/dark) use colors: null to signal that namedTheme()
+ * should be called at render time. Custom themes provide a full flavor object.
+ */
+const themes = {
+ light: {
+ id: 'light',
+ name: 'Light',
+ dark: false,
+ colors: null, // Use namedTheme('light')
+ satellite: null,
+ overlay: lightOverlay,
+ ui: lightUI,
+ swatch: ['#ddd2b9', '#4a7040', '#8a7556'],
+ fontImports: [],
+ },
+ dark: {
+ id: 'dark',
+ name: 'Dark',
+ dark: true,
+ colors: null, // Use namedTheme('dark')
+ satellite: null,
+ overlay: darkOverlay,
+ ui: darkUI,
+ swatch: ['#1c1917', '#7a9a6b', '#b8a88a'],
+ fontImports: [],
+ },
+ clean: {
+ ...cleanTheme,
+ swatch: ['#f5f5f5', '#1a73e8', '#34a853'],
+ fontImports: [],
+ },
cyberpunk: cyberpunkTheme,
- // Custom themes go here. Example:
- // 'midnight': {
- // id: 'midnight',
- // name: 'Midnight',
- // dark: true,
- // colors: { /* full flavor object matching dark-flavor-reference.json schema */ },
- // satellite: { opacity: 0.8, brightnessMin: 0.1 },
- // overlay: { /* partial overrides - missing keys fall back to dark overlay */ },
- // ui: { /* partial overrides - missing keys fall back to dark ui */ },
- // swatch: ['#0a0a12', '#6060ff', '#4040a0'],
- // fontImports: ['https://fonts.googleapis.com/css2?family=Orbitron&display=swap'],
- // },
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// EXPORTED FUNCTIONS
-// ═══════════════════════════════════════════════════════════════════════════
-
-/**
- * Get a theme configuration by ID
- * @param {string} id - Theme ID
- * @returns {object} Theme config, falls back to 'dark' if not found
- */
-export function getTheme(id) {
- return themes[id] || themes.dark
-}
-
-/**
- * Get the color flavor for a theme
- * For built-in themes, calls namedTheme(). For custom themes, returns colors directly.
- * @param {string} id - Theme ID
- * @returns {object} Flavor object for use with protomaps layers()
- */
-export function getThemeColors(id) {
- const theme = getTheme(id)
- if (theme.colors === null) {
- // Built-in theme - use namedTheme from protomaps-themes-base
- return namedTheme(id)
- }
- return theme.colors
-}
-
-/**
- * Get the sprite URL for a theme
- * Built-in themes use their own sprites. Custom themes fall back to
- * dark or light sprite based on the theme's dark flag.
- * @param {string} id - Theme ID
- * @returns {string} Full sprite URL
- */
-export function getThemeSprite(id) {
- const theme = getTheme(id)
- // Custom themes don't have matching sprites on CDN - fall back based on dark flag
- const spriteTheme = theme.colors === null ? id : (theme.dark ? 'dark' : 'light')
- return `https://protomaps.github.io/basemaps-assets/sprites/v4/${spriteTheme}`
-}
-
-/**
- * Get overlay configuration for a specific layer
- *
- * For contour variants (contoursTest, contoursTest10ft), missing keys cascade
- * from the same theme's contours config.
- *
- * For custom themes, missing keys fall back to the appropriate built-in theme
- * (dark or light based on theme.dark flag).
- *
- * @param {string} themeId - Theme ID
- * @param {string} layerKey - Overlay layer key (hillshade, contours, publicLands, etc.)
- * @returns {object} Merged overlay config for the layer
- */
-export function getOverlayConfig(themeId, layerKey) {
- const theme = getTheme(themeId)
- const builtinTheme = theme.dark ? themes.dark : themes.light
- const builtinOverlay = builtinTheme.overlay[layerKey] || {}
-
- // For contour variants, cascade from same theme's contours config
- let baseConfig = builtinOverlay
- if (layerKey === 'contoursTest' || layerKey === 'contoursTest10ft') {
- const contoursBase = builtinTheme.overlay.contours || {}
- baseConfig = { ...contoursBase, ...builtinOverlay }
- }
-
- // If this is a custom theme with overlay overrides, merge them
- if (theme.overlay && theme.overlay[layerKey]) {
- // For contour variants in custom themes, also cascade from custom contours
- if (layerKey === 'contoursTest' || layerKey === 'contoursTest10ft') {
- const customContours = theme.overlay.contours || {}
- return { ...baseConfig, ...customContours, ...theme.overlay[layerKey] }
- }
- return { ...baseConfig, ...theme.overlay[layerKey] }
- }
-
- return baseConfig
-}
-
-/**
- * Apply theme UI CSS custom properties to the document
- *
- * Sets the data-theme attribute AND applies all CSS variables from the
- * theme's ui object directly to document.documentElement.style.
- *
- * Also manages font imports: removes previously injected font tags
- * and injects new ones for the current theme's fontImports array.
- *
- * For custom themes, missing ui keys fall back to the appropriate built-in
- * theme (dark or light based on theme.dark flag).
- *
- * @param {object} theme - Theme config object (from getTheme())
- */
-export function applyThemeUI(theme) {
- const root = document.documentElement
-
- // Set data-theme attribute for any CSS selectors that still reference it
- root.setAttribute('data-theme', theme.id)
-
- // Get base UI config from appropriate built-in theme
- const builtinTheme = theme.dark ? themes.dark : themes.light
- const baseUI = builtinTheme.ui
-
- // Merge with any custom theme overrides
- const ui = theme.ui ? { ...baseUI, ...theme.ui } : baseUI
-
- // Apply all UI variables directly to root element style
- for (const [prop, value] of Object.entries(ui)) {
- root.style.setProperty(prop, value)
- }
-
- // Manage font imports
- // Remove any previously injected theme font links
- document.querySelectorAll('link[data-theme-font]').forEach(link => link.remove())
-
- // Inject new font links for this theme
- const fontImports = theme.fontImports || []
- for (const url of fontImports) {
- const link = document.createElement('link')
- link.rel = 'stylesheet'
- link.href = url
- link.setAttribute('data-theme-font', theme.id)
- document.head.appendChild(link)
- }
-}
-
-/**
- * Get list of available themes for UI display
- * @returns {Array<{id: string, name: string, dark: boolean, swatch: string[]}>}
- */
-export function themeList() {
- return Object.values(themes).map(({ id, name, dark, swatch }) => ({ id, name, dark, swatch }))
-}
-
-/**
- * Check if a theme ID is valid/registered
- * @param {string} id - Theme ID to check
- * @returns {boolean}
- */
-export function isValidTheme(id) {
- return id in themes
-}
-
-export default themes
+ // Custom themes go here. Example:
+ // 'midnight': {
+ // id: 'midnight',
+ // name: 'Midnight',
+ // dark: true,
+ // colors: { /* full flavor object matching dark-flavor-reference.json schema */ },
+ // satellite: { opacity: 0.8, brightnessMin: 0.1 },
+ // overlay: { /* partial overrides - missing keys fall back to dark overlay */ },
+ // ui: { /* partial overrides - missing keys fall back to dark ui */ },
+ // swatch: ['#0a0a12', '#6060ff', '#4040a0'],
+ // fontImports: ['https://fonts.googleapis.com/css2?family=Orbitron&display=swap'],
+ // },
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// EXPORTED FUNCTIONS
+// ═══════════════════════════════════════════════════════════════════════════
+
+/**
+ * Get a theme configuration by ID
+ * @param {string} id - Theme ID
+ * @returns {object} Theme config, falls back to 'dark' if not found
+ */
+export function getTheme(id) {
+ return themes[id] || themes.dark
+}
+
+/**
+ * Get the color flavor for a theme
+ * For built-in themes, calls namedTheme(). For custom themes, returns colors directly.
+ * @param {string} id - Theme ID
+ * @returns {object} Flavor object for use with protomaps layers()
+ */
+export function getThemeColors(id) {
+ const theme = getTheme(id)
+ if (theme.colors === null) {
+ // Built-in theme - use namedTheme from protomaps-themes-base
+ return namedTheme(id)
+ }
+ return theme.colors
+}
+
+/**
+ * Get the sprite URL for a theme
+ * Built-in themes use their own sprites. Custom themes fall back to
+ * dark or light sprite based on the theme's dark flag.
+ * @param {string} id - Theme ID
+ * @returns {string} Full sprite URL
+ */
+export function getThemeSprite(id) {
+ const theme = getTheme(id)
+ // Custom themes don't have matching sprites on CDN - fall back based on dark flag
+ const spriteTheme = theme.colors === null ? id : (theme.dark ? 'dark' : 'light')
+ return `https://protomaps.github.io/basemaps-assets/sprites/v4/${spriteTheme}`
+}
+
+/**
+ * Get overlay configuration for a specific layer
+ *
+ * For contour variants (contoursTest, contoursTest10ft), missing keys cascade
+ * from the same theme's contours config.
+ *
+ * For custom themes, missing keys fall back to the appropriate built-in theme
+ * (dark or light based on theme.dark flag).
+ *
+ * @param {string} themeId - Theme ID
+ * @param {string} layerKey - Overlay layer key (hillshade, contours, publicLands, etc.)
+ * @returns {object} Merged overlay config for the layer
+ */
+export function getOverlayConfig(themeId, layerKey) {
+ const theme = getTheme(themeId)
+ const builtinTheme = theme.dark ? themes.dark : themes.light
+ const builtinOverlay = builtinTheme.overlay[layerKey] || {}
+
+ // For contour variants, cascade from same theme's contours config
+ let baseConfig = builtinOverlay
+ if (layerKey === 'contoursTest' || layerKey === 'contoursTest10ft') {
+ const contoursBase = builtinTheme.overlay.contours || {}
+ baseConfig = { ...contoursBase, ...builtinOverlay }
+ }
+
+ // If this is a custom theme with overlay overrides, merge them
+ if (theme.overlay && theme.overlay[layerKey]) {
+ // For contour variants in custom themes, also cascade from custom contours
+ if (layerKey === 'contoursTest' || layerKey === 'contoursTest10ft') {
+ const customContours = theme.overlay.contours || {}
+ return { ...baseConfig, ...customContours, ...theme.overlay[layerKey] }
+ }
+ return { ...baseConfig, ...theme.overlay[layerKey] }
+ }
+
+ return baseConfig
+}
+
+/**
+ * Apply theme UI CSS custom properties to the document
+ *
+ * Sets the data-theme attribute AND applies all CSS variables from the
+ * theme's ui object directly to document.documentElement.style.
+ *
+ * Also manages font imports: removes previously injected font tags
+ * and injects new ones for the current theme's fontImports array.
+ *
+ * For custom themes, missing ui keys fall back to the appropriate built-in
+ * theme (dark or light based on theme.dark flag).
+ *
+ * @param {object} theme - Theme config object (from getTheme())
+ */
+export function applyThemeUI(theme) {
+ const root = document.documentElement
+
+ // Set data-theme attribute for any CSS selectors that still reference it
+ root.setAttribute('data-theme', theme.id)
+
+ // Get base UI config from appropriate built-in theme
+ const builtinTheme = theme.dark ? themes.dark : themes.light
+ const baseUI = builtinTheme.ui
+
+ // Merge with any custom theme overrides
+ const ui = theme.ui ? { ...baseUI, ...theme.ui } : baseUI
+
+ // Apply all UI variables directly to root element style
+ for (const [prop, value] of Object.entries(ui)) {
+ root.style.setProperty(prop, value)
+ }
+
+ // Manage font imports
+ // Remove any previously injected theme font links
+ document.querySelectorAll('link[data-theme-font]').forEach(link => link.remove())
+
+ // Inject new font links for this theme
+ const fontImports = theme.fontImports || []
+ for (const url of fontImports) {
+ const link = document.createElement('link')
+ link.rel = 'stylesheet'
+ link.href = url
+ link.setAttribute('data-theme-font', theme.id)
+ document.head.appendChild(link)
+ }
+}
+
+/**
+ * Get list of available themes for UI display
+ * @returns {Array<{id: string, name: string, dark: boolean, swatch: string[]}>}
+ */
+export function themeList() {
+ return Object.values(themes).map(({ id, name, dark, swatch }) => ({ id, name, dark, swatch }))
+}
+
+/**
+ * Check if a theme ID is valid/registered
+ * @param {string} id - Theme ID to check
+ * @returns {boolean}
+ */
+export function isValidTheme(id) {
+ return id in themes
+}
+
+export default themes