diff --git a/client/scripts/feature-pairs.json b/client/scripts/feature-pairs.json
index 7a630954f..72e148c37 100644
--- a/client/scripts/feature-pairs.json
+++ b/client/scripts/feature-pairs.json
@@ -1048,6 +1048,7 @@
"src/mobile/screens/trip/roadtrip/MRtLimitsEditor.tsx": "Large screen form inside MRtInfoSheet over the planner's saveRoadtripLimit, the desktop dialog's own save, and the shared dayWindow, parseAvoid and rangeFromSpec.",
"src/mobile/screens/trip/roadtrip/MRtKindSheet.tsx": "Thin sheet over planner.setRoadtripStopKind that already shares STOP_KINDS with StopKindPicker.",
"src/mobile/screens/trip/roadtrip/MRtStopSheet.tsx": "Phone only stop detail sheet over the shared roadtripRowModel, stayReading and planner writers.",
+ "src/mobile/screens/trip/roadtrip/MRtTrackSheet.tsx": "Large screen sheet over the planner's followTrack, the same hook RoadtripTrackModal runs on; the phone has no entry to it.",
"src/mobile/screens/trip/roadtrip/stageBadges.ts": "Badge helpers for the phone stage header and alternatives bar, which have no desktop counterpart.",
"src/mobile/screens/trip/roadtrip/useMRoadtrip.ts": "Phone stage screen adapter over the shared roadtripRowModel plus a live clock tick.",
"src/mobile/screens/trip/roadtrip/useMRtAlternatives.ts": "Tap to preview, confirm to write layer over the planner's shared route alternatives.",
diff --git a/client/src/mobile/screens/trip/roadtrip/MRoadtripTab.tsx b/client/src/mobile/screens/trip/roadtrip/MRoadtripTab.tsx
index 500690789..afec1d0ff 100644
--- a/client/src/mobile/screens/trip/roadtrip/MRoadtripTab.tsx
+++ b/client/src/mobile/screens/trip/roadtrip/MRoadtripTab.tsx
@@ -6,6 +6,7 @@ import { useMRtCorridor } from './useMRtCorridor'
import { useMRtAlternatives } from './useMRtAlternatives'
import MRtCorridorBar from './MRtCorridorBar'
import MRtAlternativesBar from './MRtAlternativesBar'
+import { MRtTrackEntry } from './MRtTrackSheet'
import { RtAutoRow, RtBookendRow, RtBookingChips, RtDryRow, RtLegRow, RtRideRow, RtSpillRow, RtStopRow, type RowChrome } from './MRoadtripRows'
import MBadge from '../../../components/MBadge'
import MDancingTrek from '../../../components/MDancingTrek'
@@ -289,6 +290,7 @@ function RoadtripStagePage({ planner, shell, dayId: pageDayId, alts, chrome }: O
)}
+
{rt.upNext && }
diff --git a/client/src/mobile/screens/trip/roadtrip/MRtTrackSheet.tsx b/client/src/mobile/screens/trip/roadtrip/MRtTrackSheet.tsx
new file mode 100644
index 000000000..33dc961fe
--- /dev/null
+++ b/client/src/mobile/screens/trip/roadtrip/MRtTrackSheet.tsx
@@ -0,0 +1,188 @@
+import { AlertTriangle, Check, RotateCw, Spline } from 'lucide-react';
+import { useState } from 'react';
+import type { FollowTrackOutcome } from '../../../../components/Roadtrip/useFollowTrack';
+import { useIsWide } from '../../../../hooks/sizeClass';
+import { useTranslation } from '../../../../i18n';
+import { useSettingsStore } from '../../../../store/settingsStore';
+import { formatDistance } from '../../../../utils/units';
+import MSheet from '../../../components/MSheet';
+import type { MTripSheetsProps } from '../MTripShell';
+import { SHEET_PRIMARY, SHEET_SECONDARY, TileHeader } from '../sheets/MTripSheetUi';
+
+/**
+ * Which imported track a day drives along, on an iPad or an unfolded Duo.
+ *
+ * The desktop's track dialog (RoadtripTrackModal) as a sheet, over the same `followTrack`
+ * of the planner: it lists the trip's tracks nearest to the day first, fits the drive to
+ * the one picked with via points, and drops those vias again. The phone has no entry to
+ * it; the stage there stays a plan read on the road, and reshaping a whole day's drive
+ * is work for the larger screen. The sheet opens when the planner's `followTrack` has a
+ * day, which only the large screen's stage card asks for.
+ */
+
+/** Within this many kilometres a track counts as running along the day. */
+const ON_THIS_DAY_KM = 2;
+
+/** The sentence for a finished fit: nothing to do, as close as asked, or as close as it got. */
+function outcomeKey(outcome: FollowTrackOutcome): string {
+ if (outcome.vias === 0) return 'roadtrip.track.already';
+ return outcome.capped ? 'roadtrip.track.capped' : 'roadtrip.track.done';
+}
+
+const NOTE = 'font-geist text-caption leading-snug';
+
+/**
+ * The stage card's way into the sheet, on a large screen only: the track the day follows,
+ * or the offer to follow one. Absent on the phone, for a day not stored yet, for a trip
+ * without tracks (its only answer would be "no tracks") and for a reader who may not edit
+ * days, since following a track writes via points onto the day.
+ */
+export function MRtTrackEntry({ planner, dayId }: { planner: MTripSheetsProps['planner']; dayId: number }) {
+ const { t } = useTranslation();
+ const large = useIsWide();
+ const follow = planner.followTrack;
+ if (!large || dayId < 0 || !follow.available || !planner.can('day_edit', planner.trip)) return null;
+ const name = follow.namesByDay[dayId];
+ return (
+
+ );
+}
+
+export default function MRtTrackSheet({ planner }: MTripSheetsProps) {
+ const { t } = useTranslation();
+ const unit = useSettingsStore((s) => s.settings.distance_unit);
+ const large = useIsWide();
+ const follow = planner.followTrack;
+ const { dayId, busy, round, error, outcome, tracks, viaCount, current } = follow;
+ // The pick belongs to the day it was made on: a track picked on one day means nothing
+ // on the next, so it is kept together with that day and read only while they match.
+ const [pick, setPick] = useState<{ dayId: number; trackId: number } | null>(null);
+ const picked = pick && pick.dayId === dayId ? pick.trackId : null;
+ const chosen = tracks.find((track) => track.id === picked) ?? null;
+ const open = large && dayId !== null;
+ const dayNumber = planner.roadtripRoutes.days.find((d) => d.dayId === dayId)?.dayNumber ?? 0;
+
+ return (
+
+