diff --git a/crates/ui-egui/src/mix_window.rs b/crates/ui-egui/src/mix_window.rs
--- a/crates/ui-egui/src/mix_window.rs
+++ b/crates/ui-egui/src/mix_window.rs
@@ -64,6 +64,42 @@ pub(crate) fn content(app: &mut SoundApp, ui: &mut Ui) {
     });
 }
 
+/// The tracks an Alt+Shift fader move on `id` applies to, as Alt+Shift applies a change to the
+/// selected channel strips in Pro Tools: the selected tracks that have a strip here, when `id` is one.
+fn selected_strips(s: &soundcraft_model::Session, id: TrackId) -> Vec<TrackId> {
+    let selected = &s.edit.selected_tracks;
+    if !selected.contains(&id) {
+        return Vec::new();
+    }
+    s.tracks.iter().filter(|t| selected.contains(&t.id) && !t.hidden && t.kind != TrackKind::Video).map(|t| t.id).collect()
+}
+
+/// Where each selected strip goes while the dragged one (`lead`) is at `db`: its distance from the
+/// lead when the drag began (`starts`), clamped to the fader's travel. Measured from the start and
+/// not frame to frame, so a strip held at +12 dB or -inf gets its distance back when the lead
+/// returns, as grouped faders do in Pro Tools. A strip whose volume follows an earlier one through
+/// an active mix group is left out: `mix.volume` moves it with that group already.
+fn follow_from(s: &soundcraft_model::Session, starts: &[(TrackId, f32)], lead: TrackId, db: f32) -> Vec<(TrackId, f32)> {
+    let lead_start = starts.iter().find(|(t, _)| *t == lead).map_or(db, |(_, v)| *v);
+    let delta = db - lead_start;
+    let mut grouped: Vec<TrackId> = Vec::new();
+    let mut out = Vec::new();
+    let lead_first = starts.iter().filter(|(t, _)| *t == lead).chain(starts.iter().filter(|(t, _)| *t != lead));
+    for (t, start) in lead_first {
+        if grouped.contains(t) {
+            continue;
+        }
+        for g in &s.groups {
+            if g.active && g.mix && g.members.contains(t) && g.attributes.iter().any(|a| a == "volume") {
+                grouped.extend(g.members.iter().filter(|m| *m != t));
+            }
+        }
+        let v = if *t == lead { db } else { (start + delta).clamp(-144.0, 12.0) };
+        out.push((*t, v));
+    }
+    out
+}
+
 /// Format of the destination a track's output feeds (the main mix, a bus, or stereo).
 fn output_format(app: &SoundApp, track: &Track) -> ChannelFormat {
     let s = app.engine.session();
@@ -419,9 +455,36 @@ fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapsh
     let fh = (r.max.y - bottom_h - y).max(120.0);
     let wide_meter = (if track.kind == TrackKind::Master { app.engine.session().main_format() } else { track.format }).channels() > 2;
     let fr = Rect::from_min_size(pos2(x0, y), vec2(inner_w * if wide_meter { 0.45 } else { 0.6 }, fh));
-    if let Some(db) = fader(ui, fr, track.mixer.volume_db, ui.id().with(("fader", id.0))) {
+    ui.interact(fr, ui.id().with(("fader-label", id.0)), Sense::hover())
+        .widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Slider, true, format!("{} volume", track.name)));
+    let fader_id = ui.id().with(("fader", id.0));
+    // Where each selected strip stood when an Alt+Shift drag of this fader began.
+    let starts_id = fader_id.with("starts");
+    let dragging = ui.ctx().is_being_dragged(fader_id);
+    if let Some(db) = fader(ui, fr, track.mixer.volume_db, fader_id) {
         app.gesture = Some(crate::Gesture::Fader { track: id, start_db: track.mixer.volume_db });
-        let _ = app.engine.execute_merged("mix.volume", &json!({"track": id.0, "db": db}), &format!("fader:{}", id.0));
+        let mods = ui.input(|i| i.modifiers);
+        let together = if mods.alt && mods.shift { selected_strips(&session, id) } else { Vec::new() };
+        let key = format!("fader:{}", id.0);
+        if together.len() < 2 {
+            ui.data_mut(|d| d.remove::<Vec<(TrackId, f32)>>(starts_id));
+            let _ = app.engine.execute_merged("mix.volume", &json!({"track": id.0, "db": db}), &key);
+        } else if dragging {
+            let starts = ui.data_mut(|d| {
+                d.get_temp_mut_or_insert_with(starts_id, || {
+                    together.iter().map(|t| (*t, session.track(*t).map_or(0.0, |tr| tr.mixer.volume_db))).collect::<Vec<_>>()
+                })
+                .clone()
+            });
+            for (t, v) in follow_from(&session, &starts, id, db) {
+                let _ = app.engine.execute_merged("mix.volume", &json!({"track": t.0, "db": v}), &key);
+            }
+        } else {
+            let ids: Vec<u64> = together.iter().map(|t| t.0).collect();
+            let _ = app.engine.execute_merged("mix.volume", &json!({"tracks": ids, "db": db}), &key);
+        }
+    } else if !dragging {
+        ui.data_mut(|d| d.remove::<Vec<(TrackId, f32)>>(starts_id));
     }
     let md = app.meters.get(&id).copied().unwrap_or_default();
     let slot = fader_slot(fr);
diff --git a/crates/ui-egui/tests/it/main.rs b/crates/ui-egui/tests/it/main.rs
--- a/crates/ui-egui/tests/it/main.rs
+++ b/crates/ui-egui/tests/it/main.rs
@@ -27,6 +27,7 @@ mod midi_front_visibility;
 mod midi_panel;
 mod midi_quantize;
 mod mix_narrow_headers;
+mod mix_selected_faders;
 mod mix_separate;
 mod new_midi_clip;
 mod new_tracks;
diff --git a/crates/ui-egui/tests/it/mix_selected_faders.rs b/crates/ui-egui/tests/it/mix_selected_faders.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/tests/it/mix_selected_faders.rs
@@ -0,0 +1,146 @@
+//! Alt+Shift on a selected track's fader moves every selected track's fader by the same amount, as
+//! Alt+Shift applies a change to the selected channel strips in Pro Tools; a plain drag moves one.
+
+use egui::{Event, Modifiers, PointerButton, Pos2, vec2};
+use egui_kittest::Harness;
+use egui_kittest::kittest::Queryable;
+use serde_json::json;
+use soundcraft_model::TrackId;
+use soundcraft_ui_egui::{MainWindow, Services, SoundApp};
+
+const START: [f32; 4] = [-10.0, -4.0, -20.0, -7.0];
+
+/// Kick, Snare and Bass selected (Bass hidden), Keys not; every track at a different level.
+fn harness() -> (Harness<'static, SoundApp>, [TrackId; 4]) {
+    let mut engine = soundcraft_engine::Engine::default();
+    let mut ids = Vec::new();
+    for (name, db) in ["Kick", "Snare", "Bass", "Keys"].iter().zip(START) {
+        engine.execute("track.new", &json!({"name": name})).unwrap();
+        let id = engine.session().tracks.last().unwrap().id;
+        engine.execute("mix.volume", &json!({"track": id.0, "db": db})).unwrap();
+        ids.push(id);
+    }
+    let ids: [TrackId; 4] = ids.try_into().unwrap();
+    engine.session_mut().track_mut(ids[2]).unwrap().hidden = true;
+    engine.session_mut().edit.selected_tracks = vec![ids[0], ids[1], ids[2]];
+    let mut app = SoundApp::new(engine, None, Services::default());
+    app.ui.window = MainWindow::Mix;
+    let mut h = Harness::builder().with_size(vec2(1400.0, 900.0)).with_step_dt(1.0 / 60.0).build_ui_state(
+        |ui, app: &mut SoundApp| {
+            app.logic(&ui.ctx().clone());
+            app.ui(ui);
+        },
+        app,
+    );
+    h.run_steps(4);
+    (h, ids)
+}
+
+fn volumes(h: &Harness<SoundApp>, ids: &[TrackId; 4]) -> [f32; 4] {
+    ids.map(|id| h.state().engine.session().track(id).unwrap().mixer.volume_db)
+}
+
+fn press(h: &mut Harness<SoundApp>, at: Pos2, pressed: bool, modifiers: Modifiers) {
+    h.event(Event::ModifiersChanged(modifiers));
+    h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers });
+    h.step();
+}
+
+/// Drags Kick's fader cap up by `dy` points with `modifiers` held, in a few moves.
+fn drag_kick(h: &mut Harness<SoundApp>, dy: f32, modifiers: Modifiers) {
+    let fader = h.get_by_label("Kick volume").rect();
+    let from = fader.center() - vec2(0.0, fader.height() * 0.25);
+    h.hover_at(from);
+    h.step();
+    press(h, from, true, modifiers);
+    for f in [0.25, 0.5, 1.0] {
+        h.hover_at(from - vec2(0.0, dy * f));
+        h.step();
+    }
+    press(h, from - vec2(0.0, dy), false, modifiers);
+    h.event(Event::ModifiersChanged(Modifiers::NONE));
+    h.run_steps(2);
+}
+
+/// Drags Kick's fader with Alt+Shift through `path` (points above where the drag starts, in one
+/// gesture) and returns the volumes after each move.
+fn drag_kick_through(h: &mut Harness<SoundApp>, ids: &[TrackId; 4], path: &[f32]) -> Vec<[f32; 4]> {
+    let both = Modifiers::ALT | Modifiers::SHIFT;
+    let fader = h.get_by_label("Kick volume").rect();
+    let from = fader.center() - vec2(0.0, fader.height() * 0.25);
+    h.hover_at(from);
+    h.step();
+    press(h, from, true, both);
+    let mut seen = Vec::new();
+    for dy in path {
+        h.hover_at(from - vec2(0.0, *dy));
+        h.step();
+        seen.push(volumes(h, ids));
+    }
+    press(h, from - vec2(0.0, *path.last().unwrap_or(&0.0)), false, both);
+    h.event(Event::ModifiersChanged(Modifiers::NONE));
+    h.run_steps(2);
+    seen
+}
+
+#[test]
+fn a_selected_fader_held_at_the_top_gets_its_distance_back_on_the_way_down() {
+    let (mut h, ids) = harness();
+    let seen = drag_kick_through(&mut h, &ids, &[40.0, 80.0, 110.0, 130.0, 150.0, 130.0, 80.0, 40.0, 0.0]);
+    assert!(seen.iter().any(|v| v[1] == 12.0 && v[0] < 12.0), "Snare stopped at the top while Kick went on: {seen:?}");
+    let v = volumes(&h, &ids);
+    assert!((v[1] - v[0] - (START[1] - START[0])).abs() < 1e-3, "back down, Snare is {} dB above Kick again: {v:?}", START[1] - START[0]);
+    assert_eq!(v[3], START[3]);
+}
+
+#[test]
+fn a_drag_up_from_minus_infinity_and_back_returns_the_selected_faders() {
+    let (mut h, ids) = harness();
+    h.state_mut().engine.session_mut().track_mut(ids[0]).unwrap().mixer.volume_db = -144.0;
+    h.run_steps(2);
+    let seen = drag_kick_through(&mut h, &ids, &[12.0, 24.0, 12.0, 0.0]);
+    for v in &seen {
+        let want = (START[1] + v[0] + 144.0).min(12.0);
+        assert!((v[1] - want).abs() < 1e-3, "Snare follows Kick's dB change from where it began: {v:?}");
+    }
+    let v = volumes(&h, &ids);
+    assert_eq!((v[0], v[1], v[3]), (-144.0, START[1], START[3]), "back at the bottom, every fader is where it began: {v:?}");
+}
+
+#[test]
+fn alt_shift_drag_moves_the_selected_faders_by_the_same_amount_in_one_undo_step() {
+    let (mut h, ids) = harness();
+    drag_kick(&mut h, 40.0, Modifiers::ALT | Modifiers::SHIFT);
+    let v = volumes(&h, &ids);
+    let d = v[0] - START[0];
+    assert!(d > 1.0, "the dragged fader moved: {v:?}");
+    assert!((v[1] - (START[1] + d)).abs() < 1e-3, "the other selected strip keeps its distance: {v:?}");
+    assert_eq!(v[2], START[2], "a hidden track has no strip, so it stays where it was");
+    assert_eq!(v[3], START[3], "an unselected track stays where it was");
+
+    assert!(h.state_mut().engine.undo());
+    assert_eq!(volumes(&h, &ids), START, "one undo puts every fader back");
+}
+
+#[test]
+fn a_plain_drag_moves_only_its_own_fader() {
+    let (mut h, ids) = harness();
+    drag_kick(&mut h, 40.0, Modifiers::NONE);
+    let v = volumes(&h, &ids);
+    assert!(v[0] - START[0] > 1.0, "the dragged fader moved: {v:?}");
+    assert_eq!(v[1..], START[1..]);
+}
+
+#[test]
+fn alt_shift_click_sets_the_selected_faders_to_zero() {
+    let (mut h, ids) = harness();
+    let fader = h.get_by_label("Kick volume").rect();
+    h.hover_at(fader.center());
+    h.step();
+    let both = Modifiers::ALT | Modifiers::SHIFT;
+    press(&mut h, fader.center(), true, both);
+    press(&mut h, fader.center(), false, both);
+    h.event(Event::ModifiersChanged(Modifiers::NONE));
+    h.run_steps(2);
+    assert_eq!(volumes(&h, &ids), [0.0, 0.0, START[2], START[3]]);
+}
diff --git a/docs/ui-parity.md b/docs/ui-parity.md
--- a/docs/ui-parity.md
+++ b/docs/ui-parity.md
@@ -1,6 +1,6 @@
 # UI parity
 
-> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-10-16 · **Change:** minor (Scroll Into View in the track name menu) · **Target:** Avid Pro Tools Ultimate 2026.4.1
+> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-24 · **Change:** minor (Shift-Alt-drag on a selected fader moves the selected faders) · **Target:** Avid Pro Tools Ultimate 2026.4.1
 
 How SoundCraft feels in the hands of a Pro Tools user: tools, modes, modifiers, shortcuts,
 windows, meters. Measured from `crates/model/src/session.rs` (tools, modes), the command
@@ -32,7 +32,7 @@ Legend: ✅ matches · 🟡 partial · ❌ missing.
 | Default shortcuts | 🟡 146 bound (measured: distinct `Some("…")` shortcuts in the registry) | Pro Tools' default set is several hundred incl. numeric keypad and focus keys. Collisions: Mix/Edit toggle vs zoom (#144), Heal (#140), Export (#7) |
 | Ctrl as Cmd on Windows/Linux | ✅ since #15, #41 still open in some builds | #62 shows Ctrl in labels |
 | Numeric keypad transport and memory-location recall | 🟡 | Classic / Transport / Shuttle modes not all present |
-| Modifier gestures (Alt for all tracks, Shift-Alt for selected, Cmd-click fine faders, Ctrl-click defaults) | 🟡 | Main ones on faders and pan; not across every control |
+| Modifier gestures (Alt for all tracks, Shift-Alt for selected, Cmd-click fine faders, Ctrl-click defaults) | 🟡 | Main ones on faders and pan; Shift-Alt on a selected track's fader moves every selected fader by the same amount (#865); not across every control |
 | Scroll/zoom: wheel, trackpad pinch | 🟡 | Anchored trackpad zoom in open PRs #112, #137 |
 | Drag and drop: Clip List → tracks, files → tracks | ✅ macOS/Windows; 🟡 Wayland (PR #150) | |
 | Context menus (clip, track name, ruler, Clip List) | 🟡 | Clip and track menus exist; depth lower than Pro Tools'; Clip List delete (#4); track name ▸ Scroll Into View (Edit top, Mix left) |
@@ -69,6 +69,7 @@ Legend: ✅ matches · 🟡 partial · ❌ missing.
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-24 | minor | Mix window: Shift-Alt-drag on a selected track's fader moves every selected fader by the same dB, one undo step (#865). Readiness estimate unchanged |
 | 2026-10-16 | minor | Track name menu: Scroll Into View brings the track to the top of the Edit window and the left of the Mix. Readiness estimate unchanged |
 | 2026-10-13 | minor | A plugin opens when it's inserted (#486) |
 | 2026-10-13 | minor | Drag across checkboxes (#399) |
