diff --git a/crates/engine/src/settings.rs b/crates/engine/src/settings.rs
--- a/crates/engine/src/settings.rs
+++ b/crates/engine/src/settings.rs
@@ -714,7 +714,7 @@ pub struct TimelinePrefs {
     pub still_image_unit: String,
     /// "Timeline Playback Auto-Scrolling": `noScroll` | `pageScroll` | `smoothScroll`.
     pub auto_scroll: String,
-    /// "Timeline Mouse Scrolling": `vertical` | `horizontal`.
+    /// "Timeline Mouse Scrolling": `vertical` (the default on macOS) | `horizontal` (elsewhere, as Premiere on Windows).
     pub mouse_scrolling: String,
     /// Default Audio Tracks per media type: `useFile` | `mono` | `stereo` | `5.1` | `adaptive`.
     pub mono_media_tracks: String,
@@ -744,7 +744,8 @@ impl Default for TimelinePrefs {
             still_image_duration: 5.0,
             still_image_unit: "seconds".into(),
             auto_scroll: "pageScroll".into(),
-            mouse_scrolling: "vertical".into(),
+            // Premiere's defaults: Vertical on macOS, Horizontal on Windows (Ctrl + wheel then scrolls the tracks)
+            mouse_scrolling: if cfg!(target_os = "macos") { "vertical" } else { "horizontal" }.into(),
             mono_media_tracks: "useFile".into(),
             stereo_media_tracks: "useFile".into(),
             surround_media_tracks: "useFile".into(),
diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs
--- a/crates/ui-egui/src/panels/timeline.rs
+++ b/crates/ui-egui/src/panels/timeline.rs
@@ -29,6 +29,11 @@ pub use super::timeline_hit::{Hit, ROLL_PX, hit, selection_trim_kind};
 const TOP_H: f32 = 58.0; // timecode + toolbar (left) / ruler (right)
 const RULER_H: f32 = 44.0;
 const SCROLLBAR_H: f32 = 17.0;
+/// Track height gained per point of Shift or Option + wheel over a track header (a wheel notch is 40 points).
+const TRACK_H_PER_WHEEL_POINT: f32 = 0.25;
+/// The shortest and tallest a track gets by hand (dragging its header edge, or the wheel).
+const TRACK_H_MIN: f32 = 22.0;
+const TRACK_H_MAX: f32 = 220.0;
 const DIVIDER_H: f32 = 5.0;
 const MASTER_H: f32 = 34.0;
 /// The selected gap between two clips (#648, #668).
@@ -277,6 +282,9 @@ pub struct Layout {
     pub scroll: f64,
     /// The y of the divider between the video tracks (above) and the audio tracks (below).
     pub split_y: f32,
+    /// Where the video and the audio rows show (a row scrolled out of view lies outside its span).
+    pub video_band: egui::Rangef,
+    pub audio_band: egui::Rangef,
 }
 
 impl Layout {
@@ -524,7 +532,7 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
         let rect = Rect::from_min_max(pos2(content.min.x, top), pos2(content.max.x, top + ah));
         rows.push(Row { track: tr.id, kind: TrackKind::Audio, index: i, rect, lane: app.ui.timeline.track_lanes.contains_key(&tr.id.0) });
     }
-    let layout = Layout { content, ruler, rows: rows.clone(), pps, scroll, split_y };
+    let layout = Layout { content, ruler, rows: rows.clone(), pps, scroll, split_y, video_band: video_area.y_range(), audio_band: audio_area.y_range() };
     app.tl.layout = Some(layout.clone());
 
     // ---- backgrounds
@@ -781,13 +789,7 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
     }
 
     // ---- scroll bars
-    zoom_scrollbar(
-        app,
-        ui,
-        Rect::from_min_max(pos2(content.min.x, rect.max.y - SCROLLBAR_H + 2.0), pos2(content.max.x, rect.max.y - 2.0)),
-        seq.duration().seconds(),
-        &t,
-    );
+    zoom_scrollbar(app, ui, zoom_bar_rect(rect, content), seq.duration().seconds(), &t);
     vertical_scrollbar(
         ui,
         Rect::from_min_max(pos2(content.max.x, video_area.min.y), pos2(rect.max.x, video_area.max.y)),
@@ -1565,7 +1567,7 @@ fn draw_headers(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, rows:
             if eresp.dragged() {
                 let d = eresp.drag_delta().y * if r.kind == TrackKind::Video { -1.0 } else { 1.0 };
                 let h = if r.kind == TrackKind::Video { &mut app.ui.timeline.video_track_h } else { &mut app.ui.timeline.audio_track_h };
-                *h = (*h + d).clamp(22.0, 220.0);
+                *h = (*h + d).clamp(TRACK_H_MIN, TRACK_H_MAX);
             }
         }
         let resp = ui.interact(
@@ -1832,6 +1834,11 @@ fn zoom_thumb(bar: Rect, seq_seconds: f64, scroll: f64, pps: f64) -> (f32, f32,
     (bar.min.x + at, w, if travel > 0.5 { range / travel as f64 } else { 0.0 })
 }
 
+/// The zoom scroll bar along the bottom of the Timeline, under the tracks.
+fn zoom_bar_rect(rect: Rect, content: Rect) -> Rect {
+    Rect::from_min_max(pos2(content.min.x, rect.max.y - SCROLLBAR_H + 2.0), pos2(content.max.x, rect.max.y - 2.0))
+}
+
 /// Draw the zoom scroll bar: the thumb reflects the visible range, a press beside it centres the thumb under the pointer, and the thumb and track are
 /// registered for automation.
 fn zoom_scrollbar(app: &mut FilmcraftApp, ui: &mut egui::Ui, bar: Rect, seq_seconds: f64, t: &Tokens) {
@@ -2458,22 +2465,57 @@ fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, layout: &
         app.tl.pressed = Some(Pressed { pos: p, tick: layout.tick_at(p.x), grab });
     }
 
-    // ---- wheel, as in Premiere Pro on macOS (checked in 26.5.2):
-    //   wheel            the tracks under the pointer, up and down
-    //   Cmd + wheel      the Timeline, sideways
-    //   Option + wheel   zoom about the pointer
-    // Settings ▸ Timeline ▸ Timeline Mouse Scrolling "Horizontal" swaps the first two. A sideways
-    // gesture (trackpad swipe, tilt wheel, Shift + wheel) always scrolls sideways; a pinch zooms.
+    // ---- wheel, as in Premiere Pro (macOS checked in 26.5.2):
+    //   wheel                   the tracks under the pointer, up and down
+    //   Cmd (Ctrl) + wheel      the Timeline, sideways
+    //   Option (Alt) + wheel    zoom about the pointer
+    //   wheel over the zoom scroll bar          zoom about the playhead
+    //   Shift or Option + wheel over a header   that kind of track taller or shorter
+    // Settings ▸ Timeline ▸ Timeline Mouse Scrolling "Horizontal" swaps the first two; it is
+    // Premiere's default on Windows (and ours off macOS). A sideways gesture (trackpad swipe, tilt
+    // wheel, Shift + wheel over the tracks) always scrolls sideways; a pinch zooms.
     if ui.rect_contains_pointer(rect)
         && let Some(p) = ctx.pointer_hover_pos()
     {
         let w = wheel_input(&ctx);
-        if (w.pinch - 1.0).abs() > 1e-4 || (w.alt && w.delta.y != 0.0) {
-            let f = if (w.pinch - 1.0).abs() > 1e-4 { w.pinch as f64 } else { (1.0_f64 + w.delta.y as f64 * 0.01).clamp(0.5, 2.0) };
-            let anchor_t = layout.tick_at(p.x).seconds();
+        let pinch = (w.pinch - 1.0).abs() > 1e-4;
+        // macOS turns Shift + wheel into a sideways wheel before anyone sees it: take either axis
+        let notch = if w.delta.y.abs() >= w.delta.x.abs() { w.delta.y } else { w.delta.x };
+        let on_zoom_bar = zoom_bar_rect(rect, layout.content).contains(p) && notch != 0.0 && (w.delta.y.abs() >= w.delta.x.abs() || w.shift);
+        let resizing = p.x < layout.content.min.x && (w.shift || w.alt) && !pinch;
+        if resizing {
+            // only a row that shows: one scrolled out of view lies under the captions, the Mix row or the divider
+            let kind = if layout.video_band.contains(p.y) {
+                Some(TrackKind::Video)
+            } else if layout.audio_band.contains(p.y) {
+                Some(TrackKind::Audio)
+            } else {
+                None
+            };
+            let row = layout.rows.iter().find(|r| Some(r.kind) == kind && r.rect.y_range().contains(p.y));
+            // FilmCraft keeps one height for all video tracks and one for all audio tracks; over
+            // header space with no track the wheel does nothing
+            if let Some(r) = row {
+                let h = if r.kind == TrackKind::Video { &mut app.ui.timeline.video_track_h } else { &mut app.ui.timeline.audio_track_h };
+                *h = (*h + notch * TRACK_H_PER_WHEEL_POINT).clamp(TRACK_H_MIN, TRACK_H_MAX);
+            }
+        } else if pinch || (w.alt && w.delta.y != 0.0) || on_zoom_bar {
+            let f = if pinch { w.pinch as f64 } else { (1.0_f64 + if on_zoom_bar { notch } else { w.delta.y } as f64 * 0.01).clamp(0.5, 2.0) };
+            let anchor = if on_zoom_bar && !w.alt && !pinch {
+                // about the playhead, or the middle of the view when the playhead is out of it
+                let ph = app.session.playhead();
+                let x = layout.x_of(ph);
+                if layout.content.x_range().contains(x) {
+                    (ph.seconds(), x)
+                } else {
+                    (layout.tick_at(layout.content.center().x).seconds(), layout.content.center().x)
+                }
+            } else {
+                (layout.tick_at(p.x).seconds(), p.x)
+            };
             let v = &mut app.ui.timeline;
             v.target_pps = (v.target_pps * f).clamp(0.05, 24_000.0);
-            app.tl.zoom_anchor = Some((anchor_t, p.x));
+            app.tl.zoom_anchor = Some(anchor);
         } else if w.delta != egui::Vec2::ZERO {
             let wheel_sideways = (app.session.prefs.timeline.mouse_scrolling == "horizontal") != w.command;
             let sideways = if w.delta.x.abs() > w.delta.y.abs() {
@@ -3595,6 +3637,8 @@ mod new_track_tests {
             pps: 100.0,
             scroll: 0.0,
             split_y: 200.0,
+            video_band: egui::Rangef::new(0.0, 200.0),
+            audio_band: egui::Rangef::new(205.0, 500.0),
         };
         let at = |y| new_track_at(&seq, &layout, y, 50.0, 50.0);
         assert_eq!(at(40.0), Some((TrackKind::Video, Rect::from_min_max(pos2(0.0, 100.0), pos2(800.0, 150.0)))));
@@ -3635,6 +3679,8 @@ mod new_track_tests {
             pps: 100.0,
             scroll: 0.0,
             split_y: 150.0,
+            video_band: egui::Rangef::new(0.0, 150.0),
+            audio_band: egui::Rangef::new(155.0, 500.0),
         };
         assert_eq!(new_track_at(&seq, &layout, 10.0, 50.0, 50.0), Some((TrackKind::Video, Rect::from_min_max(pos2(0.0, 0.0), pos2(800.0, 50.0)))));
         // scrolled to the bottom, V1..V3 fill the area as before
diff --git a/crates/ui-egui/src/panels/timeline_hit.rs b/crates/ui-egui/src/panels/timeline_hit.rs
--- a/crates/ui-egui/src/panels/timeline_hit.rs
+++ b/crates/ui-egui/src/panels/timeline_hit.rs
@@ -386,6 +386,8 @@ mod tests {
             pps: 100.0,
             scroll: 0.0,
             split_y: TOP + 60.0,
+            video_band: row.y_range(),
+            audio_band: egui::Rangef::new(TOP + 60.0, TOP + 60.0),
         };
         (seq, layout, track)
     }
diff --git a/crates/ui-egui/tests/it/timeline_scroll_ui.rs b/crates/ui-egui/tests/it/timeline_scroll_ui.rs
--- a/crates/ui-egui/tests/it/timeline_scroll_ui.rs
+++ b/crates/ui-egui/tests/it/timeline_scroll_ui.rs
@@ -1,7 +1,9 @@
 //! Headless UI tests of scrolling the Timeline: what the wheel does with each modifier, how far
 //! the view can go, and the zoom scroll bar. Premiere Pro 26.5.2 on macOS, checked in the app: the
 //! wheel moves the tracks up and down, Cmd + wheel moves the Timeline sideways, Option + wheel
-//! zooms, and the view stops ten minutes past the end of the sequence.
+//! zooms, and the view stops ten minutes past the end of the sequence. Adobe's Timeline
+//! preferences page: Timeline Mouse Scrolling is Vertical on macOS and Horizontal on Windows; its
+//! Timeline navigation page: the wheel over the zoom scroll bar zooms, centred on the playhead.
 
 use std::sync::mpsc::{Sender, channel};
 
@@ -97,6 +99,8 @@ impl Driver {
     /// The demo sequence with enough tracks to scroll both halves, at a known zoom.
     fn tall() -> Driver {
         let mut d = Driver::demo();
+        // the macOS default, whatever this runs on (the other is checked on its own below)
+        d.app().session.prefs.timeline.mouse_scrolling = "vertical".into();
         d.exec("sequence.addTracks", json!({"video": 12, "audio": 12}));
         d.ok("ui.set", json!({"timeline": {"pps": 50.0, "scroll": 4.0}}));
         d.frames(4);
@@ -104,6 +108,18 @@ impl Driver {
         d
     }
 
+    /// Turn the wheel over a named element (a track header, the zoom scroll bar), with modifiers.
+    fn wheel_on(&mut self, id: &str, dx: f64, dy: f64, modifiers: Value) {
+        self.ok("ui.scroll", json!({"id": id, "dx": dx, "dy": dy, "modifiers": modifiers}));
+        self.frames(3);
+    }
+
+    /// (video track height, audio track height).
+    fn heights(&mut self) -> (f32, f32) {
+        let v = &self.app().ui.timeline;
+        (v.video_track_h, v.audio_track_h)
+    }
+
     fn seconds(&mut self) -> f64 {
         self.app().session.active_sequence().unwrap().duration().seconds()
     }
@@ -168,8 +184,9 @@ fn cmd_wheel_moves_the_timeline_sideways_and_option_wheel_zooms() {
 /// sideways and Cmd + wheel moves the tracks.
 #[test]
 fn the_mouse_scrolling_setting_swaps_wheel_and_cmd_wheel() {
+    let want = if cfg!(target_os = "macos") { "vertical" } else { "horizontal" };
+    assert_eq!(filmcraft_engine::settings::TimelinePrefs::default().mouse_scrolling, want, "Premiere's default: Vertical on macOS, Horizontal on Windows");
     let mut d = Driver::tall();
-    assert_eq!(d.app().session.prefs.timeline.mouse_scrolling, "vertical", "Premiere's default on macOS");
     d.app().session.prefs.timeline.mouse_scrolling = "horizontal".into();
     d.wheel(0.2, 0.0, -100.0, none());
     assert_eq!(d.view(), (6.0, 50.0, 0.0, 0.0));
@@ -179,6 +196,82 @@ fn the_mouse_scrolling_setting_swaps_wheel_and_cmd_wheel() {
     assert!((d.view().1 - 60.0).abs() < 1e-6, "Option + wheel zooms either way");
 }
 
+/// Adobe: "By positioning the mouse over the bar, you can scroll the mouse wheel to expand and
+/// contract the bar", and "expanding and contracting the bar is centered on the playhead".
+#[test]
+fn the_wheel_over_the_zoom_scroll_bar_zooms_about_the_playhead() {
+    let mut d = Driver::tall();
+    // the playhead a long way left of the bar's middle, where the pointer is
+    d.exec("playhead.set", json!({"seconds": 6.0}));
+    d.frames(3);
+    let playhead_x = |scroll: f64, pps: f64| (6.0 - scroll) * pps;
+    let before = playhead_x(4.0, 50.0);
+    d.wheel_on("timeline.zoomBar.track", 0.0, 20.0, none());
+    let (scroll, pps, v, a) = d.view();
+    assert!((pps - 60.0).abs() < 1e-6, "zoomed in by a fifth: {pps}");
+    assert!((playhead_x(scroll, pps) - before).abs() < 0.5, "the playhead stays where it was: {scroll}");
+    assert_eq!((v, a), (0.0, 0.0), "the tracks did not move");
+    d.wheel_on("timeline.zoomBar.track", 0.0, -20.0, none());
+    let (scroll, pps, ..) = d.view();
+    assert!((pps - 48.0).abs() < 1e-6, "and out again: {pps}");
+    assert!((playhead_x(scroll, pps) - before).abs() < 0.5);
+    // the mouse scrolling setting and Cmd change nothing here
+    d.app().session.prefs.timeline.mouse_scrolling = "horizontal".into();
+    d.wheel_on("timeline.zoomBar.track", 0.0, 20.0, json!({"command": true}));
+    assert!((d.view().1 - 57.6).abs() < 1e-6);
+    // with the playhead out of view, about the middle of the view
+    d.exec("playhead.set", json!({"seconds": 600.0}));
+    d.frames(3);
+    d.ok("ui.set", json!({"timeline": {"pps": 50.0, "scroll": 4.0}}));
+    d.frames(3);
+    let width = d.rect("timeline.tracks")[2] as f64;
+    let middle = 4.0 + width / 2.0 / 50.0;
+    // the pointer near the bar's left end, well away from the middle
+    let bar = d.rect("timeline.zoomBar.track");
+    d.ok("ui.scroll", json!({"x": bar[0] + 20.0, "y": bar[1] + bar[3] / 2.0, "dx": 0.0, "dy": 20.0}));
+    d.frames(3);
+    let (scroll, pps, ..) = d.view();
+    assert!((scroll + width / 2.0 / pps - middle).abs() < 0.02, "the middle stays: {scroll}");
+}
+
+/// Premiere: Shift or Option + wheel over the track headers makes the tracks taller or shorter
+/// (FilmCraft has one height for the video tracks and one for the audio tracks); over the tracks
+/// themselves Shift + wheel still scrolls sideways and Option + wheel still zooms.
+#[test]
+fn shift_or_option_wheel_over_the_track_headers_changes_the_track_height() {
+    let mut d = Driver::tall();
+    let (vh, ah) = d.heights();
+    d.wheel_on("timeline.track.V1.target", 0.0, 40.0, json!({"shift": true}));
+    assert_eq!(d.heights(), (vh + 10.0, ah), "a notch up: the video tracks 10 points taller");
+    assert_eq!(d.view(), (4.0, 50.0, 0.0, 0.0), "nothing scrolled or zoomed");
+    // macOS hands Shift + wheel over as a sideways wheel
+    d.wheel_on("timeline.track.V1.target", -40.0, 0.0, json!({"shift": true}));
+    assert_eq!(d.heights(), (vh, ah));
+    d.wheel_on("timeline.track.A1.target", 0.0, -40.0, json!({"alt": true}));
+    assert_eq!(d.heights(), (vh, ah - 10.0), "Option: the audio tracks 10 points shorter");
+    assert_eq!(d.view(), (4.0, 50.0, 0.0, 0.0));
+    // no shorter than the smallest a drag allows, no taller than the largest
+    d.wheel_on("timeline.track.A1.target", 0.0, -1e5, json!({"alt": true}));
+    assert_eq!(d.heights().1, 22.0);
+    d.wheel_on("timeline.track.V1.target", 0.0, 1e5, json!({"shift": true}));
+    assert_eq!(d.heights().0, 220.0);
+    // over header space where no track shows (the timecode, with tracks scrolled out of view above
+    // it; the Mix row, with tracks out of view below it) nothing changes
+    let at = d.heights();
+    let tc = d.rect("timeline.timecode");
+    let tracks = d.rect("timeline.tracks");
+    for (x, y) in [(tc[0] + 10.0, tc[1] + tc[3] / 2.0), (tc[0] + 10.0, tracks[1] + tracks[3] - 17.0)] {
+        for m in [json!({"shift": true}), json!({"alt": true})] {
+            d.ok("ui.scroll", json!({"x": x, "y": y, "dx": 0.0, "dy": -40.0, "modifiers": m}));
+            d.frames(3);
+            assert_eq!(d.heights(), at, "at {x},{y} with {m}");
+        }
+    }
+    // the plain wheel over a header still moves the tracks up and down
+    d.wheel_on("timeline.track.V1.target", 0.0, 40.0, none());
+    assert_eq!(d.view().2, 40.0);
+}
+
 /// The view cannot leave the Timeline: scrolled 39 minutes past a 26 second sequence, nothing was
 /// in view and the scroll bar's thumb sat at the right end whatever was done to it.
 #[test]
