diff --git a/src/simutrans/display/simview.cc b/src/simutrans/display/simview.cc
index fcbb2b19f..e68c8e708 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"          // line-route overlay: travel direction per segment
+#include "../vehicle/vehicle_base.h"  // line-route overlay: reuse the lane offset table
 #include "simview.h"
 #include "simgraph.h"
 #include "viewport.h"
@@ -294,6 +297,94 @@ void main_view_t::display(bool force_dirty)
 		}
 	}
 
+	// line-route overlay: draw the selected line's real path as directional lane strokes in the
+	// owning player's colour (display only). Each segment is shifted toward the lane used for its
+	// own travel direction (driving-side aware); because the lane table is antisymmetric, the
+	// outward and return legs land on opposite lanes automatically. The scheduled stops keep their
+	// own obj_t::highlight channel, so the route never collides with the schedule-stop highlight.
+	{
+		const vector_tpl<koord3d>& route = welt->get_line_route_overlay();
+		if(  route.get_count() > 1  ) {
+			// Two coherent shades derived once per frame from the owning player's colour ramp: a bright
+			// core (ramp base + the GUI "bright" offset) that stays legible on dark terrain, and a
+			// neutral-dark outline that separates the stroke from light terrain. The stored colour is
+			// the ramp base (owning player's colour); reuses the existing player-colour ramp, no new
+			// setting. The addition stays within the 8-shade ramp and never underflows.
+			const palette_index_t col_idx = welt->get_line_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 and ~19/32 down, so the stroke
+			// rests on the way surface (near the engine's own on-way signal anchor of 9/16) instead of
+			// floating above it.
+			// 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 );
+
+			// Two route tiles are one leg only if both are valid, 8-neighbours and distinct; a
+			// koord3d::invalid entry is an explicit break between routable sub-legs (a failed leg), and
+			// anything non-adjacent ends the stroke too, so the bevel / U-cap never joins across a gap.
+			auto adjacent = []( const koord3d& p, const koord3d& q ) -> bool {
+				if(  p == koord3d::invalid  ||  q == koord3d::invalid  ) {
+					return false;
+				}
+				const koord dd = q.get_2d() - p.get_2d();
+				return dd.x >= -1  &&  dd.x <= 1  &&  dd.y >= -1  &&  dd.y <= 1  &&  ( dd.x != 0  ||  dd.y != 0 );
+			};
+			// Screen direction 0..7 of a leg, or -1 if the two tiles are not a valid adjacent leg.
+			auto leg_dir = [&]( const koord3d& p, const koord3d& q ) -> int {
+				return adjacent( p, q ) ? (int)ribi_t::get_dir( ribi_type( p, q ) ) : -1;
+			};
+			// Coherent lane offset (screen px, zoom- and driving-side-scaled) at the node where a leg
+			// travelling leg_d meets the other leg other_d (-1 if none). An endpoint or a 180 reversal
+			// keeps this leg's own antisymmetric side, so outward and return stay on opposite parallel
+			// lanes (joined by a short U-cap); any other turn bevels to the integer average of the two
+			// laterals, so BOTH segments meeting at the node resolve to the SAME point -> no crossing,
+			// no gap. On a straight run the two directions are equal, so the average is a no-op.
+			auto node_off = [&]( int leg_d, int other_d ) -> scr_coord {
+				sint8 bx = vehicle_base_t::get_driveleft_base_offset( leg_d, 0 );
+				sint8 by = vehicle_base_t::get_driveleft_base_offset( leg_d, 1 );
+				if(  other_d >= 0  &&  other_d != ( ( leg_d + 4 ) & 7 )  ) {
+					bx = (sint8)( ( bx + vehicle_base_t::get_driveleft_base_offset( other_d, 0 ) ) / 2 );
+					by = (sint8)( ( by + vehicle_base_t::get_driveleft_base_offset( other_d, 1 ) ) / 2 );
+				}
+				return scr_coord( tile_raster_scale_x( lane_sign * bx, IMG_SIZE ),
+				                  tile_raster_scale_y( lane_sign * by, IMG_SIZE ) );
+			};
+			// 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 );
+			};
+
+			for(  uint32 i = 1;  i < route.get_count();  i++  ) {
+				const koord3d a = route[i-1];
+				const koord3d b = route[i];
+				const int dir = leg_dir( a, b );
+				if(  dir < 0  ) {
+					continue; // not adjacent tiles: leg gap / broken leg -> no stroke across it
+				}
+				// Resolve the lane offset at each end per NODE, from both legs meeting there, so a turn
+				// bevels to a single shared point instead of two segments crossing the centre line (the
+				// old per-segment offset produced an X at corners and at there-and-back reversals).
+				const int dir_prev = ( i >= 2 ) ? leg_dir( route[i-2], a ) : -1;
+				const int dir_next = ( i + 1 < route.get_count() ) ? leg_dir( b, route[i+1] ) : -1;
+				const scr_coord pa = viewport->get_screen_coord( a ) + band + node_off( dir, dir_prev );
+				const scr_coord pb = viewport->get_screen_coord( b ) + band + node_off( dir, dir_next );
+				stroke( pa, pb );
+				// rounded caps; the end dot also hides any 1-px rasterisation gap at a bevel node
+				gfx->draw_filled_circle( pa.x, pa.y, 1, col_main );
+				gfx->draw_filled_circle( pb.x, pb.y, 1, col_main );
+				// 180 reversal at b (there-and-back turnaround): the return leg starts on the opposite
+				// lane; a short U-cap across the road joins the two parallel lanes into one route.
+				if(  dir_next >= 0  &&  dir_next == ( ( dir + 4 ) & 7 )  ) {
+					stroke( pb, viewport->get_screen_coord( b ) + band + node_off( dir_next, dir ) );
+				}
+			}
+		}
+	}
+
 	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/line_management_gui.cc b/src/simutrans/gui/line_management_gui.cc
index ca99cb21a..aa61e78f5 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,13 @@ 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);
 
+	// toggle: draw the line's real route (engine pathfinder) on the main map. 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 +246,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 act as the test driver
+		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_line_route_overlay_line() == line );
+
 		if(  line->count_convoys() != old_convoi_count  ) {
 
 			old_convoi_count = line->count_convoys();
@@ -404,6 +419,29 @@ void line_management_gui_t::apply_schedule()
 }
 
 
+// The "Show route on map" button action. The GUI does NOT calculate the route: it only issues a
+// request to tool_line_route_overlay_t, which runs the pathfinder in a proper step (not in sync_step)
+// and applies the highlight. The world is the source of truth, so we toggle against it: if this line
+// already owns the overlay, clear it ("c"); otherwise show this line's route ("s,<id>").
+void line_management_gui_t::toggle_route_overlay()
+{
+	const bool already_shown = ( welt->get_line_route_overlay_line() == line );
+	tool_t *tool = create_tool( TOOL_LINE_ROUTE_OVERLAY | SIMPLE_TOOL );
+	cbuffer_t buf;
+	if(  !already_shown  &&  line.is_bound()  ) {
+		buf.printf( "s,%u", line.get_id() );
+	}
+	else {
+		buf.printf( "c" );
+	}
+	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 +496,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 +558,15 @@ 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 route overlay so no highlight flags are left on the world (via the tool, so the
+		// clear happens in a proper step) -- but ONLY if this window's line is the one currently shown,
+		// so closing a line window never wipes another window's overlay; skip on game shutdown
+		if(  !is_saving_gui  &&  welt->get_line_route_overlay_line() == line  ) {
+			tool_t *tool = create_tool( TOOL_LINE_ROUTE_OVERLAY | SIMPLE_TOOL );
+			tool->set_default_param( "c" );
+			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..066001ecc 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,15 @@ 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;
 
+	// The "Show route on map" button action: issue a request to tool_line_route_overlay_t (the route
+	// is computed and highlighted in a proper step, never in GUI code / sync_step). Whether this
+	// line's overlay is currently shown is read from karte_t, not stored here, so two line windows
+	// stay consistent.
+	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/tool/simmenu.cc b/src/simutrans/tool/simmenu.cc
index 8f219db4f..74fb48c89 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_LINE_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_LINE_ROUTE_OVERLAY:   tool = new tool_line_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..0f94af0a1 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_LINE_ROUTE_OVERLAY,
 	SIMPLE_TOOL_COUNT,
 	SIMPLE_TOOL = 0x2000
 };
diff --git a/src/simutrans/tool/simtool.cc b/src/simutrans/tool/simtool.cc
index 9d568781f..a641376cf 100644
--- a/src/simutrans/tool/simtool.cc
+++ b/src/simutrans/tool/simtool.cc
@@ -76,6 +76,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"
@@ -8977,6 +8979,136 @@ bool tool_work_world_t::init(player_t*)
 }
 
 
+// ---- tool_line_route_overlay_t: a line's real route drawn on the main map -------------------
+// Two independent channels (prissi, forum 24000): the route PATH is drawn procedurally by the map
+// view from welt->get_line_route_overlay() in the owning player's colour, and the scheduled STOPS
+// keep the existing obj_t::highlight bit (their own colour) so they stay distinguishable and
+// selectable. Because the route no longer touches the highlight bit, it can no longer collide with
+// the schedule-stop highlight. Display-only, transient: nothing here is saved.
+// All transient overlay state (route tiles, stop tiles, shown-line identity, colour) lives on
+// karte_t and is dropped together by karte_t::clear_line_route_overlay(). This helper only SETS the
+// obj_t::highlight on the stop tiles collected there; clearing is owned by karte_t.
+static void line_route_set_stop_highlight()
+{
+	karte_t *welt = world();
+	for(  koord3d const& pos : welt->get_line_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 );
+	}
+}
+
+// Compute the line's real path leg by leg and store it, in order, in welt->access_line_route_overlay()
+// for the view to draw; also collect the scheduled stop tiles. The test driver is the line's first
+// convoy's lead vehicle (it *is* a test_driver_t), so the trace honours that convoy's waytype,
+// electrification and one-way rules. This runs during a step (from the command queue), never in the
+// display loop: the A* node array is static and not reentrant.
+static void line_route_compute(linehandle_t line)
+{
+	karte_t *welt = world();
+	vector_tpl<koord3d>& path = welt->access_line_route_overlay();
+	// path and stops were already emptied by clear_line_route_overlay() in init()
+	if(  !line.is_bound()  ||  line->count_convoys() == 0  ) {
+		return;
+	}
+	convoihandle_t cnv = line->get_convoy(0);
+	if(  !cnv.is_bound()  ||  cnv->get_vehicle_count() == 0  ) {
+		return;
+	}
+	schedule_t *schedule = line->get_schedule();
+	if(  schedule == NULL  ||  schedule->get_count() < 2  ) {
+		return;
+	}
+	// stops keep their own highlight channel: remember the schedule entry tiles
+	for(  uint8 i = 0;  i < schedule->get_count();  i++  ) {
+		welt->access_line_route_overlay_stops().append_unique( schedule->entries[i].pos );
+	}
+	// route colour = owning player's colour ramp base (per-company distinct, minimap convention)
+	if(  line->get_owner()  ) {
+		welt->set_line_route_overlay_color( line->get_owner()->get_player_color1() );
+	}
+	vehicle_t *test_driver = cnv->front();
+	const sint32 max_speed = cnv->get_min_top_speed();
+	const uint8 count = schedule->get_count();
+	route_t seg;
+	// the schedule is cyclic, so also connect the last entry back to the first
+	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; // repeated stop: zero-length leg
+		}
+		if(  seg.calc_route(welt, start, ziel, test_driver, max_speed, 0) != route_t::no_route  ) {
+			// keep the path ORDERED (not deduped) so the view can draw tile-to-tile segments;
+			// only drop a tile equal to the previous one (leg boundaries share the stop tile)
+			for(  uint32 n = 0;  n < seg.get_count();  n++  ) {
+				const koord3d t = seg.at(n);
+				if(  path.get_count() == 0  ||  path[path.get_count()-1] != t  ) {
+					path.append( t );
+				}
+			}
+		}
+		else if(  path.get_count() > 0  &&  path[path.get_count()-1] != koord3d::invalid  ) {
+			// leg could not be routed: push an explicit break so the view never joins the sub-route
+			// before it to the one after it (the bevel / U-cap must not cross this)
+			path.append( koord3d::invalid );
+		}
+	}
+	// a trailing break (the last leg failed) carries no segment: drop it to keep the vector tidy
+	if(  path.get_count() > 0  &&  path[path.get_count()-1] == koord3d::invalid  ) {
+		path.remove_at( path.get_count()-1 );
+	}
+}
+
+bool tool_line_route_overlay_t::init(player_t*)
+{
+	karte_t *welt = world();
+	// default_param: "s,<line_id>" show that line's route, "c" (or anything else) clear
+	const char *p = default_param;
+	while(  p  &&  *p  &&  *p <= ' '  ) {
+		p++;
+	}
+	const char cmd = (p  &&  *p) ? *p : 'c';
+
+	// always drop the previous overlay first: this un-highlights the old stop tiles and clears the
+	// route path, the stop list, the shown-line identity and the colour, all in one place
+	welt->clear_line_route_overlay( true );
+
+	if(  cmd == 's'  ) {
+		// skip up to and past the comma to reach the id
+		while(  *p  &&  *p++ != ','  ) {
+		}
+		linehandle_t line;
+		line.set_id( (uint16)atoi(p) );
+		if(  line.is_bound()  ) {
+			line_route_compute( line );
+			if(  welt->get_line_route_overlay().get_count() > 0  ) {
+				welt->set_line_route_overlay_line( line ); // this line now owns the overlay
+				line_route_set_stop_highlight();
+			}
+			else {
+				// nothing routable to show (no convoy / every leg broken): keep the overlay empty and
+				// unbound, dropping the stop tiles compute may have collected
+				welt->clear_line_route_overlay( false );
+			}
+		}
+	}
+	welt->set_dirty(); // repaint so the drawn route appears / disappears
+	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..2649d33d4 100644
--- a/src/simutrans/tool/simtool.h
+++ b/src/simutrans/tool/simtool.h
@@ -1037,6 +1037,22 @@ public:
 	bool is_work_keeps_game_state() const OVERRIDE { return false; }
 };
 
+/**
+ * Display-only overlay: compute and highlight a line's real route on the main map.
+ * The pathfinder runs here, during a proper step -- never in GUI code / sync_step, which is
+ * enforced by is_init_keeps_game_state()==false (the request is queued from the GUI and executed
+ * from the command queue between steps). It is local/per-client (WFL_LOCAL): nothing is sent over
+ * the network, nothing is saved. The GUI only issues the request; this tool does the work.
+ * default_param: "s,<line_id>" shows that line's route, "c" clears the overlay.
+ */
+class tool_line_route_overlay_t : public tool_t {
+public:
+	tool_line_route_overlay_t() : tool_t(TOOL_LINE_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..959f4e0e0 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_line_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;
+	line_route_overlay_color = 0; // display-only line-route overlay (empty until a line is shown)
 	nosave_warning = nosave = false;
 	loaded_rotation = 0;
 	last_year = 1930;
@@ -2361,6 +2363,31 @@ void karte_t::rotate90_plans(sint16 x_min, sint16 x_max, sint16 y_min, sint16 y_
 }
 
 
+void karte_t::clear_line_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 : line_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 );
+		}
+	}
+	line_route_overlay.clear();
+	line_route_overlay_stops.clear();
+	line_route_overlay_line = linehandle_t();
+	line_route_overlay_color = 0;
+}
+
+
 void karte_t::rotate90()
 {
 DBG_MESSAGE( "karte_t::rotate90()", "called" );
@@ -2373,6 +2400,11 @@ DBG_MESSAGE( "karte_t::rotate90()", "called" );
 	// clear marked region
 	zeiger->change_pos( koord3d::invalid );
 
+	// drop the line-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_line_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 +3845,9 @@ bool karte_t::load(const char *filename)
 	gfx->set_show_load_cursor(true);
 	loadsave_t file;
 
+	// drop any line-route overlay from the previous game (display only, not saved)
+	clear_line_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..e4a2700c8 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"
@@ -172,6 +173,19 @@ private:
 	zeiger_t *zeiger;
 	/** @} */
 
+	/**
+	 * Line-route overlay currently shown on the main map (display only, never saved). Three parts,
+	 * always cleared together by clear_line_route_overlay(): the ordered route tiles the view draws
+	 * (koord3d::invalid marks a break between routable sub-legs), the scheduled stop tiles that carry
+	 * the obj_t::highlight red highlight, and the identity of the line being shown (so a second line
+	 * window cannot clear the first one's overlay). Empty / unbound when no overlay is shown.
+	 */
+	vector_tpl<koord3d> line_route_overlay;
+	vector_tpl<koord3d> line_route_overlay_stops;
+	linehandle_t line_route_overlay_line;
+	/// Colour index (owning player's colour ramp base) the overlay route is drawn in.
+	uint8 line_route_overlay_color;
+
 	/**
 	 * Time when last mouse moved to check for ambient sound events.
 	 */
@@ -719,6 +733,23 @@ public:
 	 */
 	zeiger_t * get_zeiger() const { return zeiger; }
 
+	/// Ordered tiles of the line-route overlay (display only). Written by the overlay tool, read by the view.
+	const vector_tpl<koord3d>& get_line_route_overlay() const { return line_route_overlay; }
+	vector_tpl<koord3d>& access_line_route_overlay() { return line_route_overlay; }
+	const vector_tpl<koord3d>& get_line_route_overlay_stops() const { return line_route_overlay_stops; }
+	vector_tpl<koord3d>& access_line_route_overlay_stops() { return line_route_overlay_stops; }
+	uint8 get_line_route_overlay_color() const { return line_route_overlay_color; }
+	void set_line_route_overlay_color(uint8 c) { line_route_overlay_color = c; }
+	linehandle_t get_line_route_overlay_line() const { return line_route_overlay_line; }
+	void set_line_route_overlay_line(linehandle_t l) { line_route_overlay_line = l; }
+	/**
+	 * Drop the whole line-route overlay: route tiles, stop tiles and the shown-line identity, and
+	 * reset 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, e.g. on re-show or rotate90; skip it on
+	 * world teardown in init()/load(), where the tiles belong to the outgoing world). Display only.
+	 */
+	void clear_line_route_overlay( bool clear_highlight );
+
 	/**
 	 * Marks an area using the grund_t mark flag.
 	 */
