+
diff --git a/app/web/static/main.js b/app/web/static/main.js
index 91bb636..c811f29 100644
--- a/app/web/static/main.js
+++ b/app/web/static/main.js
@@ -173,4 +173,90 @@ document.addEventListener('DOMContentLoaded', function() {
localStorage.setItem('freemoto-roundtrip-norepeat', roundTripNoRepeat.checked ? '1' : '0');
});
}
+
+ // Swap start/end like Google Maps
+ (function(){
+ var swapBtn = document.getElementById('swapBtn');
+ if (!swapBtn) return;
+ function swapInputs() {
+ var s = document.getElementById('sourceInput');
+ var d = document.getElementById('destInput');
+ if (!s || !d) return;
+ // Swap visible values
+ var tmpVal = s.value; s.value = d.value; d.value = tmpVal;
+ // Swap lat/lon datasets
+ var sLat = s.dataset.lat, sLon = s.dataset.lon;
+ s.dataset.lat = d.dataset.lat || '';
+ s.dataset.lon = d.dataset.lon || '';
+ d.dataset.lat = sLat || '';
+ d.dataset.lon = sLon || '';
+ // If both look valid, trigger plot
+ var sourceLat = parseFloat(s.dataset.lat);
+ var sourceLon = parseFloat(s.dataset.lon);
+ var destLat = parseFloat(d.dataset.lat);
+ var destLon = parseFloat(d.dataset.lon);
+ if (!isNaN(sourceLat) && !isNaN(sourceLon) && !isNaN(destLat) && !isNaN(destLon)) {
+ var plot = document.getElementById('plotRouteBtn');
+ if (plot) plot.click();
+ }
+ }
+ swapBtn.addEventListener('click', swapInputs);
+ })();
+
+ // Recenter FAB behavior
+ (function(){
+ var recenter = document.getElementById('recenterBtn');
+ if (!recenter) return;
+ recenter.addEventListener('click', function(){
+ if (navigator.geolocation) {
+ navigator.geolocation.getCurrentPosition(function(position){
+ var lat = position.coords.latitude;
+ var lon = position.coords.longitude;
+ map.setView([lat, lon], Math.max(map.getZoom(), 14));
+ });
+ }
+ });
+ })();
+
+ // Mobile panel toggle (portrait)
+ try {
+ var panel = document.querySelector('.nav-panel');
+ var toggleBtn = document.getElementById('panelToggle');
+ function isSmallPortrait() {
+ var mqW = window.matchMedia('(max-width: 576px)');
+ var mqP = window.matchMedia('(orientation: portrait)');
+ return (mqW.matches && mqP.matches);
+ }
+ // Initialize collapsed state on load for small portrait
+ if (panel && isSmallPortrait()) {
+ panel.classList.add('collapsed');
+ if (toggleBtn) toggleBtn.setAttribute('aria-expanded', 'false');
+ }
+ // Auto-collapse panel when user hits Plot on small portrait
+ var plotBtn = document.getElementById('plotRouteBtn');
+ if (plotBtn) {
+ plotBtn.addEventListener('click', function(){
+ if (panel && isSmallPortrait()) {
+ panel.classList.add('collapsed');
+ if (toggleBtn) toggleBtn.setAttribute('aria-expanded', 'false');
+ }
+ });
+ }
+ if (toggleBtn && panel) {
+ toggleBtn.addEventListener('click', function() {
+ panel.classList.toggle('collapsed');
+ var expanded = !panel.classList.contains('collapsed');
+ toggleBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
+ });
+ }
+ // When orientation/viewport changes, ensure panel doesn't exceed screen
+ window.addEventListener('resize', function(){
+ if (!panel) return;
+ if (isSmallPortrait()) {
+ // Keep collapsed if it would cover map too much
+ if (!panel.classList.contains('collapsed')) panel.classList.add('collapsed');
+ if (toggleBtn) toggleBtn.setAttribute('aria-expanded', 'false');
+ }
+ });
+ } catch (_) {}
});
\ No newline at end of file
diff --git a/app/web/static/styles.css b/app/web/static/styles.css
new file mode 100644
index 0000000..fdb8d6a
--- /dev/null
+++ b/app/web/static/styles.css
@@ -0,0 +1,280 @@
+/* FreeMoto โ Fresh responsive UI
+ Author: Cascade
+*/
+
+:root {
+ --panel-radius: 16px;
+ --pill-radius: 999px;
+ --glass-bg: rgba(255, 255, 255, 0.9);
+ --glass-border: rgba(16, 24, 40, 0.08);
+ --brand-start: #0ea5e9;
+ --brand-end: #6366f1;
+ --text-muted: #64748b;
+ --shadow-1: 0 8px 24px rgba(2, 8, 20, 0.08);
+ --shadow-2: 0 10px 30px rgba(2, 8, 20, 0.12);
+}
+
+html, body {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ overflow: hidden;
+ background: linear-gradient(180deg, #f1f5f9, #e2e8f0);
+ color: #0f172a;
+}
+
+/* Map canvas */
+#map {
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+}
+
+/* Top App Bar */
+.appbar {
+ position: fixed;
+ inset: 0 0 auto 0;
+ z-index: 1002;
+ background: linear-gradient(90deg, var(--brand-start), var(--brand-end));
+ color: #fff;
+ box-shadow: var(--shadow-1);
+}
+.appbar .navbar-brand {
+ color: #fff;
+}
+.appbar .btn {
+ color: #fff;
+ border-color: rgba(255, 255, 255, 0.6);
+}
+.appbar .badge {
+ background: rgba(255, 255, 255, 0.15);
+}
+
+/* Floating Control Panel (mobile: bottom pill, desktop: floating card) */
+.nav-panel {
+ position: fixed;
+ left: 12px;
+ right: 12px;
+ bottom: 12px;
+ z-index: 1001;
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ backdrop-filter: blur(10px) saturate(160%);
+ border-radius: var(--pill-radius);
+ box-shadow: var(--shadow-2);
+ padding: 10px 12px;
+ transition: max-height 200ms ease, transform 200ms ease, padding 200ms ease;
+}
+.nav-panel .form-control,
+.nav-panel .btn {
+ border-radius: var(--pill-radius);
+}
+.nav-panel .input-group > .input-group-text {
+ border-radius: var(--pill-radius) 0 0 var(--pill-radius);
+}
+.section-title {
+ font-weight: 600;
+ color: #334155;
+}
+#routeInfoCard {
+ font-size: 0.95rem;
+}
+
+/* Suggestions dropdown */
+#sourceSuggestions,
+#destSuggestions {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ z-index: 2000;
+ max-height: 240px;
+ overflow: auto;
+ border-radius: 12px;
+}
+
+/* Directions bottom sheet */
+.sheet {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1001;
+}
+.sheet-card {
+ border-radius: var(--panel-radius) var(--panel-radius) 0 0;
+ max-height: 60vh;
+ overflow: hidden;
+ box-shadow: var(--shadow-2);
+}
+.sheet .handle {
+ width: 44px;
+ height: 5px;
+ border-radius: var(--pill-radius);
+ background: #cbd5e1;
+ margin: 6px auto 6px;
+}
+.sheet-header {
+ position: sticky;
+ top: 0;
+ background: #fff;
+ z-index: 1;
+ padding: 4px 8px;
+ border-bottom: 1px solid #eef2f6;
+}
+.sheet-body {
+ overflow: auto;
+ max-height: calc(60vh - 42px);
+}
+#directionsSheet { display: none; }
+.sheet.collapsed .sheet-card { max-height: 28vh; }
+.sheet.collapsed .sheet-body { max-height: calc(28vh - 42px); }
+
+/* Floating actions (mobile) */
+.floating-group {
+ position: fixed;
+ right: 14px;
+ bottom: 90px;
+ z-index: 1002;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.floating-group .fab,
+.floating-group .btn {
+ border-radius: 999px;
+ box-shadow: var(--shadow-2);
+}
+
+.floating-group .fab {
+ width: 48px;
+ height: 48px;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Toggle button for control panel on mobile */
+.panel-toggle {
+ position: fixed;
+ left: 14px;
+ bottom: 90px;
+ z-index: 1002;
+}
+
+/* Mobile portrait refinements */
+@media (max-width: 576px) and (orientation: portrait) {
+ .nav-panel {
+ left: 8px;
+ right: 8px;
+ bottom: 8px;
+ max-height: 48vh;
+ overflow: visible; /* allow suggestion dropdowns to overflow */
+ }
+ .nav-panel.collapsed {
+ max-height: 56px; /* show a small header */
+ padding: 8px 12px;
+ pointer-events: none; /* let map interactions pass through when collapsed */
+ }
+ .floating-group { bottom: 76px; }
+ .panel-toggle { bottom: 76px; }
+ .section-title { font-size: 0.95rem; }
+}
+
+/* Desktop layout refinements */
+@media (min-width: 768px) {
+ .nav-panel {
+ top: 88px;
+ bottom: auto;
+ left: 24px;
+ right: auto;
+ width: 560px;
+ border-radius: var(--panel-radius);
+ padding: 16px;
+ }
+ .nav-panel .form-control,
+ .nav-panel .btn {
+ border-radius: var(--panel-radius);
+ }
+ .nav-panel .input-group > .input-group-text {
+ border-radius: var(--panel-radius) 0 0 var(--panel-radius);
+ }
+}
+
+/* Dark theme */
+/* Improved dark mode for readability */
+[data-theme="dark"] :root {
+ --glass-bg: rgba(17, 24, 39, 0.92); /* slate-900 w/ opacity */
+ --glass-border: rgba(255, 255, 255, 0.06);
+ --text-muted: #9ca3af; /* slate-400 */
+}
+[data-theme="dark"] body {
+ background: linear-gradient(180deg, #0b1220, #0f172a);
+ color: #e5e7eb; /* slate-200 */
+}
+[data-theme="dark"] a { color: #93c5fd; }
+[data-theme="dark"] .appbar {
+ background: linear-gradient(90deg, #334155, #475569); /* slate gradient */
+ color: #e5e7eb;
+}
+[data-theme="dark"] .appbar .navbar-brand { color: #f1f5f9; }
+[data-theme="dark"] .appbar .btn {
+ color: #e5e7eb;
+ border-color: rgba(148, 163, 184, 0.7);
+}
+[data-theme="dark"] .nav-panel {
+ background: var(--glass-bg);
+ border-color: var(--glass-border);
+ box-shadow: 0 10px 30px rgba(0,0,0,0.55);
+}
+[data-theme="dark"] .section-title { color: #e2e8f0; }
+[data-theme="dark"] .form-control,
+[data-theme="dark"] .input-group-text {
+ background: #111827; /* gray-900 */
+ color: #e5e7eb;
+ border-color: #374151; /* gray-700 */
+}
+[data-theme="dark"] .form-control::placeholder { color: #9ca3af; opacity: 1; }
+[data-theme="dark"] .btn-outline-secondary { color: #e5e7eb; border-color: #93c5fd; }
+[data-theme="dark"] .btn-primary { background-color: #4f46e5; border-color: #4338ca; }
+[data-theme="dark"] .btn-success { background-color: #16a34a; border-color: #15803d; color: #f9fafb; }
+[data-theme="dark"] .btn-warning { background-color: #f59e0b; border-color: #d97706; color: #111827; }
+[data-theme="dark"] .badge,
+[data-theme="dark"] .badge.text-bg-light { background-color: #374151 !important; color: #f9fafb !important; }
+[data-theme="dark"] #summaryPill.badge { background-color: #1f2937 !important; color: #f9fafb !important; border: 1px solid #334155; }
+[data-theme="dark"] .card,
+[data-theme="dark"] .sheet-card { background: #0f172a; color: #e5e7eb; border-color: #334155; }
+[data-theme="dark"] .list-group-item { background: #0b1220; color: #e5e7eb; border-color: #334155; }
+[data-theme="dark"] .list-group-item-action:hover { background-color: #111827; color: #ffffff; }
+[data-theme="dark"] .sheet-header { background: #0b1220; border-bottom-color: #334155; }
+[data-theme="dark"] .handle { background: #475569; }
+[data-theme="dark"] .alert { background-color: #0f172a; color: #e5e7eb; border-color: #334155; }
+[data-theme="dark"] .alert-info { background-color: #0b1e2b; border-color: #1f3a5f; color: #dbeafe; }
+[data-theme="dark"] .alert-secondary { background-color: #1f2937; border-color: #374151; color: #e5e7eb; }
+
+/* Dark accordion */
+[data-theme="dark"] .accordion-item { background-color: #0b1220; border: 1px solid #334155; }
+[data-theme="dark"] .accordion-button { background-color: #0b1220; color: #e5e7eb; }
+[data-theme="dark"] .accordion-button:not(.collapsed) { background-color: #0f172a; color: #ffffff; box-shadow: inset 0 -1px 0 #334155; }
+[data-theme="dark"] .accordion-button:focus { box-shadow: 0 0 0 .2rem rgba(147, 197, 253, 0.25); border-color: #93c5fd; }
+[data-theme="dark"] .accordion-body { background-color: #0f172a; color: #e5e7eb; }
+
+/* Outline button hover/focus in dark */
+[data-theme="dark"] .btn-outline-secondary:hover,
+[data-theme="dark"] .btn-outline-secondary:focus { background-color: rgba(147, 197, 253, 0.12); color: #e5e7eb; border-color: #93c5fd; }
+
+/* Range sliders in dark mode */
+[data-theme="dark"] input[type="range"] { accent-color: #93c5fd; }
+[data-theme="dark"] .form-range::-webkit-slider-thumb { background-color: #93c5fd; }
+[data-theme="dark"] .form-range::-moz-range-thumb { background-color: #93c5fd; }
+[data-theme="dark"] .form-range::-ms-thumb { background-color: #93c5fd; }
+
+/* Suggestions dropdown contrast */
+[data-theme="dark"] #sourceSuggestions .list-group-item,
+[data-theme="dark"] #destSuggestions .list-group-item { background-color: #0f172a; color: #e5e7eb; border-color: #334155; }
+[data-theme="dark"] #sourceSuggestions .list-group-item:hover,
+[data-theme="dark"] #destSuggestions .list-group-item:hover { background-color: #111827; color: #ffffff; }
diff --git a/docker-compose.yml b/docker-compose.yml
index ae83e73..ce45ba6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -26,6 +26,6 @@ services:
- "8002:8002"
volumes:
# Mount a host directory for custom files and tile caching if desired
- - ${PWD}/custom_files:/custom_files
+ - ./custom_files:/custom_files
environment:
- tile_urls=https://download.geofabrik.de/europe/germany-latest.osm.pbf