diff --git a/simutrans/text/en.tab b/simutrans/text/en.tab
index 0e8333d4b..54f541bee 100644
--- a/simutrans/text/en.tab
+++ b/simutrans/text/en.tab
@@ -1685,3 +1685,9 @@ Oak
 %s bypass %s
 9suburb
 %s mezzo %s
+Show route on map
+Show route on map
+Show the calculated route of this line on the main map.
+Show the calculated route of this line on the main map.
+Show the calculated route of this convoy on the main map.
+Show the calculated route of this convoy on the main map.
diff --git a/src/simutrans/display/simview.cc b/src/simutrans/display/simview.cc
index fcbb2b19f..9f748f7a2 100644
--- a/src/simutrans/display/simview.cc
+++ b/src/simutrans/display/simview.cc
@@ -6,6 +6,9 @@
 #include <stdio.h>
 
 #include "../world/simworld.h"
+
+#include "../dataobj/ribi.h"          // schedule-route overlay: diagonal detection + travel direction
+#include "../vehicle/vehicle_base.h"  // schedule-route overlay: reuse the lane offset table
 #include "simview.h"
 #include "simgraph.h"
 #include "viewport.h"
@@ -294,6 +297,76 @@ void main_view_t::display(bool force_dirty)
 		}
 	}
 
+	// schedule-route overlay (display only): draw the shown schedule's real path in the owning player's
+	// colour. A run of "bend" (diagonal) tiles is collapsed to one straight stroke between the
+	// straight/endpoint nodes bounding it, so a diagonal stretch is a straight line, not a staircase.
+	// Each stroke uses the antisymmetric lane table, so outward and return legs land on opposite lanes.
+	// Scheduled stops keep their own obj_t::highlight channel, distinct from the route.
+	{
+		const vector_tpl<schedule_route_pos_t>& route = welt->get_schedule_route_overlay();
+		if(  route.get_count() > 1  ) {
+			// Two coherent shades from the owning player's colour ramp: a bright core (ramp base + the GUI
+			// "bright" offset) legible on dark terrain, and a neutral-dark outline that separates it from
+			// light terrain. Reuses the existing player-colour ramp; the addition stays within the ramp.
+			const palette_index_t col_idx = welt->get_schedule_route_overlay_color();
+			const PIXVAL col_main = gfx->palette_lookup( (palette_index_t)(col_idx + env_t::gui_player_color_bright) );
+			const PIXVAL col_halo = gfx->palette_lookup( COL_BLACK );
+			const sint8 lane_sign = welt->get_settings().is_drive_left() ? 1 : -1;
+			// tile-centre anchor (screen pixels): half a tile across, ~19/32 down onto the way surface.
+			const scr_coord band( IMG_SIZE/2, (IMG_SIZE*19)/32 );
+
+			// lane offset (screen px, zoom- and driving-side-scaled) for travel screen-direction 0..7
+			auto lane_off = [&]( int dir ) -> scr_coord {
+				if(  dir < 0  ) {
+					return scr_coord( 0, 0 );
+				}
+				const sint8 bx = vehicle_base_t::get_driveleft_base_offset( (uint8)dir, 0 );
+				const sint8 by = vehicle_base_t::get_driveleft_base_offset( (uint8)dir, 1 );
+				return scr_coord( tile_raster_scale_x( lane_sign * bx, IMG_SIZE ),
+				                  tile_raster_scale_y( lane_sign * by, IMG_SIZE ) );
+			};
+			// travel screen-direction 0..7 between two route tiles (from the world delta), -1 if identical
+			auto travel_dir = []( const koord3d& a, const koord3d& b ) -> int {
+				const ribi_t::ribi r = ribi_type( b.get_2d() - a.get_2d() );
+				return r == ribi_t::none ? -1 : (int)ribi_t::get_dir( r );
+			};
+			// One 4-px band: a 1-px black outline above and below a 2-px bright core, between two points.
+			auto stroke = [&]( const scr_coord& p, const scr_coord& q ) {
+				gfx->draw_line( p.x, p.y - 1, q.x, q.y - 1, col_halo );
+				gfx->draw_line( p.x, p.y + 2, q.x, q.y + 2, col_halo );
+				gfx->draw_line( p.x, p.y,     q.x, q.y,     col_main );
+				gfx->draw_line( p.x, p.y + 1, q.x, q.y + 1, col_main );
+			};
+
+			// Walk the ordered tiles. Within a connected sub-route (delimited by new_segment) emit strokes
+			// only between NODE tiles -- sub-route ends and straight/endpoint tiles. An interior bend
+			// (a diagonal tile between two same-sub-route tiles) is skipped, so a diagonal staircase
+			// becomes one straight stroke between the nodes bounding it. A new_segment tile starts a fresh
+			// sub-route, so no stroke ever crosses an unroutable-leg break.
+			uint32 node = 0; // index of the last emitted node in the current sub-route
+			for(  uint32 i = 1;  i < route.get_count();  i++  ) {
+				if(  route[i].new_segment  ) {
+					node = i; // a disconnected sub-route starts here: do not join across the break
+					continue;
+				}
+				const bool ends_here = ( i + 1 >= route.get_count() ) || route[i+1].new_segment;
+				const bool interior  = ( i > node ) && !ends_here;
+				if(  interior  &&  ribi_t::is_bend( (ribi_t::ribi)route[i].ribi )  ) {
+					continue; // absorbed into the straight diagonal stroke between its bounding nodes
+				}
+				const int dir = travel_dir( route[node].pos, route[i].pos );
+				const scr_coord off = lane_off( dir );
+				const scr_coord pa = viewport->get_screen_coord( route[node].pos ) + band + off;
+				const scr_coord pb = viewport->get_screen_coord( route[i].pos )    + band + off;
+				stroke( pa, pb );
+				// rounded caps; the end dot also hides any 1-px rasterisation gap between adjacent strokes
+				gfx->draw_filled_circle( pa.x, pa.y, 1, col_main );
+				gfx->draw_filled_circle( pb.x, pb.y, 1, col_main );
+				node = i;
+			}
+		}
+	}
+
 	obj_t *zeiger = welt->get_zeiger();
 	DBG_DEBUG4("main_view_t::display", "display pointer");
 	if( zeiger  &&  zeiger->get_pos() != koord3d::invalid ) {
diff --git a/src/simutrans/gui/convoi_info.cc b/src/simutrans/gui/convoi_info.cc
index 163ef2638..4bed2a8d2 100644
--- a/src/simutrans/gui/convoi_info.cc
+++ b/src/simutrans/gui/convoi_info.cc
@@ -169,6 +169,11 @@ void convoi_info_t::init(convoihandle_t cnv)
 	container_schedule.add_component(&scd);
 	scd.add_listener(this);
 
+	bt_show_route.init( button_t::roundbox_state | button_t::flexible, "Show route on map" );
+	bt_show_route.set_tooltip( "Show the calculated route of this convoy on the main map." );
+	bt_show_route.add_listener( this );
+	container_schedule.add_component( &bt_show_route );
+
 	switch_mode.add_tab(&container_stats, translator::translate("Chart"));
 
 	container_stats.set_table_layout(1,0);
@@ -293,6 +298,30 @@ void convoi_info_t::apply_schedule()
 }
 
 
+// "Show route on map": the GUI only issues the request; the tool computes in a proper step. A convoy on
+// a line is shown via its line, so the toggle checks either identity.
+void convoi_info_t::toggle_route_overlay()
+{
+	if(  !cnv.is_bound()  ) {
+		return;
+	}
+	const bool already_shown = ( welt->get_schedule_route_overlay_convoi() == cnv )
+		|| ( cnv->get_line().is_bound()  &&  welt->get_schedule_route_overlay_line() == cnv->get_line() );
+	tool_t *tool = create_tool( TOOL_SCHEDULE_ROUTE_OVERLAY | SIMPLE_TOOL );
+	cbuffer_t buf;
+	if(  already_shown  ) {
+		buf.printf( "x" );
+	}
+	else {
+		buf.printf( "c,%u", cnv.get_id() );
+	}
+	tool->set_default_param( buf );
+	welt->set_tool( tool, cnv->get_owner() );
+	// init always returns false and the queued command copies the parameters, so it is safe to delete
+	delete tool;
+}
+
+
 void convoi_info_t::init_line_selector()
 {
 	if (cnv.is_bound()) {
@@ -472,6 +501,12 @@ void convoi_info_t::draw(scr_coord pos, scr_size size)
 
 	withdraw_button.pressed = cnv->get_withdraw();
 
+	// route overlay: needs vehicles to supply the representative descriptor. Source of truth is the
+	// world -- a convoy on a line is shown keyed on the line, so read either identity.
+	bt_show_route.enable( cnv->get_vehicle_count() > 0 );
+	bt_show_route.pressed = ( welt->get_schedule_route_overlay_convoi() == cnv )
+		|| ( cnv->get_line().is_bound()  &&  welt->get_schedule_route_overlay_line() == cnv->get_line() );
+
 	update_labels();
 
 	route_bar.set_base(cnv->get_route()->get_count()-1);
@@ -535,6 +570,10 @@ bool convoi_info_t::action_triggered( gui_action_creator_t *comp, value_t v)
 		env_t::default_sortmode = (int)(env_t::default_sortmode+1) % ((int)freight_list_sorter_t::SORT_MODES-1);
 		cnv->reset_freight_info();
 	}
+	// display-only route overlay: available to any viewer of the window (not gated by edit rights)
+	else if(  comp == &bt_show_route  ) {
+		toggle_route_overlay();
+	}
 
 	bool edit_allowed = (cnv.is_bound() && (cnv->get_owner() == welt->get_active_player() || welt->get_active_player()->is_public_service()));
 
@@ -673,6 +712,13 @@ bool convoi_info_t::infowin_event(const event_t *ev)
 		}
 		scd.highlight_schedule(false);
 		minimap_t::get_instance()->set_selected_cnv(convoihandle_t());
+		// clear the route overlay (via the tool, in a proper step) only if THIS convoy window requested it
+		if(  !is_saving_gui  &&  welt->get_schedule_route_overlay_req_convoi() == cnv  ) {
+			tool_t *tool = create_tool( TOOL_SCHEDULE_ROUTE_OVERLAY | SIMPLE_TOOL );
+			tool->set_default_param( "x" );
+			welt->set_tool( tool, cnv->get_owner() );
+			delete tool;
+		}
 	}
 
 	if(  ev->ev_class == INFOWIN  &&  ev->ev_code == WIN_TOP  ) {
diff --git a/src/simutrans/gui/convoi_info.h b/src/simutrans/gui/convoi_info.h
index 9c69d7ba0..7cb3659cc 100644
--- a/src/simutrans/gui/convoi_info.h
+++ b/src/simutrans/gui/convoi_info.h
@@ -63,6 +63,7 @@ private:
 	button_t no_load_button;
 	button_t sale_button;
 	button_t withdraw_button;
+	button_t bt_show_route;
 	gui_combobox_t line_selector;
 	// to add new lines automatically
 	uint32 old_line_count;
@@ -90,6 +91,9 @@ private:
 	// checks if possible / necessary
 	void rename_cnv();
 
+	// "Show route on map": issue the request to tool_schedule_route_overlay_t (computed in a step).
+	void toggle_route_overlay();
+
 	static bool route_search_in_progress;
 
 	void show_hide_statistics( bool show );
diff --git a/src/simutrans/gui/line_management_gui.cc b/src/simutrans/gui/line_management_gui.cc
index ca99cb21a..fb3ac1677 100644
--- a/src/simutrans/gui/line_management_gui.cc
+++ b/src/simutrans/gui/line_management_gui.cc
@@ -11,12 +11,14 @@
 #include "../dataobj/schedule.h"
 #include "../dataobj/loadsave.h"
 #include "../dataobj/translator.h"
+#include "../dataobj/route.h"
 
 #include "../vehicle/vehicle.h"
 
 #include "../convoihandle.h"
 #include "../simconvoi.h"
 #include "../obj/depot.h"
+#include "../ground/grund.h"
 #include "../simhalt.h"
 #include "../simline.h"
 #include "../tool/simmenu.h"
@@ -118,6 +120,12 @@ line_management_gui_t::line_management_gui_t( linehandle_t line_, player_t* play
 	add_component( &loading_info, 2 );
 	loading_text.printf( translator::translate("Capacity: %s\nLoad: %d (%d%%)"), 0, 0, 0);
 
+	// in the always-visible header so it works from every tab
+	bt_show_route.init( button_t::roundbox_state | button_t::flexible, "Show route on map" );
+	bt_show_route.set_tooltip( "Show the calculated route of this line on the main map." );
+	bt_show_route.add_listener( this );
+	add_component( &bt_show_route, 3 );
+
 	// tab panel: connections, chart panels, details
 	add_component( &switch_mode, 3 );
 	switch_mode.add_listener( this );
@@ -237,6 +245,12 @@ void line_management_gui_t::draw(scr_coord pos, scr_size size)
 		bt_withdraw_line.enable(is_change_allowed);
 		bt_find_convois.enable(is_change_allowed);
 
+		// route overlay needs a convoy to supply the representative vehicle
+		bt_show_route.enable( line->count_convoys() > 0 );
+		// source of truth is the world: pressed iff this line is the one currently shown, so opening a
+		// second line window (or another window showing a different line) reflects the real state
+		bt_show_route.pressed = ( welt->get_schedule_route_overlay_line() == line );
+
 		if(  line->count_convoys() != old_convoi_count  ) {
 
 			old_convoi_count = line->count_convoys();
@@ -404,6 +418,26 @@ void line_management_gui_t::apply_schedule()
 }
 
 
+// "Show route on map": the GUI only issues the request; the tool computes in a proper step.
+void line_management_gui_t::toggle_route_overlay()
+{
+	const bool already_shown = ( welt->get_schedule_route_overlay_line() == line );
+	tool_t *tool = create_tool( TOOL_SCHEDULE_ROUTE_OVERLAY | SIMPLE_TOOL );
+	cbuffer_t buf;
+	if(  !already_shown  &&  line.is_bound()  ) {
+		buf.printf( "l,%u", line.get_id() );
+	}
+	else {
+		buf.printf( "x" );
+	}
+	tool->set_default_param( buf );
+	welt->set_tool( tool, player );
+	// init always returns false and the queued command copies the parameters, so it is safe to
+	// delete immediately
+	delete tool;
+}
+
+
 bool line_management_gui_t::action_triggered( gui_action_creator_t *comp, value_t v )
 {
 	if(line->count_convoys()>0) {
@@ -458,6 +492,9 @@ bool line_management_gui_t::action_triggered( gui_action_creator_t *comp, value_
 			delete tmp_tool;
 		}
 	}
+	else if(  comp == &bt_show_route  ) {
+		toggle_route_overlay();
+	}
 	else if(  comp == &bt_find_convois  ) {
 		for(convoihandle_t cnv : welt->convoys()) {
 			if(  cnv->get_owner()==player  ) {
@@ -517,6 +554,13 @@ bool line_management_gui_t::infowin_event( const event_t *ev )
 		}
 		scd.highlight_schedule( false );
 		minimap_t::get_instance()->set_selected_cnv(convoihandle_t());
+		// clear the overlay (via the tool, in a proper step) only if THIS line window requested it
+		if(  !is_saving_gui  &&  welt->get_schedule_route_overlay_req_line() == line  ) {
+			tool_t *tool = create_tool( TOOL_SCHEDULE_ROUTE_OVERLAY | SIMPLE_TOOL );
+			tool->set_default_param( "x" );
+			welt->set_tool( tool, player );
+			delete tool;
+		}
 	}
 
 	if(  ev->ev_class == INFOWIN  &&  ev->ev_code == WIN_TOP  ) {
diff --git a/src/simutrans/gui/line_management_gui.h b/src/simutrans/gui/line_management_gui.h
index c56beaec7..9c1fa590e 100644
--- a/src/simutrans/gui/line_management_gui.h
+++ b/src/simutrans/gui/line_management_gui.h
@@ -23,6 +23,8 @@
 #include "components/gui_textinput.h"
 
 #include "../linehandle.h"
+#include "../dataobj/koord3d.h"
+#include "../tpl/vector_tpl.h"
 
 class player_t;
 class loadsave_t;
@@ -47,8 +49,12 @@ class line_management_gui_t : public gui_frame_t, public action_listener_t
 	gui_button_to_chart_array_t button_to_chart;
 
 	button_t bt_withdraw_line, bt_find_convois;
+	button_t bt_show_route;
 	gui_scrolled_list_t scrolly_convois, scrolly_halts;
 
+	// "Show route on map": issue the request to tool_schedule_route_overlay_t (computed in a step).
+	void toggle_route_overlay();
+
 	gui_aligned_container_t container_schedule, container_stats, container_convois, container_halts;
 
 	cbuffer_t loading_text;
diff --git a/src/simutrans/simconvoi.cc b/src/simutrans/simconvoi.cc
index ac79cddd5..66a9be691 100644
--- a/src/simutrans/simconvoi.cc
+++ b/src/simutrans/simconvoi.cc
@@ -1902,6 +1902,9 @@ bool convoi_t::set_schedule(schedule_t * f)
 	// to avoid jumping trains
 	alte_richtung = fahr[0]->get_direction();
 	wait_lock = 0;
+	// The final schedule and line membership are now visible. This also covers callers that bypass
+	// tool_change_convoi_t, such as AI and depot code.
+	welt->schedule_route_notify_convoi_changed( self );
 	return true;
 }
 
@@ -3278,6 +3281,10 @@ void convoi_t::self_destruct()
  */
 void convoi_t::destroy()
 {
+	// central display-only overlay hook, while self is still valid: clear it if this was the requester
+	// (covers lineless convoys and every deletion route; a no-op during world teardown)
+	welt->schedule_route_notify_convoi_destroyed( self );
+
 	// can be only done here, with a valid convoihandle ...
 	if(fahr[0]) {
 		fahr[0]->set_convoi(NULL);
@@ -3413,6 +3420,8 @@ void convoi_t::set_line(linehandle_t org_line)
 	}
 	line_update_pending = org_line;
 	check_pending_updates();
+	// Notify after check_pending_updates(), when both the new line and its copied schedule are final.
+	welt->schedule_route_notify_convoi_changed( self );
 }
 
 
diff --git a/src/simutrans/simline.cc b/src/simutrans/simline.cc
index cc32c8870..ed4662a5f 100644
--- a/src/simutrans/simline.cc
+++ b/src/simutrans/simline.cc
@@ -210,6 +210,9 @@ void simline_t::remove_convoy(convoihandle_t cnv)
 		recalc_catg_index();
 		financial_history[0][LINE_CONVOIS] = count_convoys();
 		recalc_status();
+		// central display-only overlay hook: the managed list now reflects the removal, so a recompute
+		// picks the next representative and never re-selects the convoy that just left
+		welt->schedule_route_notify_line_convoi_removed( self, cnv );
 	}
 	if(line_managed_convoys.empty()) {
 		unregister_stops();
diff --git a/src/simutrans/tool/simmenu.cc b/src/simutrans/tool/simmenu.cc
index 8f219db4f..24dfeafe0 100644
--- a/src/simutrans/tool/simmenu.cc
+++ b/src/simutrans/tool/simmenu.cc
@@ -171,6 +171,7 @@ const char* tool_t::id_to_string(uint16 id)
 			CASE_TO_STRING(TOOL_DAY_NIGHT_TOGGLE);
 			CASE_TO_STRING(TOOL_WORK_WORLD);
 			CASE_TO_STRING(TOOL_HALT_PERMISSION);
+			CASE_TO_STRING(TOOL_SCHEDULE_ROUTE_OVERLAY);
 		}
 	}
 	else if (id & DIALOGE_TOOL) {
@@ -344,6 +345,7 @@ tool_t* create_simple_tool(int toolnr)
 	case TOOL_SINGLE_WAY_TOOGLE:    tool = new tool_show_single_ways_t();     break;
 	case TOOL_WORK_WORLD:           tool = new tool_work_world_t();           break;
 	case TOOL_HALT_PERMISSION:      tool = new tool_change_permission_t();    break;
+	case TOOL_SCHEDULE_ROUTE_OVERLAY: tool = new tool_schedule_route_overlay_t(); break;
 	case UNUSED_TOOL_ADD_MESSAGE: // fall-through - intended!!!111elf
 	case UNUSED_WKZ_PWDHASH_TOOL:
 		dbg->warning("create_simple_tool()", "Deprecated tool [%i] requested", toolnr);
diff --git a/src/simutrans/tool/simmenu.h b/src/simutrans/tool/simmenu.h
index 756b41cf1..de93d0495 100644
--- a/src/simutrans/tool/simmenu.h
+++ b/src/simutrans/tool/simmenu.h
@@ -134,6 +134,7 @@ enum {
 	TOOL_SINGLE_WAY_TOOGLE,
 	TOOL_WORK_WORLD,
 	TOOL_HALT_PERMISSION,
+	TOOL_SCHEDULE_ROUTE_OVERLAY,
 	SIMPLE_TOOL_COUNT,
 	SIMPLE_TOOL = 0x2000
 };
diff --git a/src/simutrans/tool/simtool.cc b/src/simutrans/tool/simtool.cc
index 9d568781f..20ccfe901 100644
--- a/src/simutrans/tool/simtool.cc
+++ b/src/simutrans/tool/simtool.cc
@@ -37,6 +37,9 @@
 #include "../descriptor/tunnel_desc.h"
 
 #include "../vehicle/air_vehicle.h"
+#include "../vehicle/road_vehicle.h"   // schedule-route overlay: build a throwaway test driver
+#include "../vehicle/rail_vehicle.h"   // (of the right subclass) from the representative descriptor
+#include "../vehicle/water_vehicle.h"
 #include "../vehicle/simroadtraffic.h"
 #include "../vehicle/pedestrian.h"
 
@@ -76,6 +79,8 @@
 #include "../dataobj/schedule.h"
 #include "../dataobj/route.h"
 #include "../dataobj/scenario.h"
+#include "../simline.h"
+#include "../vehicle/vehicle.h"
 #include "../network/network_cmd_ingame.h" // for dragging raise / lower tools
 
 #include "../builder/tunnelbauer.h"
@@ -7918,6 +7923,9 @@ bool scenario_check_convoy(karte_t *welt, player_t *player, convoihandle_t cnv,
  * 'l' : apply new line [number]
  * 'd' : go to nearest depot
  */
+// schedule-route overlay auto-update hooks (defined with the overlay tool below).
+static void schedule_route_on_line_changed( linehandle_t line );
+
 bool tool_change_convoi_t::init( player_t *player )
 {
 	char tool=0;
@@ -7959,6 +7967,7 @@ bool tool_change_convoi_t::init( player_t *player )
 	// first letter is now the actual command
 	switch(  tool  ) {
 		case 'x': // self destruction ...
+			// the overlay reacts centrally in convoi_t::destroy() / simline_t::remove_convoy()
 			if (cnv->get_state()==convoi_t::INITIAL) {
 				// delete cnv in depot
 				if (grund_t *gr = welt->lookup(cnv->get_pos())) {
@@ -8142,6 +8151,10 @@ bool tool_change_line_t::init( player_t *player )
 		case 'd': // delete line
 			{
 				if (line.is_bound()) {
+					// drop the overlay now, while the handle is still bound, if it shows this line
+					if(  welt->get_schedule_route_overlay_line() == line  ) {
+						welt->clear_schedule_route_overlay( true );
+					}
 					// close a schedule window, if still active
 					gui_frame_t *w = win_get_magic( (ptrdiff_t)line.get_rep() );
 					if(w) {
@@ -8294,6 +8307,12 @@ bool tool_change_line_t::init( player_t *player )
 			}
 			break;
 	}
+	// the line's schedule ('g') just changed and is now applied: if this line is the shown
+	// schedule-route overlay's source, recompute it (display only, no recursion). Deletion ('d')
+	// clears the overlay inside its own case, while the line handle is still bound.
+	if(  tool=='g'  ) {
+		schedule_route_on_line_changed( line );
+	}
 	return false;
 }
 
@@ -8977,6 +8996,343 @@ bool tool_work_world_t::init(player_t*)
 }
 
 
+// ---- tool_schedule_route_overlay_t: a schedule's real route drawn on the main map -----------
+// Display-only overlay for a line or a lineless convoy. The route is drawn procedurally by the map view;
+// the scheduled stops keep their own obj_t::highlight channel, so the two never collide. The pathfinder
+// runs here from a step (command queue), never in the display loop (the A* node array is not reentrant).
+
+// Set obj_t::highlight on the collected stop tiles; clearing is owned by karte_t.
+static void schedule_route_set_stop_highlight()
+{
+	karte_t *welt = world();
+	for(  koord3d const& pos : welt->get_schedule_route_overlay_stops()  ) {
+		grund_t *gr = welt->lookup(pos);
+		if(  gr == NULL  ) {
+			continue;
+		}
+		for(  uint idx = 0;  idx < gr->obj_count();  idx++  ) {
+			obj_t *obj = gr->obj_bei(idx);
+			if(  !obj->is_moving()  ) {
+				obj->set_flag( obj_t::highlight );
+			}
+		}
+		if(  gr->is_water()  ||  gr->ist_natur()  ) {
+			gr->set_flag( grund_t::marked );
+		}
+		gr->set_flag( grund_t::dirty );
+	}
+}
+
+// Route probes are off-map and must not mutate the representative convoy. A throwaway vehicle of the
+// right subclass carries the real convoy only as read-only context (electrification, min top speed,
+// tile length). It is built at koord3d::invalid so ~rail_vehicle_t() cannot unreserve a live tile, and
+// the route is always taken via route_t::calc_route (never the rail subclass override, which writes cnv).
+static vehicle_t* schedule_route_make_probe( convoihandle_t cnv, const vehicle_desc_t *desc )
+{
+	convoi_t *ctx = cnv.get_rep();
+	player_t *owner = cnv.is_bound() ? cnv->get_owner() : NULL;
+	vehicle_t *v;
+	switch(  desc->get_waytype()  ) {
+		case road_wt:        v = new road_vehicle_t(       koord3d::invalid, desc, owner, ctx ); break;
+		case monorail_wt:    v = new monorail_vehicle_t(   koord3d::invalid, desc, owner, ctx ); break;
+		case track_wt:
+		case tram_wt:        v = new rail_vehicle_t(        koord3d::invalid, desc, owner, ctx ); break;
+		case water_wt:       v = new water_vehicle_t(       koord3d::invalid, desc, owner, ctx ); break;
+		case air_wt:         v = new air_vehicle_t(         koord3d::invalid, desc, owner, ctx ); break;
+		case maglev_wt:      v = new maglev_vehicle_t(      koord3d::invalid, desc, owner, ctx ); break;
+		case narrowgauge_wt: v = new narrowgauge_vehicle_t( koord3d::invalid, desc, owner, ctx ); break;
+		default:             return NULL;
+	}
+	v->set_flag( obj_t::not_on_map );
+	return v;
+}
+
+// Reproduce the per-waytype max_tile_len each real vehicle passes to route_t::calc_route, so the probe
+// advances into destination platforms exactly as the vehicle would:
+//   road: the convoy's tile length; rail and its subclasses (track/tram/monorail/maglev/narrowgauge):
+//   the convoy length only when halts are used as scheduled, else 8888; water and the rest: 0. Air is
+//   not routed through here (it uses air_vehicle_t::calc_route).
+static sint32 schedule_route_max_tile_len( convoihandle_t cnv, waytype_t wt )
+{
+	switch(  wt  ) {
+		case track_wt:
+		case tram_wt:
+		case monorail_wt:
+		case maglev_wt:
+		case narrowgauge_wt:
+			return world()->get_settings().get_stop_halt_as_scheduled() ? cnv->get_tile_length() : 8888;
+		case road_wt:
+			return cnv->get_tile_length();
+		default:
+			return 0;
+	}
+}
+
+// Compute @p schedule leg by leg into the overlay, using @p cnv as the representative convoy for the
+// route context (waytype, electrification, min top speed, tile length, owner). Each tile keeps its route
+// ribi and an explicit break before a sub-route that follows an unroutable leg. Assumes the overlay was
+// cleared. The real convoy is never the test driver.
+static void schedule_route_compute_into( schedule_t *schedule, convoihandle_t cnv )
+{
+	if(  schedule == NULL  ||  !cnv.is_bound()  ||  cnv->get_vehicle_count() == 0  ||  schedule->get_count() < 2  ) {
+		return;
+	}
+	karte_t *welt = world();
+	const vehicle_desc_t *desc = cnv->front()->get_desc();
+	const sint32 max_speed = speed_to_kmh( cnv->get_min_top_speed() ); // route_t/air expect km/h, not internal units
+	const sint32 max_len   = schedule_route_max_tile_len( cnv, desc->get_waytype() );
+	vector_tpl<schedule_route_pos_t>& path = welt->access_schedule_route_overlay();
+	const uint8 count = schedule->get_count();
+	for(  uint8 i = 0;  i < count;  i++  ) {
+		welt->access_schedule_route_overlay_stops().append_unique( schedule->entries[i].pos );
+	}
+	if(  cnv->get_owner()  ) {
+		welt->set_schedule_route_overlay_color( cnv->get_owner()->get_player_color1() );
+	}
+	vehicle_t *probe = schedule_route_make_probe( cnv, desc );
+	if(  probe == NULL  ) {
+		return;
+	}
+	const bool is_air = ( desc->get_waytype() == air_wt );
+	route_t seg;
+	bool pending_break = false;
+	for(  uint8 i = 0;  i < count;  i++  ) {
+		const koord3d start = schedule->entries[i].pos;
+		const koord3d ziel  = schedule->entries[(i+1)%count].pos;
+		if(  start == ziel  ) {
+			continue;
+		}
+		// planes fly (air_vehicle_t::calc_route); everything else follows ways (route_t::calc_route)
+		const bool routed = is_air
+			? probe->calc_route( start, ziel, max_speed, &seg )
+			: seg.calc_route( welt, start, ziel, probe, max_speed, max_len ) != route_t::no_route;
+		if(  routed  &&  seg.get_count() > 0  ) {
+			const koord3d_vector_t& r = seg.get_route();
+			for(  uint32 n = 0;  n < r.get_count();  n++  ) {
+				const koord3d t = r[n];
+				if(  !pending_break  &&  path.get_count() > 0  &&  path[path.get_count()-1].pos == t  ) {
+					continue; // consecutive legs share the stop tile
+				}
+				path.append( schedule_route_pos_t( t, (uint8)r.get_ribi(n), pending_break ) );
+				pending_break = false;
+			}
+		}
+		else if(  path.get_count() > 0  ) {
+			pending_break = true; // unroutable leg: the next drawn tile begins a new sub-route
+		}
+	}
+	delete probe;
+}
+
+// First convoy of @p line that can represent its route: bound, has vehicles, not self-destructing.
+static convoihandle_t schedule_route_line_representative( linehandle_t line )
+{
+	if(  line.is_bound()  ) {
+		for(  uint32 i = 0;  i < line->count_convoys();  i++  ) {
+			const convoihandle_t c = line->get_convoy( i );
+			if(  c.is_bound()  &&  c->get_vehicle_count() > 0  &&  c->get_state() != convoi_t::SELF_DESTRUCT  ) {
+				return c;
+			}
+		}
+	}
+	return convoihandle_t();
+}
+
+// Build the effective source from a line, drawn with its first usable convoy; a line with no usable
+// representative leaves the overlay empty. Sets the effective line + stop highlight on success.
+static void schedule_route_build_line( linehandle_t line )
+{
+	const convoihandle_t rep = schedule_route_line_representative( line );
+	if(  !rep.is_bound()  ) {
+		return;
+	}
+	schedule_route_compute_into( line->get_schedule(), rep );
+	if(  world()->get_schedule_route_overlay().get_count() > 0  ) {
+		world()->set_schedule_route_overlay_line( line );
+		world()->set_schedule_route_overlay_rep( rep );  // remember the drawn representative
+		schedule_route_set_stop_highlight();
+	}
+}
+
+// Build the effective source from a lineless convoy's own schedule.
+static void schedule_route_build_convoi_lineless( convoihandle_t cnv )
+{
+	if(  !cnv.is_bound()  ||  cnv->get_vehicle_count() == 0  ) {
+		return;
+	}
+	schedule_route_compute_into( cnv->get_schedule(), cnv );
+	if(  world()->get_schedule_route_overlay().get_count() > 0  ) {
+		world()->set_schedule_route_overlay_convoi( cnv );
+		world()->set_schedule_route_overlay_rep( cnv );
+		schedule_route_set_stop_highlight();
+	}
+}
+
+// Rebuild the effective source from a requester into a cleared overlay. A convoy requester follows its
+// convoy (drawn via its current line, or its own schedule when lineless); a line requester tracks its
+// line. The current line's schedule is always used, never a stale copy.
+static void schedule_route_rebuild_from_requester( linehandle_t req_line, convoihandle_t req_convoi )
+{
+	if(  req_line.is_bound()  ) {
+		schedule_route_build_line( req_line );
+	}
+	else if(  req_convoi.is_bound()  ) {
+		if(  req_convoi->get_line().is_bound()  ) {
+			schedule_route_build_line( req_convoi->get_line() );
+		}
+		else {
+			schedule_route_build_convoi_lineless( req_convoi );
+		}
+	}
+}
+
+// Keep the requester window identity separate from the effective schedule source: a line window is both;
+// a convoy window is the requester while its line may be the effective source.
+static void schedule_route_show_line( linehandle_t line )
+{
+	karte_t *welt = world();
+	welt->clear_schedule_route_overlay( true );
+	schedule_route_build_line( line );
+	if(  welt->get_schedule_route_overlay().get_count() > 0  ) {
+		welt->set_schedule_route_overlay_req_line( line );
+	}
+	else {
+		welt->clear_schedule_route_overlay( false );
+	}
+	welt->set_dirty();
+}
+
+static void schedule_route_show_convoi( convoihandle_t cnv )
+{
+	karte_t *welt = world();
+	welt->clear_schedule_route_overlay( true );
+	if(  cnv.is_bound()  &&  cnv->get_line().is_bound()  ) {
+		schedule_route_build_line( cnv->get_line() );
+	}
+	else {
+		schedule_route_build_convoi_lineless( cnv );
+	}
+	if(  welt->get_schedule_route_overlay().get_count() > 0  ) {
+		welt->set_schedule_route_overlay_req_convoi( cnv );
+	}
+	else {
+		welt->clear_schedule_route_overlay( false );
+	}
+	welt->set_dirty();
+}
+
+// Recompute the shown overlay from the requester identity (not the previous effective source), so a
+// convoy requester follows its convoy across line changes while its button stays pressed.
+static void schedule_route_refresh()
+{
+	karte_t *welt = world();
+	const linehandle_t   req_line   = welt->get_schedule_route_overlay_req_line();
+	const convoihandle_t req_convoi = welt->get_schedule_route_overlay_req_convoi();
+	welt->clear_schedule_route_overlay( true );
+	schedule_route_rebuild_from_requester( req_line, req_convoi );
+	if(  welt->get_schedule_route_overlay().get_count() > 0  ) {
+		welt->set_schedule_route_overlay_req_line( req_line );
+		welt->set_schedule_route_overlay_req_convoi( req_convoi );
+	}
+	welt->set_dirty();
+}
+
+// A line changed: refresh if the requester is that line, or a convoy requester currently on it.
+static void schedule_route_on_line_changed( linehandle_t line )
+{
+	if(  !line.is_bound()  ) {
+		return;
+	}
+	karte_t *welt = world();
+	const convoihandle_t req_convoi = welt->get_schedule_route_overlay_req_convoi();
+	const bool affects = ( welt->get_schedule_route_overlay_req_line() == line )
+		|| ( req_convoi.is_bound()  &&  req_convoi->get_line() == line );
+	if(  affects  ) {
+		schedule_route_refresh();
+	}
+}
+
+// A convoy joined a line, changed its line, or installed an individual schedule. Called by convoi_t only
+// after its final line and schedule are visible, including changes that bypass tool_change_convoi_t.
+void karte_t::schedule_route_notify_convoi_changed( convoihandle_t cnv )
+{
+	if(  is_destroying()  ||  !cnv.is_bound()  ) {
+		return;
+	}
+	const linehandle_t req_line = get_schedule_route_overlay_req_line();
+	const bool affects = ( get_schedule_route_overlay_req_convoi() == cnv )
+		|| ( req_line.is_bound()  &&  req_line == cnv->get_line() );
+	if(  affects  ) {
+		schedule_route_refresh();
+	}
+}
+
+// Central removal notification, invoked from simline_t::remove_convoy() after the managed-convoy list
+// already reflects the removal. Recompute the shown line with its next representative (or clear) only when
+// the convoy that left was the one the drawn route was computed from. Covers every removal route that
+// detaches a convoy from a line (destruction, sale/disassemble, line trim, delete-line, live line change).
+void karte_t::schedule_route_notify_line_convoi_removed( linehandle_t line, convoihandle_t cnv )
+{
+	if(  is_destroying()  ) {
+		return; // world teardown: never rebuild routes or touch grounds
+	}
+	// The requesting convoy itself is handled by schedule_route_notify_convoi_destroyed() (real deletion)
+	// or by its schedule/line hook (a live line change re-shows it), never cleared from here.
+	if(  schedule_route_overlay_req_convoi == cnv  ) {
+		return;
+	}
+	const bool line_shown = ( schedule_route_overlay_req_line.is_bound()  &&  schedule_route_overlay_req_line == line )
+		|| ( schedule_route_overlay_req_convoi.is_bound()  &&  schedule_route_overlay_req_convoi->get_line() == line );
+	if(  line_shown  &&  schedule_route_overlay_rep == cnv  ) {
+		schedule_route_refresh();
+	}
+}
+
+// Central destruction notification, invoked from convoi_t::destroy(). If the destroyed convoy was the
+// requester (lineless or on a line), clear the overlay. No-op during world teardown / load.
+void karte_t::schedule_route_notify_convoi_destroyed( convoihandle_t cnv )
+{
+	if(  is_destroying()  ) {
+		return;
+	}
+	if(  schedule_route_overlay_req_convoi == cnv  ) {
+		clear_schedule_route_overlay( true );
+		set_dirty();
+	}
+}
+
+bool tool_schedule_route_overlay_t::init(player_t*)
+{
+	karte_t *welt = world();
+	// default_param: "l,<line_id>" line window, "c,<convoi_id>" convoy window, anything else clears
+	const char *p = default_param;
+	while(  p  &&  *p  &&  *p <= ' '  ) {
+		p++;
+	}
+	const char cmd = (p  &&  *p) ? *p : 'x';
+	if(  cmd == 'l'  ||  cmd == 'c'  ) {
+		while(  *p  &&  *p++ != ','  ) {
+		}
+		const uint16 id = (uint16)atoi(p);
+		if(  cmd == 'l'  ) {
+			linehandle_t line;
+			line.set_id( id );
+			schedule_route_show_line( line );
+		}
+		else {
+			convoihandle_t cnv;
+			cnv.set_id( id );
+			schedule_route_show_convoi( cnv );
+		}
+	}
+	else {
+		welt->clear_schedule_route_overlay( true );
+		welt->set_dirty();
+	}
+	return false; // unsafe tools must return false
+}
+
+
 
 /*
  * Add a message to the message queue
diff --git a/src/simutrans/tool/simtool.h b/src/simutrans/tool/simtool.h
index 08e3dc40c..9afa63704 100644
--- a/src/simutrans/tool/simtool.h
+++ b/src/simutrans/tool/simtool.h
@@ -1037,6 +1037,20 @@ public:
 	bool is_work_keeps_game_state() const OVERRIDE { return false; }
 };
 
+/**
+ * Display-only overlay: compute and highlight a schedule's route on the main map (source is a line or a
+ * lineless convoy). Local/per-client (WFL_LOCAL), nothing saved. is_init_keeps_game_state()==false runs
+ * the pathfinder from the command queue between steps, never in GUI code.
+ * default_param: "l,<line_id>", "c,<convoi_id>", anything else clears.
+ */
+class tool_schedule_route_overlay_t : public tool_t {
+public:
+	tool_schedule_route_overlay_t() : tool_t(TOOL_SCHEDULE_ROUTE_OVERLAY | SIMPLE_TOOL) { flags = WFL_LOCAL | WFL_NO_CHK; }
+	bool init(player_t*) OVERRIDE;
+	bool is_init_keeps_game_state() const OVERRIDE { return false; }
+	bool is_work_keeps_game_state() const OVERRIDE { return false; }
+};
+
 class tool_rotate90_t : public tool_t {
 public:
 	tool_rotate90_t() : tool_t(TOOL_ROTATE90 | SIMPLE_TOOL) {}
diff --git a/src/simutrans/vehicle/vehicle_base.h b/src/simutrans/vehicle/vehicle_base.h
index e253f5dbc..ee10bb41c 100644
--- a/src/simutrans/vehicle/vehicle_base.h
+++ b/src/simutrans/vehicle/vehicle_base.h
@@ -104,6 +104,11 @@ public:
 
 	static void set_overtaking_offsets( bool driving_on_the_left );
 
+	// Per screen-direction (ribi_t::get_dir 0..7) lane offset in base-64 pixel units, used by
+	// vehicles to shift to the left-hand lane. The table is antisymmetric (offset[opposite] ==
+	// -offset), so the line-route overlay reuses it to draw outward and return on opposite lanes.
+	static sint8 get_driveleft_base_offset( uint8 dir, uint8 axis ) { return driveleft_base_offsets[dir & 7][axis & 1]; }
+
 	// if true, this convoi needs to restart for correct alignment
 	bool need_realignment() const;
 
diff --git a/src/simutrans/world/simworld.cc b/src/simutrans/world/simworld.cc
index e1c354b44..817a66c5c 100644
--- a/src/simutrans/world/simworld.cc
+++ b/src/simutrans/world/simworld.cc
@@ -1232,6 +1232,7 @@ void karte_t::init(settings_t* const sets, sint8 const* const h_field)
 	ticks = 0;
 	last_step_ticks = ticks;
 	schedule_counter = 0;
+	clear_schedule_route_overlay( false ); // display-only overlay: drop any route from the previous world
 	// ticks = 0x7FFFF800;  // Testing the 31->32 bit step
 
 	last_month = 0;
@@ -2039,6 +2040,7 @@ karte_t::karte_t() :
 
 	zeiger = NULL;
 	schedule_counter = 0;
+	schedule_route_overlay_color = 0; // display-only schedule-route overlay (empty until a source is shown)
 	nosave_warning = nosave = false;
 	loaded_rotation = 0;
 	last_year = 1930;
@@ -2361,6 +2363,35 @@ void karte_t::rotate90_plans(sint16 x_min, sint16 x_max, sint16 y_min, sint16 y_
 }
 
 
+void karte_t::clear_schedule_route_overlay( bool clear_highlight )
+{
+	if(  clear_highlight  ) {
+		// unset the schedule-stop highlight on exactly the tiles we marked (no map scan)
+		for(  koord3d const& pos : schedule_route_overlay_stops  ) {
+			grund_t *gr = lookup( pos );
+			if(  gr == NULL  ) {
+				continue;
+			}
+			for(  uint idx = 0;  idx < gr->obj_count();  idx++  ) {
+				gr->obj_bei( idx )->clear_flag( obj_t::highlight );
+			}
+			if(  gr->is_water()  ||  gr->ist_natur()  ) {
+				gr->clear_flag( grund_t::marked );
+			}
+			gr->set_flag( grund_t::dirty );
+		}
+	}
+	schedule_route_overlay.clear();
+	schedule_route_overlay_stops.clear();
+	schedule_route_overlay_line       = linehandle_t();
+	schedule_route_overlay_convoi     = convoihandle_t();
+	schedule_route_overlay_req_line   = linehandle_t();
+	schedule_route_overlay_req_convoi = convoihandle_t();
+	schedule_route_overlay_rep        = convoihandle_t();
+	schedule_route_overlay_color      = 0;
+}
+
+
 void karte_t::rotate90()
 {
 DBG_MESSAGE( "karte_t::rotate90()", "called" );
@@ -2373,6 +2404,11 @@ DBG_MESSAGE( "karte_t::rotate90()", "called" );
 	// clear marked region
 	zeiger->change_pos( koord3d::invalid );
 
+	// drop the schedule-route overlay: its stored tiles are pre-rotation coordinates (display only, the
+	// user re-shows the route to recompute it on the rotated map). Un-highlight the stop tiles here,
+	// while their coordinates still match, so no red highlight is left on the rotated objects.
+	clear_schedule_route_overlay( true );
+
 	// preprocessing, detach stops from factories to prevent crash
 	for(halthandle_t const s : haltestelle_t::get_alle_haltestellen()) {
 		s->release_factory_links();
@@ -3813,6 +3849,9 @@ bool karte_t::load(const char *filename)
 	gfx->set_show_load_cursor(true);
 	loadsave_t file;
 
+	// drop any schedule-route overlay from the previous game (display only, not saved)
+	clear_schedule_route_overlay( false );
+
 	// clear hash table with missing paks (may cause some small memory loss though)
 	pakset_manager_t::clear_missing_paks();
 
diff --git a/src/simutrans/world/simworld.h b/src/simutrans/world/simworld.h
index 80e55ae29..43c8846dd 100644
--- a/src/simutrans/world/simworld.h
+++ b/src/simutrans/world/simworld.h
@@ -13,6 +13,7 @@
 
 #include "../convoihandle.h"
 #include "../halthandle.h"
+#include "../linehandle.h"
 
 #include "../tpl/weighted_vector_tpl.h"
 #include "../tpl/array2d_tpl.h"
@@ -61,6 +62,21 @@ class building_desc_t;
 typedef void (karte_t::*xy_loop_func)(sint16, sint16, sint16, sint16);
 
 
+/**
+ * One tile of the transient schedule-route overlay (display only, never saved). Keeps the route's
+ * own ribi so the view can render a diagonal stretch as a straight stroke instead of a staircase,
+ * and an explicit segment break so two sub-routes split by an unroutable leg are never joined.
+ * new_segment and ribi are independent, so ribi_t::none never has to double as a break marker.
+ */
+struct schedule_route_pos_t {
+	koord3d pos;
+	uint8   ribi;        ///< ribi_t::ribi of the route at this tile (may be ribi_t::none)
+	bool    new_segment; ///< true: a routing break precedes this tile (do not join it to the previous)
+	schedule_route_pos_t() : pos(koord3d::invalid), ribi(0), new_segment(false) {}
+	schedule_route_pos_t(koord3d p, uint8 r, bool ns) : pos(p), ribi(r), new_segment(ns) {}
+};
+
+
 /**
  * The map is the central part of the simulation. It stores all data and objects.
  * @brief Stores all data and objects of the simulated world.
@@ -172,6 +188,20 @@ private:
 	zeiger_t *zeiger;
 	/** @} */
 
+	// Display-only schedule-route overlay (never saved). All parts are cleared together by
+	// clear_schedule_route_overlay(). The EFFECTIVE source (whose schedule is drawn) is a line or a
+	// lineless convoy; the REQUESTER (window subject that asked to show it) is tracked separately so
+	// closing that window clears the overlay while a convoy shown via its line still keeps the line's
+	// pressed state. Safe handles are stored so a destroyed source/requester leaves no dangling pointer.
+	vector_tpl<schedule_route_pos_t> schedule_route_overlay;
+	vector_tpl<koord3d> schedule_route_overlay_stops;
+	linehandle_t   schedule_route_overlay_line;    // effective source: a line
+	convoihandle_t schedule_route_overlay_convoi;  // effective source: a lineless convoy
+	linehandle_t   schedule_route_overlay_req_line;    // requester: a line window
+	convoihandle_t schedule_route_overlay_req_convoi;  // requester: a convoy window
+	convoihandle_t schedule_route_overlay_rep;         // the convoy the drawn route was computed from
+	uint8 schedule_route_overlay_color;
+
 	/**
 	 * Time when last mouse moved to check for ambient sound events.
 	 */
@@ -719,6 +749,49 @@ public:
 	 */
 	zeiger_t * get_zeiger() const { return zeiger; }
 
+	/// Ordered tiles of the schedule-route overlay (display only). Written by the overlay tool, read by the view.
+	const vector_tpl<schedule_route_pos_t>& get_schedule_route_overlay() const { return schedule_route_overlay; }
+	vector_tpl<schedule_route_pos_t>& access_schedule_route_overlay() { return schedule_route_overlay; }
+	const vector_tpl<koord3d>& get_schedule_route_overlay_stops() const { return schedule_route_overlay_stops; }
+	vector_tpl<koord3d>& access_schedule_route_overlay_stops() { return schedule_route_overlay_stops; }
+	uint8 get_schedule_route_overlay_color() const { return schedule_route_overlay_color; }
+	void set_schedule_route_overlay_color(uint8 c) { schedule_route_overlay_color = c; }
+	// Effective source of the shown overlay (the schedule being drawn): at most one bound.
+	linehandle_t   get_schedule_route_overlay_line() const { return schedule_route_overlay_line; }
+	void set_schedule_route_overlay_line(linehandle_t l) { schedule_route_overlay_line = l; }
+	convoihandle_t get_schedule_route_overlay_convoi() const { return schedule_route_overlay_convoi; }
+	void set_schedule_route_overlay_convoi(convoihandle_t c) { schedule_route_overlay_convoi = c; }
+	// Requester (window subject that asked to show it): closing that window clears the overlay.
+	linehandle_t   get_schedule_route_overlay_req_line() const { return schedule_route_overlay_req_line; }
+	void set_schedule_route_overlay_req_line(linehandle_t l) { schedule_route_overlay_req_line = l; }
+	convoihandle_t get_schedule_route_overlay_req_convoi() const { return schedule_route_overlay_req_convoi; }
+	void set_schedule_route_overlay_req_convoi(convoihandle_t c) { schedule_route_overlay_req_convoi = c; }
+	// The convoy whose descriptor/context the drawn line route was computed from (its representative).
+	convoihandle_t get_schedule_route_overlay_rep() const { return schedule_route_overlay_rep; }
+	void set_schedule_route_overlay_rep(convoihandle_t c) { schedule_route_overlay_rep = c; }
+	/**
+	 * Drop the whole schedule-route overlay: route tiles, stop tiles, source + requester identity and the
+	 * colour. When @p clear_highlight, also unset obj_t::highlight / grund_t::marked on the current stop
+	 * tiles (do this while the map is stable; skip it on world teardown in init()/load()). Display only.
+	 */
+	void clear_schedule_route_overlay( bool clear_highlight );
+
+	/**
+	 * Central lifecycle notifications for the display-only schedule-route overlay (defined in
+	 * tool/simtool.cc, next to the probe/refresh logic). Called from the single convoy-removal chokepoints
+	 * so every deletion route is covered without per-tool hooks:
+	 *  - schedule_route_notify_line_convoi_removed(): from simline_t::remove_convoy(), after the list
+	 *    reflects the removal; recomputes the shown line with its next representative (or clears) only when
+	 *    the convoy that left was the drawn representative.
+	 *  - schedule_route_notify_convoi_changed(): from convoi_t::set_line()/set_schedule(), after the final
+	 *    membership and schedule are visible; makes a convoy requester follow direct changes from any caller.
+	 *  - schedule_route_notify_convoi_destroyed(): from convoi_t::destroy(); clears the overlay when the
+	 *    destroyed convoy was the requester. All are no-ops during world teardown (is_destroying()).
+	 */
+	void schedule_route_notify_line_convoi_removed( linehandle_t line, convoihandle_t cnv );
+	void schedule_route_notify_convoi_changed( convoihandle_t cnv );
+	void schedule_route_notify_convoi_destroyed( convoihandle_t cnv );
+
 	/**
 	 * Marks an area using the grund_t mark flag.
 	 */
