diff --git a/README.md b/README.md
index ceec1db..4b0f277 100644
--- a/README.md
+++ b/README.md
@@ -56,8 +56,9 @@ Ordnerinhalt auf einen Webserver (nginx, Apache, Vercel, o.ä.) legen.
- **Abfahrten**: Live-Abfahrtstafel (VBB + echte DB-Bahnhöfe), Verkehrsmittel-Filter,
Gleiswechsel-Hinweis, Störungen & Aufzugsstatus direkt unter der Tafel
-- **Route**: Routenplanung über eine selbst gehostete OTP2-Instanz, konfigurierbarer
- Umstiegszeit-Puffer, rollstuhlgerechte Verbindungen, Route teilen (Link)
+- **Route**: Routenplanung über HAFAS (VBB + Deutsche Bahn, dieselbe Anbindung wie die
+ iOS-App), bis zu 3 Zwischenhalte, Verkehrsmittel-Filter, Fahrrad-Option,
+ rollstuhlgerechte Verbindungen, Früher/Später-Nachladen, Route teilen (Link)
- **Karte**: Live-Fahrzeugpositionen, Fahrt isolieren/verfolgen, display-breite
Darstellung auf jeder Bildschirmgröße
- **Favoriten**: eigener Tab, Live-Vorschau der nächsten Abfahrt, Störungs-Badges
diff --git a/index.html b/index.html
index d84451c..dcf8055 100644
--- a/index.html
+++ b/index.html
@@ -130,29 +130,35 @@
+
+
+
+
+
+
+
-
-
-
-
- 🛰 Routenplanung über unsere selbst gehostete, offene Instanz (VBB-Region)
+ 🛰 Routenplanung mit Echtzeitdaten von VBB & Deutscher Bahn
+
+
+
+
diff --git a/js/api.js b/js/api.js
index 92841e8..183ed6c 100644
--- a/js/api.js
+++ b/js/api.js
@@ -102,25 +102,52 @@ const API = (() => {
},
/**
- * Nutzt unsere selbst gehostete OTP2-Instanz (VBB-Region) statt HAFAS.
- * `from`/`to` brauchen nur noch { lat, lon, name } - egal ob Haltestelle,
- * Adresse, POI oder GPS-Standort, alle liefern das bereits mit.
+ * Nutzt dieselbe HAFAS-Anbindung (VBB + Deutsche Bahn) wie die iOS-App.
+ * `from`/`to` brauchen { lat, lon, name }. `vias` ist eine Liste von bis
+ * zu 3 Zwischenhalten ({ lat, lon, name, waitMinutes }), in Reihenfolge
+ * der Route. Sobald mindestens ein Via gesetzt ist, verkettet das
+ * Backend mehrere Einzelabfragen - dann gelten immer "Abfahrt um" sowie
+ * feste results=1 pro Abschnitt, und earlierRef/laterRef liefern null
+ * zurück (siehe routes/journeys.js im Backend).
+ * `earlierRef`/`laterRef` laden, aus einer vorherigen Antwort übernommen,
+ * die nächste bzw. vorherige Seite an Verbindungen nach (ohne Vias).
+ * Gibt die volle Antwort zurück ({ journeys, earlierRef, laterRef }).
*/
- getJourneys({ from, to, when, arrival, polylines, results = 5, transferSlack, wheelchair }) {
- return request(
- `/otp/journeys?${qs({
- fromLat: from.lat,
- fromLon: from.lon,
- toLat: to.lat,
- toLon: to.lon,
- when: when || undefined,
- arrival: arrival ? 'true' : undefined,
- polylines: polylines ? 'true' : undefined,
- results,
- transferSlack,
- wheelchair: wheelchair ? 'true' : undefined,
- })}`,
- );
+ getJourneys({
+ from, to, when, arrival, polylines, results = 5,
+ wheelchair, modes, bike, vias = [], earlierRef, laterRef,
+ }) {
+ const params = {
+ fromLat: from.lat,
+ fromLon: from.lon,
+ fromName: from.name || undefined,
+ toLat: to.lat,
+ toLon: to.lon,
+ toName: to.name || undefined,
+ results,
+ polylines: polylines ? 'true' : undefined,
+ accessibility: wheelchair ? 'true' : undefined,
+ modes: modes || undefined,
+ bike: bike ? 'true' : undefined,
+ };
+
+ if (earlierRef) {
+ params.earlierRef = earlierRef;
+ } else if (laterRef) {
+ params.laterRef = laterRef;
+ } else if (when) {
+ params.when = when;
+ if (arrival) params.arrival = 'true';
+ }
+
+ vias.slice(0, 3).forEach((via, i) => {
+ params[`via${i}Lat`] = via.lat;
+ params[`via${i}Lon`] = via.lon;
+ if (via.name) params[`via${i}Name`] = via.name;
+ if (via.waitMinutes) params[`via${i}WaitMinutes`] = via.waitMinutes;
+ });
+
+ return request(`/journeys?${qs(params)}`);
},
getFares() {
diff --git a/js/app.js b/js/app.js
index dc9f36e..a2d6913 100644
--- a/js/app.js
+++ b/js/app.js
@@ -3,18 +3,24 @@ const App = (() => {
const LAST_STOP_KEY = 'oepnv-navi:db-last-stop';
const RECENT_SEARCHES_KEY = 'oepnv-navi:db-recent-searches';
const SEEN_DISRUPTIONS_KEY = 'oepnv-navi:db-seen-disruptions';
- const TRANSFER_SLACK_KEY = 'oepnv-navi:db-transfer-slack';
const WHEELCHAIR_KEY = 'oepnv-navi:db-wheelchair';
+ const BIKE_KEY = 'oepnv-navi:db-bike';
const FAVORITES_POLL_MS = 30000;
+ const MAX_VIAS = 3;
+ const VIA_WAIT_OPTIONS = [0, 5, 10, 15, 20, 30, 45, 60];
let currentStop = null;
let departuresPollHandle = null;
let departuresModeFilter = null;
+ let journeyModeFilter = null;
let favoritesPollHandle = null;
let journeyFrom = null;
let journeyTo = null;
let journeyArrivalMode = false;
+ let journeyVias = []; // { lat, lon, name, waitMinutes }
+ let journeyEarlierRef = null;
+ let journeyLaterRef = null;
let lastJourneyParams = null;
let remarksLoadedOnce = false;
@@ -1185,17 +1191,6 @@ const App = (() => {
setupJourneyInput('journey-from', (loc) => (journeyFrom = loc));
setupJourneyInput('journey-to', (loc) => (journeyTo = loc));
- // Zuletzt gewählten Umstiegszeit-Puffer wiederherstellen, damit die
- // Einstellung nicht bei jedem Besuch neu gesetzt werden muss.
- const transferSlackSelect = document.getElementById('journey-transfer-slack');
- if (transferSlackSelect) {
- const saved = localStorage.getItem(TRANSFER_SLACK_KEY);
- if (saved !== null) transferSlackSelect.value = saved;
- transferSlackSelect.addEventListener('change', () => {
- localStorage.setItem(TRANSFER_SLACK_KEY, transferSlackSelect.value);
- });
- }
-
// Ebenso für "nur rollstuhlgerechte Verbindungen".
const wheelchairToggle = document.getElementById('journey-wheelchair');
if (wheelchairToggle) {
@@ -1205,6 +1200,15 @@ const App = (() => {
});
}
+ // Ebenso für "Fahrrad mitnehmen".
+ const bikeToggle = document.getElementById('journey-bike');
+ if (bikeToggle) {
+ bikeToggle.checked = localStorage.getItem(BIKE_KEY) === 'true';
+ bikeToggle.addEventListener('change', () => {
+ localStorage.setItem(BIKE_KEY, bikeToggle.checked ? 'true' : 'false');
+ });
+ }
+
document.getElementById('journey-use-location').addEventListener('click', async () => {
const input = document.getElementById('journey-from');
input.value = 'Standort wird ermittelt…';
@@ -1228,6 +1232,135 @@ const App = (() => {
const shareBtn = document.getElementById('journey-share-btn');
if (shareBtn) shareBtn.addEventListener('click', shareCurrentJourney);
+
+ const addViaBtn = document.getElementById('journey-add-via');
+ if (addViaBtn) addViaBtn.addEventListener('click', addJourneyVia);
+ renderJourneyVias();
+
+ const earlierBtn = document.getElementById('journey-earlier-btn');
+ const laterBtn = document.getElementById('journey-later-btn');
+ if (earlierBtn) earlierBtn.addEventListener('click', () => loadAdjacentJourneys('earlier'));
+ if (laterBtn) laterBtn.addEventListener('click', () => loadAdjacentJourneys('later'));
+ }
+
+ // ---------- Zwischenhalte (Vias) ----------
+ //
+ // Bis zu MAX_VIAS Zwischenhalte, analog zur iOS-App. Das Backend verkettet
+ // dafür intern mehrere HAFAS-Einzelabfragen (siehe routes/journeys.js) -
+ // sobald mindestens ein Via gesetzt ist, gelten serverseitig immer
+ // "Abfahrt um" und es gibt keine Früher/Später-Buttons mehr.
+ function addJourneyVia() {
+ if (journeyVias.length >= MAX_VIAS) {
+ showToast(I18N.t('journey.maxViasReached'), 'warning');
+ return;
+ }
+ journeyVias.push({ lat: null, lon: null, name: '', waitMinutes: 0 });
+ renderJourneyVias();
+ }
+
+ function removeJourneyVia(index) {
+ journeyVias.splice(index, 1);
+ renderJourneyVias();
+ }
+
+ function renderJourneyVias() {
+ const container = document.getElementById('journey-vias');
+ if (!container) return;
+ container.innerHTML = '';
+
+ journeyVias.forEach((via, index) => {
+ const row = document.createElement('div');
+ row.className = 'journey-via-row';
+
+ const waitOptionsHtml = VIA_WAIT_OPTIONS
+ .map((m) => ``)
+ .join('');
+
+ row.innerHTML = `
+
+
+
+
+
+
+ `;
+
+ const input = row.querySelector('.journey-via-input');
+ const resultsEl = row.querySelector('.search-results');
+ setupLocationAutocomplete(input, resultsEl, (loc) => {
+ journeyVias[index] = {
+ ...journeyVias[index],
+ lat: loc.latitude,
+ lon: loc.longitude,
+ name: loc.name,
+ };
+ input.value = loc.name;
+ resultsEl.hidden = true;
+ });
+
+ const waitSelect = row.querySelector('select');
+ waitSelect.value = String(via.waitMinutes || 0);
+ waitSelect.addEventListener('change', () => {
+ journeyVias[index] = { ...journeyVias[index], waitMinutes: parseInt(waitSelect.value, 10) || 0 };
+ });
+
+ row.querySelector('.journey-via-remove').addEventListener('click', () => removeJourneyVia(index));
+
+ container.appendChild(row);
+ });
+
+ const addBtn = document.getElementById('journey-add-via');
+ if (addBtn) addBtn.hidden = journeyVias.length >= MAX_VIAS;
+ }
+
+ // ---------- Früher / Später ----------
+ //
+ // Lädt die vorherige bzw. nächste Seite an Verbindungen nach, mit der
+ // Referenz aus der vorherigen Antwort - ohne Vias verfügbar (siehe
+ // routes/journeys.js).
+ async function loadAdjacentJourneys(direction) {
+ if (!lastJourneyParams) return;
+ const resultsEl = document.getElementById('journey-results');
+ const earlierBtn = document.getElementById('journey-earlier-btn');
+ const laterBtn = document.getElementById('journey-later-btn');
+
+ const params = {
+ ...lastJourneyParams,
+ earlierRef: direction === 'earlier' ? journeyEarlierRef : undefined,
+ laterRef: direction === 'later' ? journeyLaterRef : undefined,
+ };
+
+ if (earlierBtn) earlierBtn.disabled = true;
+ if (laterBtn) laterBtn.disabled = true;
+
+ try {
+ const response = await API.getJourneys(params);
+ lastJourneyParams = params;
+ journeyEarlierRef = response.earlierRef || null;
+ journeyLaterRef = response.laterRef || null;
+ renderJourneys(response.journeys, resultsEl);
+ updateJourneyLoadMoreButtons();
+ } catch (err) {
+ showToast(err.message, 'warning');
+ if (earlierBtn) earlierBtn.disabled = !journeyEarlierRef;
+ if (laterBtn) laterBtn.disabled = !journeyLaterRef;
+ }
+ }
+
+ function updateJourneyLoadMoreButtons() {
+ const row = document.getElementById('journey-load-more-row');
+ const earlierBtn = document.getElementById('journey-earlier-btn');
+ const laterBtn = document.getElementById('journey-later-btn');
+ const hasRefs = !!(journeyEarlierRef || journeyLaterRef);
+ if (row) row.hidden = !hasRefs;
+ if (earlierBtn) earlierBtn.disabled = !journeyEarlierRef;
+ if (laterBtn) laterBtn.disabled = !journeyLaterRef;
}
// Setzt "Jetzt/Abfahrt um/Ankunft um" programmatisch (Klick-Handler UND
@@ -1265,22 +1398,30 @@ const App = (() => {
if (shareRow) shareRow.hidden = true;
const whenInput = document.getElementById('journey-when').value;
- const transferSlackSelect = document.getElementById('journey-transfer-slack');
const wheelchairToggle = document.getElementById('journey-wheelchair');
+ const bikeToggle = document.getElementById('journey-bike');
+ const validVias = journeyVias.filter((v) => v.lat != null && v.lon != null);
const params = {
from: journeyFrom,
to: journeyTo,
when: whenInput || undefined,
arrival: journeyArrivalMode,
results: 5,
- transferSlack: transferSlackSelect && transferSlackSelect.value !== '' ? transferSlackSelect.value : undefined,
wheelchair: wheelchairToggle ? wheelchairToggle.checked : false,
+ bike: bikeToggle ? bikeToggle.checked : false,
+ modes: journeyModeFilter ? journeyModeFilter.getModesParam() : undefined,
+ vias: validVias,
};
lastJourneyParams = params;
+ journeyEarlierRef = null;
+ journeyLaterRef = null;
try {
- const { journeys } = await API.getJourneys(params);
- renderJourneys(journeys, resultsEl);
+ const response = await API.getJourneys(params);
+ journeyEarlierRef = response.earlierRef || null;
+ journeyLaterRef = response.laterRef || null;
+ renderJourneys(response.journeys, resultsEl);
+ updateJourneyLoadMoreButtons();
if (shareRow) shareRow.hidden = false;
} catch (err) {
resultsEl.innerHTML = `
@@ -1289,6 +1430,7 @@ const App = (() => {
`;
+ updateJourneyLoadMoreButtons();
const retryBtn = document.getElementById('journey-retry');
if (retryBtn) retryBtn.addEventListener('click', () => runJourneySearch());
}
@@ -1310,8 +1452,8 @@ const App = (() => {
const toName = journeyTo.name === 'Mein Standort' ? 'Startpunkt' : journeyTo.name;
const whenInput = document.getElementById('journey-when').value;
- const transferSlackSelect = document.getElementById('journey-transfer-slack');
const wheelchairToggle = document.getElementById('journey-wheelchair');
+ const bikeToggle = document.getElementById('journey-bike');
const params = new URLSearchParams({
route: '1',
@@ -1324,8 +1466,20 @@ const App = (() => {
arrival: journeyArrivalMode ? '1' : '0',
});
if (whenInput) params.set('when', whenInput);
- if (transferSlackSelect && transferSlackSelect.value !== '') params.set('transferSlack', transferSlackSelect.value);
if (wheelchairToggle && wheelchairToggle.checked) params.set('wheelchair', '1');
+ if (bikeToggle && bikeToggle.checked) params.set('bike', '1');
+ if (journeyModeFilter) {
+ const modes = journeyModeFilter.getModesParam();
+ if (modes) params.set('modes', modes);
+ }
+ journeyVias
+ .filter((v) => v.lat != null && v.lon != null)
+ .forEach((via, i) => {
+ params.set(`via${i}Lat`, via.lat);
+ params.set(`via${i}Lon`, via.lon);
+ if (via.name) params.set(`via${i}Name`, via.name);
+ if (via.waitMinutes) params.set(`via${i}Wait`, via.waitMinutes);
+ });
return `${window.location.origin}${window.location.pathname}?${params.toString()}`;
}
@@ -1379,13 +1533,26 @@ const App = (() => {
const whenValue = params.get('when') || '';
setWhenChoice(params.get('arrival') === '1' ? 'arrival' : (whenValue ? 'departure' : 'now'), whenValue);
- const transferSlackParam = params.get('transferSlack');
- const transferSlackSelect = document.getElementById('journey-transfer-slack');
- if (transferSlackSelect && transferSlackParam) transferSlackSelect.value = transferSlackParam;
-
const wheelchairToggle = document.getElementById('journey-wheelchair');
if (wheelchairToggle) wheelchairToggle.checked = params.get('wheelchair') === '1';
+ const bikeToggle = document.getElementById('journey-bike');
+ if (bikeToggle) bikeToggle.checked = params.get('bike') === '1';
+
+ journeyVias = [];
+ for (let i = 0; i < MAX_VIAS; i++) {
+ const viaLat = parseFloat(params.get(`via${i}Lat`));
+ const viaLon = parseFloat(params.get(`via${i}Lon`));
+ if (Number.isNaN(viaLat) || Number.isNaN(viaLon)) continue;
+ journeyVias.push({
+ lat: viaLat,
+ lon: viaLon,
+ name: params.get(`via${i}Name`) || '',
+ waitMinutes: parseInt(params.get(`via${i}Wait`), 10) || 0,
+ });
+ }
+ renderJourneyVias();
+
activateTab('journey');
runJourneySearch();
}
@@ -1393,6 +1560,15 @@ const App = (() => {
function setupJourneyInput(inputId, onSelect) {
const input = document.getElementById(inputId);
const results = document.getElementById(`${inputId}-results`);
+ setupLocationAutocomplete(input, results, (loc) => {
+ onSelect({ id: loc.id || undefined, lat: loc.latitude, lon: loc.longitude, name: loc.name, kind: loc.kind });
+ });
+ }
+
+ // Wiederverwendbare Haltestellen-/Adress-Autovervollständigung. Wird für
+ // Start/Ziel (feste IDs im DOM) UND für dynamisch erzeugte
+ // Zwischenhalt-Zeilen genutzt (siehe renderJourneyVias()).
+ function setupLocationAutocomplete(input, results, onSelect) {
let debounce = null;
let requestId = 0;
wireSearchKeyboardNav(input, results);
@@ -1410,7 +1586,7 @@ const App = (() => {
const { locations } = await API.searchLocations(query);
if (thisRequestId !== requestId) return; // veraltete Antwort, verwerfen
renderSearchResults(results, locations, (loc) => {
- onSelect({ id: loc.id || undefined, lat: loc.latitude, lon: loc.longitude, name: loc.name, kind: loc.kind });
+ onSelect(loc);
input.value = loc.name;
results.hidden = true;
});
@@ -1421,7 +1597,7 @@ const App = (() => {
});
document.addEventListener('click', (e) => {
- if (!e.target.closest(`#${inputId}`) && !e.target.closest(`#${inputId}-results`)) {
+ if (e.target !== input && !results.contains(e.target)) {
results.hidden = true;
}
});
@@ -1606,6 +1782,12 @@ const App = (() => {
() => {},
);
LiveMap.setModeFilter(() => mapModeFilter.getModesParam());
+
+ journeyModeFilter = ModeFilter.create(
+ document.getElementById('journey-mode-filter'),
+ 'oepnv-navi:modes:journey',
+ () => {},
+ );
document.getElementById('map-locate').addEventListener('click', () => LiveMap.locateUser());
document.getElementById('map-isolation-reset').addEventListener('click', () => LiveMap.clearIsolation());
diff --git a/js/i18n.js b/js/i18n.js
index 8d8ce55..ffae322 100644
--- a/js/i18n.js
+++ b/js/i18n.js
@@ -89,16 +89,22 @@ const I18N = (() => {
'journey.departureAt': 'Abfahrt um',
'journey.arrivalAt': 'Ankunft um',
'journey.wheelchair': 'Nur rollstuhlgerechte Verbindungen',
- 'journey.transferSlack': 'Umstiegszeit-Puffer',
- 'journey.transferSlackDefault': 'Standard',
- 'journey.transferSlack0': '0 min (knapp)',
- 'journey.transferSlack15': '15 min (sicher)',
+ 'journey.bike': 'Fahrrad mitnehmen',
+ 'journey.modes': 'Verkehrsmittel',
+ 'journey.addVia': 'Zwischenhalt hinzufügen',
+ 'journey.viaPlaceholder': 'Zwischenhalt {n}',
+ 'journey.removeVia': 'Zwischenhalt entfernen',
+ 'journey.viaWaitLabel': 'Aufenthalt dort',
+ 'journey.viaWaitDirect': 'Direkt weiter',
+ 'journey.earlier': 'Früher',
+ 'journey.later': 'Später',
'journey.submit': 'Verbindungen suchen',
- 'journey.sourceNote': '🛰 Routenplanung über unsere selbst gehostete, offene Instanz (VBB-Region)',
+ 'journey.sourceNote': '🛰 Routenplanung mit Echtzeitdaten von VBB & Deutscher Bahn',
'journey.share': 'Route teilen',
'journey.selectFromResults': 'Bitte Start und Ziel aus der Liste auswählen.',
'journey.searching': 'Suche Verbindungen…',
'journey.noResults': 'Keine Verbindungen gefunden.',
+ 'journey.maxViasReached': 'Maximal 3 Zwischenhalte möglich.',
'disruptions.title': 'Störungen',
'disruptions.text': 'Ortsbezogene und netzweite Hinweise.',
'disruptions.nearbyTitle': 'In deiner Nähe',
@@ -204,16 +210,22 @@ const I18N = (() => {
'journey.departureAt': 'Depart at',
'journey.arrivalAt': 'Arrive by',
'journey.wheelchair': 'Wheelchair-accessible connections only',
- 'journey.transferSlack': 'Transfer buffer',
- 'journey.transferSlackDefault': 'Default',
- 'journey.transferSlack0': '0 min (tight)',
- 'journey.transferSlack15': '15 min (safe)',
+ 'journey.bike': 'Take my bike along',
+ 'journey.modes': 'Transit modes',
+ 'journey.addVia': 'Add stopover',
+ 'journey.viaPlaceholder': 'Stopover {n}',
+ 'journey.removeVia': 'Remove stopover',
+ 'journey.viaWaitLabel': 'Wait time there',
+ 'journey.viaWaitDirect': 'Continue immediately',
+ 'journey.earlier': 'Earlier',
+ 'journey.later': 'Later',
'journey.submit': 'Search connections',
- 'journey.sourceNote': '🛰 Journey planning via our self-hosted, open instance (VBB region)',
+ 'journey.sourceNote': '🛰 Journey planning with real-time VBB & Deutsche Bahn data',
'journey.share': 'Share route',
'journey.selectFromResults': 'Please choose start and destination from the list.',
'journey.searching': 'Searching for connections…',
'journey.noResults': 'No connections found.',
+ 'journey.maxViasReached': 'Maximum of 3 stopovers.',
'disruptions.title': 'Disruptions',
'disruptions.text': 'Location-based and network-wide notices.',
'disruptions.nearbyTitle': 'Near you',
diff --git a/styles.css b/styles.css
index 4012a5e..f367731 100644
--- a/styles.css
+++ b/styles.css
@@ -1426,22 +1426,78 @@ button:active { transform: translateY(1px); }
.journey-options-row { display: flex; align-items: end; justify-content: space-between; gap: 12px; }
.journey-options-row .mode-filter { margin-bottom: 0; }
-.transfer-slack-field { display: flex; flex-direction: column; gap: 5px; }
-.transfer-slack-field label {
+.journey-vias { display: flex; flex-direction: column; gap: 8px; margin: 4px 0 8px; }
+.journey-via-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+}
+.journey-input-marker--via { color: var(--ink-muted); }
+.journey-via-row__fields {
+ flex: 1;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+.journey-via-row__search { position: relative; flex: 1; min-width: 160px; }
+.journey-via-row__search input {
+ width: 100%;
+ background: var(--panel-raised);
+ border: 1px solid var(--line);
+ color: var(--ink);
+ font-family: var(--font-body);
+ font-size: 14px;
+ padding: 8px 12px;
+ border-radius: var(--radius);
+}
+.journey-via-wait { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-muted); white-space: nowrap; }
+.journey-via-wait select {
+ background: var(--panel);
+ color: var(--ink);
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 6px 8px;
+ font-family: inherit;
+ font-size: 12px;
+}
+.journey-via-remove {
+ background: transparent;
+ border: 1px solid var(--line);
+ color: var(--ink-muted);
+ border-radius: 999px;
+ width: 30px;
+ height: 30px;
+ flex-shrink: 0;
+ cursor: pointer;
+}
+.journey-via-remove:hover { color: var(--signal-red); border-color: var(--signal-red); }
+.journey-add-via { margin: 0 0 12px; }
+
+.journey-modes-field { display: flex; flex-direction: column; gap: 6px; margin: 0 0 12px; }
+.journey-modes-field__label {
color: var(--ink-muted);
font: 600 10px var(--font-label);
letter-spacing: 0.09em;
text-transform: uppercase;
}
-.transfer-slack-field select {
+.journey-modes-field .mode-filter { margin-bottom: 0; }
+
+.journey-load-more-row { display: flex; justify-content: space-between; gap: 10px; margin-top: 12px; }
+.journey-load-more-btn {
+ flex: 1;
background: var(--panel);
color: var(--ink);
border: 1px solid var(--line);
- border-radius: 8px;
- padding: 8px 10px;
- font-family: inherit;
+ border-radius: 999px;
+ padding: 9px 12px;
font-size: 13px;
+ cursor: pointer;
}
+.journey-load-more-btn:disabled { opacity: 0.4; cursor: default; }
+.journey-load-more-btn:not(:disabled):hover { border-color: var(--signal-amber); color: var(--signal-amber); }
+
.journey-submit { min-height: 38px; padding: 9px 15px; }
.journey-submit span { margin-left: 6px; }
.journey-card { padding: 16px; }
@@ -2161,7 +2217,8 @@ button:active { transform: translateY(1px); }
.journey-options-row { display: block; }
.journey-options-row .journey-submit { width: 100%; margin-top: 8px; }
.journey-options-row .mode-filter { margin-bottom: 8px; }
- .transfer-slack-field select { width: 100%; }
+ .journey-via-row__fields { flex-direction: column; align-items: stretch; }
+ .journey-via-wait select { width: 100%; }
.mobile-nav {
display: block; position: fixed; bottom: 0; left: 0; right: 0;
/* Muss über der (jetzt display-breiten) Live-Karte liegen - Leaflet nutzt