Commit graph

47 commits

Author SHA1 Message Date
f788aefae6 Filter out urban roads from BLM layer
- Exclude ARTERIAL and COLLECTOR functional classes
- Exclude any route with HWY_CLASS (highways)
- Keeps RESOURCE, LOCAL, and UNKNOWN in rural areas

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-30 20:58:58 +00:00
e2d5ca9718 Style BLM routes by surface type and vehicle class
- Filter out SOLID SURFACE (paved roads)
- Color by route use class:
  - Orange: 4WD High Clearance
  - Tan: 4WD Low
  - Red-brown: ATV
  - Dark red: Motorized Single Track
  - Light tan: 2WD Low
  - Green: Non-Mechanized
- Line pattern by surface type:
  - Solid: Natural (dirt)
  - Dashed: Natural Improved (graded)
  - Dotted: Aggregate (gravel)
  - Dash-dot: Snow
  - Dash-dot-dot: Other/Unknown
- Snow routes use distinct blue color

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-30 20:53:38 +00:00
efebf74522 Add BLM roads/trails layer
- Add BLM routes vector source from pmtiles
- Style: dashed olive/sage lines, distinct from USFS
- Labels at zoom 12+ using ROUTE_PRMRY_NM
- Hit-area layer (14px) for click targets
- Popup showing route name, asset class, transport, surface, length
- "BLM Roads" toggle in Layer Control panel
- Feature-flag gated: has_blm_trails (default false)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-30 20:40:44 +00:00
e1d56ff925 feat(map): add invisible hit-area layers for USFS trails/roads
- Add 14px wide transparent hit layers below visible styled layers
- Click events now target hit layers for easier selection
- Cursor changes to pointer on hover over hit layers
- Keeps visual styling thin while providing fat click targets

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-30 17:30:39 +00:00
2b90f8b17a feat(map): add USFS trails and roads layer
- Add USFS trails/roads as toggleable map layer via PMTiles
- Trails: dashed brown lines, roads: solid khaki lines
- Labels at zoom 12+ for trail and road names
- Click handler shows popup with trail/road info
- Feature-flag gated with has_usfs_trails (default false)
- Add Trails toggle to Layer Control panel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-30 16:43:30 +00:00
e53ff561e8 feat(keyboard): add Escape key to close/deselect place card
Pressing Escape when a place is selected now:
- Closes the place card
- Clears the selected place state
- Removes the boundary outline from the map
- Clears the selected label highlight

Same behavior as clicking empty map area.
2026-04-30 03:41:22 +00:00
a1a499de07 style(labels): improve label readability with Google Maps-style halos
Apply solid opaque halos to interactive label layers for clean knockout
against any background (parks, water, terrain):

BASE (unhighlighted):
- Text: #2a2a2a (light) / #e0e0e0 (dark)
- Halo: solid white/black, 0.9 opacity, 1.8px width
- Acts as background knockout, not decoration

HOVER:
- Text: pure black/white for focus
- Halo: full opacity (1.0), 2px width
- Subtle emphasis without glowing effect

SELECTED:
- Text: accent color (theme green)
- Halo: solid white/black, full opacity, 2.2px width
- Clear visual distinction for clicked item

Key insight: halo is a readability tool, not visual effect.
Keep it tight, opaque, and matching background intent.
2026-04-30 03:37:04 +00:00
5010b45a7c fix(highlight): use data-driven expressions to target specific features
Instead of changing entire layer paint properties (which highlights all
labels in the layer), use MapLibre case expressions to target only the
specific feature by name. This prevents highlighting ALL labels when
hovering/selecting one.

Expression format:
  ["case", ["==", ["get", "name"], featureName], highlightColor, originalColor]

Fixes text duplication at z14+ on small places.
2026-04-30 03:07:43 +00:00
39996fdafe fix(map): boundary fill opacity and highlight text duplication
Two fixes for rendering at close zoom (z14+):

1. Boundary rendering:
   - Add subtle fill layer with 0.05 opacity (barely visible tint)
   - Insert fill and outline layers BELOW symbol layers using
     firstSymbolId so labels render on top
   - Dashed outline remains primary indicator

2. Highlight text duplication:
   - Remove separate hover-hl-* and selected-hl-* symbol layers
     that were creating duplicate/ghost text
   - Instead, modify ORIGINAL layer paint properties directly using
     setPaintProperty on text-color, text-halo-color, text-halo-width
   - Store original paint values for restoration on clear
   - Single symbol layer per label = no duplication

Test at z14+ on small places like Rock Creek Park - no text ghosting.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-30 03:03:18 +00:00
68f0a8dff7 fix(map): atomic boundary transition when switching places
Remove the clear-then-set pattern when clicking a new labeled feature.
Previously, clicking Kimberly after Twin Falls would:
1. Clear Twin Falls boundary
2. Set new place
3. Wait for API to return
4. Set Kimberly boundary

This caused the old boundary to disappear before the new one was ready,
requiring two clicks.

Now when clicking a new labeled feature:
1. Set new place immediately
2. When API returns, updateBoundary(newData) replaces old data in-place

The GeoJSON source only holds one dataset - setting new data naturally
replaces old data. Explicit clear only needed when deselecting or
clicking empty map.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-30 01:43:19 +00:00
ac7cec972f fix(map): call updateBoundary directly, remove useEffect
The useEffect-based boundary rendering was unreliable due to React's
state lifecycle - the effect would fire before boundary data arrived
from the API, then not re-trigger properly when data was populated.

New approach:
- Remove the boundary useEffect entirely
- Define updateBoundary function in map load handler
- Store function reference in Zustand store and local ref
- PlaceCard calls updateBoundary(geometry) directly when API returns
- Click handlers call updateBoundary(null) to clear

This bypasses React's render cycle - the map library handles its own
state and we tell it what to draw when we have the data.

Test sequence:
- Click Twin Falls → boundary shows on first click
- Click Kimberly → boundary shows on first click
- Switch between them → old clears, new shows
- Click empty map → boundary clears

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 23:23:11 +00:00
b657c0f000 fix(map): re-trigger boundary render when API data arrives
The boundary useEffect was only triggered when selectedPlace changed,
but boundary data arrives asynchronously from /api/place fetch. By the
time fetchPlaceDetails completed and enriched selectedPlace with
boundary, the useEffect had already fired and saw no boundary.

Fix: add selectedPlace?.boundary to the dependency array so the effect
re-runs when boundary data is populated by the API response.

Test sequence:
- Click Twin Falls → boundary shows on first click
- Click Kimberly → boundary shows on first click (was broken)
- Click empty map → boundary clears
- Click Twin Falls again → boundary shows on first click

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 23:12:37 +00:00
29f995d885 fix(map): boundary lifecycle cleanup on place deselection
Clear boundary source data when:
- Clicking outside the marker circle (deselecting)
- Clicking a new place (before setting new boundary)
- Clicking empty map area (generic map click)

This ensures:
1. Re-clicking the same city after dismissing re-renders boundary
   (selectedPlace is properly cleared, triggering fresh useEffect)
2. Clicking away doesn't leave stale boundary outline
   (boundary source explicitly cleared to empty FeatureCollection)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 23:07:41 +00:00
472ef4d0e8 fix(map): resolve initialization crashes and boundary rendering
Fixes three critical bugs:

1. CRASH: Guard addSource/addLayer calls with existence checks to
   prevent "Source already exists" errors in React strict mode
   double-mount scenarios

2. BOUNDARY: Wrap boundary update logic in a function and properly
   handle async style loading - check isStyleLoaded() and use
   map.once('load') as fallback

3. FONTS: Use 'Noto Sans Regular' for highlight layers instead of
   'Noto Sans Medium'/'Noto Sans Bold' which 404 on protomaps CDN

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 22:47:24 +00:00
4edc453835 fix(map): use filter-based highlights for PMTiles compatibility
Replace feature-state based highlighting with filter-based approach
using dedicated highlight layers. PMTiles don't have feature IDs,
causing setFeatureState to silently fail. The new approach:

- Creates hover-hl-* and selected-hl-* layers per source-layer
- Uses EMPTY_FILTER to hide layers by default
- Updates filter to match feature name when highlighting
- Preserves all existing functionality (zoom, boundary, place card)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 22:33:13 +00:00
b381d8d97f feat(map): add hover and click highlight for interactive features
- Add visual hover feedback on map labels (cities, POIs, regions)
  using MapLibre feature-state paint property expressions
- Hover: cursor pointer + brighter text + subtle halo glow
- Click highlight: accent color text + accent glow halo
- Highlight persists until place card closed or different feature clicked
- Remove DOM overlay marker for feature mode (use native paint instead)
- Consolidate interactive layers into INTERACTIVE_LAYERS constant
- Re-apply styles on theme change

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 21:46:35 +00:00
a1f929e10a fix: measure tool — multi-point with segment distances and running total 2026-04-28 23:41:09 +00:00
99bd2218a4 feat: geocode search + map pick for contact location 2026-04-28 23:05:28 +00:00
1f448b273f feat: wire radial menu — directions, add stop, save, measure distance 2026-04-28 22:39:42 +00:00
0ba8911100 Fix button wrapping and add scale bar control
- Add whiteSpace: nowrap to Get Directions button to prevent text wrap
- Add ScaleControl (imperial units) to bottom-right of map
- Add dark theme styling for scale bar

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-28 22:06:35 +00:00
0c5f9f47d0 fix: restore RadialMenu — incorrectly disabled 2026-04-27 06:15:26 +00:00
6bf8717803 fix: route polyline on first route — use idle event instead of load
The load event only fires once during map init. When route state
changes before style is fully loaded, the once(load) handler
never executes because load already fired. idle fires after every
render cycle, ensuring the route draws once the style is ready.
2026-04-27 05:01:39 +00:00
53d25dafd8 fix: final cleanup — disable radial menu, document auth pattern
B14: Comment out RadialMenu render — all 5 wedges show "coming soon"
     toasts with no functional actions. Code preserved for when
     actions are wired.

B5:  Add detailed JSDoc to fetchAuthState explaining the
     redirect:manual pattern and its dependencies on Caddy/Authentik
     configuration.

B9:  Investigated — user-select CSS errors come from MapLibre's own
     maplibre-gl.css, not our code. WONTFIX (library issue).

Caddy changes (CT 101):
B6:  Fixed recon.echo6.co header stripping bug — same pattern as navi
B8:  Added /api/traffic/* to @authed_user (TomTom API must be authed)
2026-04-27 04:11:34 +00:00
fe77c6d459 fix: route polyline visibility + login URL
B1: Add ResizeObserver to MapView.jsx to handle layout settling.
The map canvas had 0 height at init because layout hadnt settled
when the useEffect fired. ResizeObserver calls map.resize() on
any container size change.

B11: Use native outpost start URL for login initiation:
/outpost.goauthentik.io/start?rd=%2F
This properly triggers auth flow and redirects to / after login.
Removed the Caddy /login handler that wasnt redirecting correctly.
2026-04-27 03:51:34 +00:00
a40f68fa26 fix: resolve 5 confirmed bugs from code review
- MapView.jsx: extract addBoundaryLayer function, use getComputedStyle
  for accent color (MapLibre rejects CSS vars in paint properties)
- PlaceCard.jsx: gate fetchNearbyContacts on auth.authenticated
- PlaceDetail.jsx: gate fetchNearbyContacts on auth.authenticated
- api.js: replace invalid timeout option with AbortSignal.timeout()
- RadialMenu.jsx: remove user-select from SVG style (Firefox rejects)
- Panel.jsx: add Cancel button for pending directions state
2026-04-27 02:50:46 +00:00
f5e0b9606e fix: search viewport init, theme-clears-route bug, preview-during-route
Three regressions fixed:

1. mapCenter is now initialized on map 'load' event, not just 'moveend'.
   Searches immediately after page load now correctly include viewport
   bias instead of falling back to default Twin Falls coords.

2. setThemeOverride had stray code from startDirections that wiped
   stops and added undefined place data. Toggling theme cleared the
   active route. Restored setThemeOverride to its correct
   theme-only implementation.

3. usePanelState returned ROUTE_CALCULATED before checking selectedPlace,
   so preview cards could never appear alongside a calculated route.
   Refactored to decouple preview state from route state - preview
   renders whenever selectedPlace exists, independent of route state.
2026-04-26 22:21:23 +00:00
5eb83e9b4b feat(panel): single-panel architecture with UX refinements
Major refactor consolidating two-panel layout (Routes/Contacts + floating
PlaceDetail) into one 400px left column with state-driven content.

Architecture:
- New PlaceCard component for preview and stop cards (collapsible)
- Panel states: IDLE, PREVIEW, ROUTING, PREVIEW_ROUTING, ROUTE_CALCULATED
- usePanelState selector in store.js derives state from selectedPlace/stops/route
- StopList now renders stops as PlaceCard with variant=stop
- PlaceDetail.jsx removed from App.jsx (content moved to PlaceCard)

UX refinements:
- Panel width 400px (was 360px) to fit buttons on one line
- Map zoom padding updated to 420px for wider panel
- Body text bumped to text-sm (14px) for readability
- Get Directions button hidden when 2+ stops (route auto-calculates)
- PlaceCard title prefers feature name (raw.name) over formatted address
- Preview card shows above route during PREVIEW_ROUTING state
- Directions flow no longer shows toast when GPS denied
2026-04-26 21:14:39 +00:00
d0f89c6783 feat(map): polygon boundary, zoom-to-feature, Wikidata link cleanup
Three improvements:

1. When a place has a boundary polygon (from Nominatim), render its
   outline on the map using a dashed accent line. Falls back to the
   existing pulsing ring for places without polygons.

2. Selecting a feature now smoothly zooms the map to fit:
   - With polygon: fitBounds to polygon bbox
   - Without polygon: zoom level based on feature kind (city=11,
     region=7, POI=16, etc.)
   Terrain clicks do not change zoom.

3. Wikidata IDs render as styled 'View on Wikidata' links instead
   of raw 'Wikidata: Qxxxxx' strings.
2026-04-26 08:26:56 +00:00
b354fd0aa0 feat: Consolidated UX improvements for map selection
- Snap selection to feature geometry when clicking labeled places
- Add wikidata enrichment for basemap labels (population, description)
- Differentiate visual feedback: reticle marker vs pulsing highlight
- Clear previous feature highlight when selection changes
- Store selection mode (reticle/feature) and feature info in state

Frontend: MapView click handler, PlaceDetail wikidata fetch, CSS
Backend: /api/place/wikidata/<id> route for Wikidata API lookups

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-26 08:15:09 +00:00
4fcc3d1af4 fix(map): resolve clicks to rendered label features
Click handler now queries rendered features at click point and uses
the highest-priority labeled feature (POI > locality > region) as
the place identifier. Previously, clicks went straight to coord-based
reverse-geocode, causing 'Twin Falls' label clicks to resolve to
whatever feature was nearest (e.g., a radio tower).

Falls back to reverse-geocode when no labeled feature exists at the
click point. Two-click selection model and hover affordance unchanged.
2026-04-26 07:49:08 +00:00
985221e8cd feat(map): add POI and label hover affordance
POIs, city names, and other labeled features now show a pointer cursor
on hover, signaling that they are clickable. Matches the standard
map-app interaction pattern (Google Maps, etc.).

Implemented via MapLibre mouseenter/mouseleave handlers for interactive
layers: pois, places_locality, places_region, places_country,
places_subplace.
2026-04-26 07:28:53 +00:00
37a5eb5b1b feat(map): two-click selection model with precise center dot
Replaces 'every click selects something' with a deliberate two-click
flow:
- First click drops marker (existing circle plus new precise center dot)
  and opens place panel
- Second click INSIDE the marker circle opens the radial menu
- Second click OUTSIDE the circle deselects without selecting the new
  spot — requires another click to select

The 4px filled center dot at exact click coordinates gives precise
visual feedback for GPS-coord readout. The existing circle's radius
defines the same-spot tolerance, visually showing the radial-trigger
hit area.

Right-click radial unchanged. Search-dropdown selection drops a marker
for consistency.
2026-04-26 07:17:33 +00:00
19b8c6e23b feat(radial): remove redundant 'What's here' wedge
Left-click on the map already reverse-geocodes and opens the place
panel, making the 'What's here' wedge redundant. Radial drops from
6 to 5 wedges (72° each).

Remaining wedges (all stubs except they show toast):
- Drop pin
- Directions to here
- Save place (auth-gated)
- Add as stop
- Directions from here
2026-04-26 06:44:12 +00:00
2e975ea59e feat(map): add radial context menu with reverse-geocode action
Implements RadialMenu component (general-purpose, configurable wedges)
and useContextMenu hook (right-click on desktop, 450ms long-press with
8px movement threshold on touch).

First wired action: "What's here" — reverse-geocodes the trigger
location and opens the place panel for the result. Remaining wedges
(Drop pin, Directions from here, Directions to here, Add as stop,
Save place) render but stub to a toast — wiring deferred to follow-up
sessions.

Per design doc NAVI-DIRECTIONS-REDESIGN.md sections covering Phases a
and b of the implementation sequence.
2026-04-26 05:42:48 +00:00
b12ebe672e feat(search): add viewport bias for location-aware geocoding
- Add mapCenter state to store (lat/lon/zoom)
- Track map center on moveend in MapView
- Pass mapCenter to searchGeocode from SearchBar
- Update searchGeocode API call to include viewport params

Search results now prioritize locations near the current map view.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-26 04:03:52 +00:00
ec6f4254b9 fix: zoom indicator now updates live via dedicated useEffect
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-26 00:20:31 +00:00
6db09fd3d3 fix: add z-index to zoom indicator for visibility
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-26 00:07:54 +00:00
3f461dd9a8 feat: add zoom level indicator to map UI
Adds a persistent zoom indicator pill in the bottom-left corner showing
current zoom level (e.g., "Z 11.4"). Updates live as user pans/zooms.

Also includes contour test layer support (blue color scheme) for
development verification of contour pipeline changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-26 00:03:37 +00:00
f6cbf5f2cc feat: add contours layer with tier-aware zoom visibility
- Adds contour PMTiles vector source (contours-na.pmtiles)
- Minor/intermediate/index tier rendering at z11+/z8+/z4+
- Elevation labels on index contours at z12+
- Dark theme opacity adjustment
- has_contours feature flag gated

Completes T pipeline integration (Phase 1).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-24 00:44:20 +00:00
cdd11dc043 Add Public Lands vector tile overlay layer (PAD-US)
Toggleable vector tile overlay rendering 651k PAD-US protected areas
as colored polygons on the map. Data-driven styling by agency/designation
(USFS green, NPS darker green, BLM tan, wilderness amber, state teal).
Unit name labels at z10+. Feature-flagged via has_public_lands_layer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 18:52:53 +00:00
03e9780834 fix: mobile UX — GPS permission flow, traffic toggle, overflow
GPS permission:
- Remove silent mount-time getCurrentPosition calls that cause iOS Safari
  to cache a "denied" state without ever prompting the user
- LocateButton always retries getCurrentPosition on tap (user gesture)
- Only show "denied" toast on PERMISSION_DENIED (code 1), not timeout
- MapView watchPosition now starts only after confirmed grant, not unconditionally

Traffic toggle:
- Fix isStyleLoaded() race in LayerControl — if style not loaded when
  toggle fires, defer to map.once("style.load") instead of silently bailing
- Change outside-click handler from mousedown to pointerdown for mobile

Mobile UX (from prior session):
- Add LocateButton component (crosshair GPS locate/re-center)
- Reposition layer control + locate button to top-right on mobile
  (below MapLibre nav controls, above bottom sheet)
- ModeSelector: add min-w-0 to prevent flex overflow at 390px
- StopItem: remove button visible on touch (60% opacity vs hover-only)
- Panel: overflow-x-hidden + safe-area-inset-bottom on mobile sheet
- Body overflow-x guard on mobile viewports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 03:27:21 +00:00
4020d5ae0a Add hillshade and traffic overlay layers with layer control UI
- New LayerControl component with popover toggles for hillshade/traffic
- MapView: add/remove hillshade raster-dem and traffic raster layers
- Overlay layers persist in localStorage, survive theme swaps
- Hillshade defaults ON, traffic defaults OFF when available

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 00:52:20 +00:00
edc5a9788d feat(navi): config-driven tile source, defaults, and feature flags
Load deployment config from /api/config on startup:
- src/config.js: loader with 3s timeout + hardcoded fallback
- src/hooks/useConfig.js: useConfig() and useFeature() hooks
- MapView.jsx: tile URL, attribution, center, zoom from config
- main.jsx: loads config before first render

Falls back to home profile defaults if backend unavailable.
No visible behavior change — infrastructure for future features.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 23:36:02 +00:00
Ubuntu
a819458865 feat(navi): copy popover + map-click pin drop with reverse geocode
Replaces Share button with Copy dropdown (Address / Coordinates).
Map single-click drops preview pin, opens PlaceDetail with
"Dropped pin" placeholder, reverse geocodes via /api/reverse to
fill in address. Stop-pin clicks preserved via flag ref. Escape
key closes PlaceDetail. Double-click zoom unaffected.

Phase 3 Step 5 C1.5 of Navi.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 21:27:07 +00:00
Ubuntu
02f2b25db3 feat(navi): GPS origin + place detail panel + basic actions
Adds synthetic "Your location" stop A when GPS granted; place
detail panel slides in on search result click with Directions /
Add stop / Save (stub) / Share actions; elevation via Valhalla
/height; react-hot-toast for feedback; pendingDestination state
for GPS-denied Directions flow.

Phase 3 Step 5 C1 of Navi.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 20:59:18 +00:00
Ubuntu
6983e2655b feat: switch to NA tileset + browser geolocation
Swap tile source from idaho.pmtiles (427 MB, Idaho-only) to na.pmtiles
(31 GB, CONUS + Canada + Mexico + Alaska). Replace MapLibre GeolocateControl
with browser geolocation API that flies to user location on load and stores
coordinates in Zustand for future use.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 18:43:36 +00:00
Ubuntu
e7b08a7dc9 feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints

Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00