diff --git a/crates/ui-egui/src/edit_window.rs b/crates/ui-egui/src/edit_window.rs
--- a/crates/ui-egui/src/edit_window.rs
+++ b/crates/ui-egui/src/edit_window.rs
@@ -27,9 +27,14 @@ mod universe;
 
 #[cfg(test)]
 use automation::automation_id;
+use clips::clip_cuts;
+pub(crate) use clips::draw_clip_labels;
 pub use clips::strip_silence_stroke;
 #[cfg(test)]
-use clips::{clip_name_height, fade_curve_points};
+use clips::{
+    ClipCut, PILL_BG, PILL_HEIGHT, PILL_INSET, PILL_MIN_WIDTH, PILL_PAD, clip_cut, clip_cut_in, clip_marks, fade_curve_points, marks_pill_left,
+    name_pill_width,
+};
 use drag::drive_drag;
 #[cfg(test)]
 use drag::fade_drag_range;
diff --git a/crates/ui-egui/src/edit_window/clips.rs b/crates/ui-egui/src/edit_window/clips.rs
--- a/crates/ui-egui/src/edit_window/clips.rs
+++ b/crates/ui-egui/src/edit_window/clips.rs
@@ -48,8 +48,9 @@ pub(super) fn grid_lines(painter: &egui::Painter, s: &Session, tl: Rect, lane: R
     }
 }
 
-pub(super) fn clip_name_height(s: &Session, lane: Rect) -> f32 {
-    if lane.height() >= 40.0 && s.edit.flag("view.clip.name") { 13.0 } else { 0.0 }
+/// Whether clips show their name (View › Clip › Name) in a lane tall enough to carry it.
+pub(super) fn clip_name_shown(s: &Session, lane: Rect) -> bool {
+    lane.height() >= 40.0 && s.edit.flag("view.clip.name")
 }
 
 pub(super) fn fade_curve_points(fade: soundcraft_model::Fade, fade_in: bool, a: f32, b: f32, body: Rect, pixels_per_point: f32) -> Vec<Pos2> {
@@ -138,51 +139,46 @@ fn grid_veil(line: Color32, bg: Color32) -> Color32 {
     }
 }
 
-/// `sorted`: whether `track.clips()` is sorted by start (worked out once per track and frame).
+/// `sorted`: whether `track.clips()` is sorted by start (worked out once per track and frame); `cut`: how the clip
+/// meets its neighbours in its playlist (see [`clip_cuts`]).
 pub(super) fn draw_clip(
     app: &SoundApp,
     painter: &egui::Painter,
     s: &Session,
     track: &Track,
     sorted: bool,
+    cut: ClipCut,
     clip: &Clip,
     tl: Rect,
     lane: Rect,
     dim: bool,
 ) {
-    let x0 = x_of(s, tl, clip.start);
-    let x1 = x_of(s, tl, clip.end()).max(x0 + 1.0);
-    let r = Rect::from_min_max(pos2(x0, lane.min.y + 1.0), pos2(x1, lane.max.y - 1.0));
+    let r = clip_rect(s, tl, lane, clip);
+    let (x0, x1) = (r.min.x, r.max.x);
     let selected = s.edit.selected_clips.contains(&clip.id)
         || (s.edit.selected_tracks.contains(&track.id)
             && !s.edit.selection.is_empty()
             && s.edit.selection.start <= clip.start
             && s.edit.selection.end >= clip.end());
     let color = clip.color.unwrap_or(track.color);
-    let (mut body, bar, mut wave) = clip_colors(color, selected);
+    let (mut body, _, mut wave) = clip_colors(color, selected);
     if clip.muted || dim {
         body = body.gamma_multiply(0.45);
         wave = Color32::from_rgb(70, 70, 70);
     }
     let body = if s.edit.flag("view.clip.transparency") { body.gamma_multiply(0.55) } else { body };
-    painter.rect(r, CornerRadius::same(2), body, Stroke::new(1.0, Color32::from_rgb(10, 10, 10)), StrokeKind::Inside);
-    let name_h = clip_name_height(s, lane);
-    if name_h > 0.0 {
-        let nb = Rect::from_min_max(r.min, pos2(r.max.x, r.min.y + name_h));
-        painter.rect_filled(nb, CornerRadius { nw: 2, ne: 2, sw: 0, se: 0 }, if clip.muted { Color32::from_rgb(70, 70, 70) } else { bar });
-        let name_clip = nb.intersect(lane);
-        // Narrower than this, no glyph would show: the name starts 4 points in.
-        if name_clip.width() > 6.0 {
-            painter.with_clip_rect(name_clip).text(
-                pos2(nb.min.x.max(lane.min.x) + 4.0, nb.center().y),
-                Align2::LEFT_CENTER,
-                &clip.name,
-                bold(10.5),
-                Color32::from_rgb(12, 12, 12),
-            );
-        }
+    let corners = cut.corners();
+    let outline = Stroke::new(1.0, CLIP_OUTLINE);
+    if cut.right {
+        // The cut's single line is drawn by the next clip: stroke past our right edge and clip it away.
+        painter.rect_filled(r, corners, body);
+        let past = Rect::from_min_max(r.min, pos2(r.max.x + 2.0, r.max.y));
+        painter.with_clip_rect(painter.clip_rect().intersect(r)).rect_stroke(past, corners, outline, StrokeKind::Inside);
+    } else {
+        painter.rect(r, corners, body, outline, StrokeKind::Inside);
     }
-    let body_r = Rect::from_min_max(pos2(r.min.x, r.min.y + name_h), r.max);
+    // The body is the whole clip: the name is a small pill drawn over it (see `draw_clip_pills`).
+    let body_r = r;
     let vis = body_r.intersect(lane);
     // The waveform and fade curve share the same temporary envelope every frame.
     let (fade_in, fade_out) = clip_preview_fades(clip, app.gesture.as_ref().filter(|_| !dim));
@@ -311,9 +307,6 @@ pub(super) fn draw_clip(
     }
     // Optional clip overlays (View › Clip).
     let ov = Color32::from_rgb(16, 16, 16);
-    if s.edit.flag("view.clip.rating") && clip.rating > 0 && name_h > 0.0 {
-        painter.text(pos2(r.max.x - 18.0, r.min.y + 7.0), Align2::RIGHT_CENTER, "★".repeat(usize::from(clip.rating.min(5))), regular(9.0), ov);
-    }
     if s.edit.flag("view.clip.sync_point") && clip.sync_point > 0 {
         let x = x_of(s, tl, clip.start + clip.sync_point);
         painter.add(Shape::convex_polygon(
@@ -321,7 +314,7 @@ pub(super) fn draw_clip(
             Color32::WHITE,
             Stroke::NONE,
         ));
-        painter.line_segment([pos2(x, r.min.y + name_h), pos2(x, r.max.y)], Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 255, 255, 90)));
+        painter.line_segment([pos2(x, r.min.y), pos2(x, r.max.y)], Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 255, 255, 90)));
     }
     let time_text = if s.edit.flag("view.clip.time.current") {
         Some(s.format_position(clip.start, s.edit.main_counter))
@@ -366,12 +359,179 @@ pub(super) fn draw_clip(
             }
         }
     }
-    if clip.edit_locked || clip.time_locked {
-        painter.text(pos2(r.max.x - 10.0, r.min.y + 7.0), Align2::CENTER_CENTER, "🔒", regular(9.0), Color32::WHITE);
+    // Video clips get their pills from `video_track::draw_lane`, after their thumbnails.
+    if !clip.is_video() {
+        draw_clip_labels(painter, s, tl, lane, clip);
+    }
+    if cut.left {
+        // The cut's one line, on top of the waveform so it runs the full height.
+        painter.rect_filled(Rect::from_min_size(r.min, vec2(1.0, r.height())), 0.0, CLIP_OUTLINE);
     }
     let _ = app;
 }
 
+/// Where a clip sits in its lane: one point in from the lane's top and bottom, at least one point wide.
+pub(super) fn clip_rect(s: &Session, tl: Rect, lane: Rect, clip: &Clip) -> Rect {
+    let x0 = x_of(s, tl, clip.start);
+    let x1 = x_of(s, tl, clip.end()).max(x0 + 1.0);
+    Rect::from_min_max(pos2(x0, lane.min.y + 1.0), pos2(x1, lane.max.y - 1.0))
+}
+
+/// The pills of `clip` (name, marks). Drawn right after the clip itself, so clips and overlays drawn later cover them.
+pub(crate) fn draw_clip_labels(painter: &egui::Painter, s: &Session, tl: Rect, lane: Rect, clip: &Clip) {
+    let r = clip_rect(s, tl, lane, clip);
+    if r.max.x < lane.min.x || r.min.x > lane.max.x {
+        return;
+    }
+    let show_name = clip_name_shown(s, lane);
+    let marks = clip_marks(clip, s.edit.flag("view.clip.rating"));
+    draw_clip_pills(painter, lane, r, show_name.then_some(clip.name.as_str()), &marks);
+}
+
+/// The marks a clip carries at its top-right: its star rating and a lock when edit- or time-locked.
+pub(super) fn clip_marks(clip: &Clip, show_rating: bool) -> String {
+    let mut marks = if show_rating && clip.rating > 0 { "★".repeat(usize::from(clip.rating.min(5))) } else { String::new() };
+    if clip.edit_locked || clip.time_locked {
+        if !marks.is_empty() {
+            marks.push(' ');
+        }
+        marks.push('🔒');
+    }
+    marks
+}
+
+/// Gap between a pill and the clip outline's inner edge (so the pill sits 1 pt inside the outline).
+pub(super) const PILL_INSET: f32 = 2.0;
+pub(super) const PILL_HEIGHT: f32 = 13.0;
+/// Horizontal padding between a pill's edge and its text.
+pub(super) const PILL_PAD: f32 = 3.0;
+/// The narrowest name pill worth drawing: its padding plus about one letter.
+pub(super) const PILL_MIN_WIDTH: f32 = 12.0;
+/// Half-transparent black behind white text, readable on the light and the dark clip bodies alike.
+pub(super) const PILL_BG: Color32 = Color32::from_black_alpha(125);
+
+/// Width of the name pill: the text plus its padding, capped to the `room` the clip leaves it (the text is then
+/// cut cleanly at the pill's edge, with no ellipsis). `None` when there is no room for anything useful.
+pub(super) fn name_pill_width(room: f32, text_w: f32) -> Option<f32> {
+    if room.is_nan() || room < PILL_MIN_WIDTH {
+        return None;
+    }
+    Some((text_w.max(0.0) + 2.0 * PILL_PAD).min(room))
+}
+
+/// Left edge of the marks pill, which sits at the clip's top-right: `None` when the clip is too narrow to hold it
+/// (it would veil the clip), where `left` is the leftmost x a pill may use.
+pub(super) fn marks_pill_left(left: f32, clip_right: f32, w: f32) -> Option<f32> {
+    let x = clip_right - PILL_INSET - w;
+    (x >= left).then_some(x)
+}
+
+/// The clip's name as a small rounded pill at its top-left, and its marks (rating, lock) as a matching pill at the
+/// top-right. The name keeps clear of the marks and floats to the left edge of the lane when the clip starts
+/// off-screen, so it stays readable while scrolling.
+fn draw_clip_pills(painter: &egui::Painter, lane: Rect, r: Rect, name: Option<&str>, marks: &str) {
+    let p = painter.with_clip_rect(painter.clip_rect().intersect(r));
+    let pill = |rect: Rect, galley: Arc<egui::Galley>| {
+        p.rect_filled(rect, CornerRadius::same(3), PILL_BG);
+        let at = pos2(rect.min.x + PILL_PAD, rect.center().y - galley.size().y * 0.5);
+        p.with_clip_rect(p.clip_rect().intersect(rect)).galley(at, galley, Color32::WHITE);
+    };
+    let top = r.min.y + PILL_INSET;
+    let left = r.min.x.max(lane.min.x) + PILL_INSET;
+    let mut right = r.max.x.min(lane.max.x) - PILL_INSET;
+    if !marks.is_empty() {
+        let galley = p.layout_no_wrap(marks.to_owned(), regular(9.0), Color32::WHITE);
+        let w = galley.size().x + 2.0 * PILL_PAD;
+        if let Some(x) = marks_pill_left(left, r.max.x, w) {
+            right = right.min(x - PILL_INSET);
+            pill(Rect::from_min_size(pos2(x, top), vec2(w, PILL_HEIGHT)), galley);
+        }
+    }
+    // No layout where no pill fits: zoomed out on a large session, most clips are too narrow.
+    if let Some(name) = name.filter(|n| !n.is_empty() && right - left >= PILL_MIN_WIDTH) {
+        let galley = p.layout_no_wrap(name.to_owned(), bold(10.0), Color32::WHITE);
+        if let Some(w) = name_pill_width(right - left, galley.size().x) {
+            pill(Rect::from_min_size(pos2(left, top), vec2(w, PILL_HEIGHT)), galley);
+        }
+    }
+}
+
+/// The 1 pt outline around a clip.
+const CLIP_OUTLINE: Color32 = Color32::from_rgb(10, 10, 10);
+
+/// How a clip meets its neighbours in the playlist. Two clips butted at a cut share ONE 1 pt line, as in
+/// the incumbent: the later clip draws it (its left edge), the earlier one drops its right edge, and both
+/// sides of the cut have square corners. A gap or an overlap leaves the clip drawn on its own.
+/// Selection only recolours the body, so it never changes which edges are drawn.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub(super) struct ClipCut {
+    /// Starts exactly where the clip before it ends.
+    pub(super) left: bool,
+    /// Ends exactly where the clip after it starts.
+    pub(super) right: bool,
+}
+
+impl ClipCut {
+    /// The body's corners: square on a side that meets another clip, rounded otherwise.
+    pub(super) fn corners(self) -> CornerRadius {
+        let (nw, ne) = (if self.left { 0 } else { 2 }, if self.right { 0 } else { 2 });
+        CornerRadius { nw, ne, sw: nw, se: ne }
+    }
+}
+
+/// The cut of the clip spanning `start..end`, given the `(start, end)` of every other clip of its playlist, in any
+/// order (a playlist's order is its stacking order, not its time order). A side is butted when another clip ends
+/// (or starts) exactly there and no third clip covers that point.
+pub(super) fn clip_cut(start: Samples, end: Samples, others: impl IntoIterator<Item = (Samples, Samples)>) -> ClipCut {
+    // An empty clip has no span to share a line with.
+    if end <= start {
+        return ClipCut::default();
+    }
+    let (mut left, mut right, mut left_covered, mut right_covered) = (false, false, false, false);
+    for (s, e) in others.into_iter().filter(|(s, e)| e > s) {
+        left |= e == start;
+        right |= s == end;
+        left_covered |= s < start && start < e;
+        right_covered |= s < end && end < e;
+    }
+    ClipCut { left: left && !left_covered, right: right && !right_covered }
+}
+
+/// [`clip_cut`] for `clip` among its playlist's `clips`, scanning them all.
+pub(super) fn clip_cut_in(clips: &[Clip], clip: &Clip) -> ClipCut {
+    clip_cut(clip.start, clip.end(), clips.iter().filter(|c| c.id != clip.id).map(|c| (c.start, c.end())))
+}
+
+/// [`clip_cut`] for each clip of a playlist. Sorted by start (the usual case), one pass over the playlist and two
+/// binary searches per clip, so large sessions do no per-clip scan every frame; unsorted, a scan per clip.
+pub(super) fn clip_cuts(clips: &[Clip]) -> impl Fn(&Clip) -> ClipCut + '_ {
+    // reach[k]: the furthest end of the first k clips (empty clips reach nothing).
+    let reach: Option<Vec<Samples>> = clips.is_sorted_by_key(|c| c.start).then(|| {
+        std::iter::once(Samples::MIN)
+            .chain(clips.iter().scan(Samples::MIN, |far, c| {
+                if c.end() > c.start {
+                    *far = (*far).max(c.end());
+                }
+                Some(*far)
+            }))
+            .collect()
+    });
+    move |clip| {
+        let Some(reach) = &reach else { return clip_cut_in(clips, clip) };
+        let (start, end) = (clip.start, clip.end());
+        if end <= start {
+            return ClipCut::default();
+        }
+        let reached = |k: usize| reach.get(k).copied().unwrap_or(Samples::MIN);
+        // Butted and uncovered on the left: the clips starting before it reach exactly its start. On the right: the
+        // clips starting before its end (itself included) reach exactly its end, and a clip starts there.
+        let before = clips.partition_point(|c| c.start < start);
+        let upto = clips.partition_point(|c| c.start < end);
+        let next = clips.get(upto..).unwrap_or_default().iter().take_while(|c| c.start == end).any(|c| c.end() > c.start);
+        ClipCut { left: reached(before) == start, right: next && reached(upto) == end }
+    }
+}
+
 /// The earlier clip of a crossfade pair: the one before `clip` in its playlist, overlapping its start.
 fn crossfade_partner<'a>(track: &'a Track, clip: &Clip) -> Option<&'a Clip> {
     let (p, i) = track.playlists.iter().find_map(|p| {
diff --git a/crates/ui-egui/src/edit_window/input.rs b/crates/ui-egui/src/edit_window/input.rs
--- a/crates/ui-egui/src/edit_window/input.rs
+++ b/crates/ui-egui/src/edit_window/input.rs
@@ -1,5 +1,5 @@
 use super::automation::{automation_id, breakpoint_at, plugin_range, shown_automation};
-use super::clips::{clip_name_height, fade_control_position};
+use super::clips::fade_control_position;
 use super::drag::{drag_snap, fade_curve_drag_value, fade_drag_range, spot_or_run};
 use super::layout::{sample_at, x_of};
 use super::*;
@@ -20,7 +20,7 @@ fn fade_handle_at(s: &Session, tl: Rect, lane: Rect, clip: &Clip, p: Pos2) -> Op
     if clip.length <= 0 || clip.end() <= clip.start {
         return None;
     }
-    let y = lane.min.y + 1.0 + clip_name_height(s, lane);
+    let y = lane.min.y + 1.0;
     [(true, clip.fade_in), (false, clip.fade_out)]
         .into_iter()
         .filter(|(_, fade)| fade.len > 0)
@@ -51,7 +51,7 @@ pub(super) fn fade_midpoint(s: &Session, tl: Rect, lane: Rect, clip: &Clip, fade
     if b - a < 32.0 {
         return None;
     }
-    let body = Rect::from_min_max(pos2(a, lane.min.y + 1.0 + clip_name_height(s, lane)), pos2(b, lane.max.y - 1.0));
+    let body = Rect::from_min_max(pos2(a, lane.min.y + 1.0), pos2(b, lane.max.y - 1.0));
     let p = fade_control_position(fade, a, b, body);
     lane.contains(p).then_some(p)
 }
@@ -252,7 +252,7 @@ pub(super) fn lane_interaction(app: &mut SoundApp, ui: &mut Ui, track: &Track, l
     {
         let fade = if fade_in { c.fade_in } else { c.fade_out };
         let anchor = (origin.y, fade.gain(0.5));
-        let height = (lane.height() - 2.0 - clip_name_height(&s, lane)).max(1.0);
+        let height = (lane.height() - 2.0).max(1.0);
         app.gesture = Some(Gesture::FadeCurve { clip: c.id, fade_in, curvature: fade_curve_drag_value(fade, anchor, height, p.y), anchor, height });
         ui.ctx().request_repaint();
     } else if resp.drag_started()
diff --git a/crates/ui-egui/src/edit_window/tests.rs b/crates/ui-egui/src/edit_window/tests.rs
--- a/crates/ui-egui/src/edit_window/tests.rs
+++ b/crates/ui-egui/src/edit_window/tests.rs
@@ -967,7 +967,7 @@ fn fade_midpoint_handles_remain_visible_and_can_be_grabbed_again() {
                         h.step();
                         assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::ResizeVertical);
                         start_drag(&mut h, control);
-                        let to = control - vec2(0.0, (lane.height() - 2.0 - clip_name_height(&before, lane)) * 0.1);
+                        let to = control - vec2(0.0, (lane.height() - 2.0) * 0.1);
                         h.hover_at(to);
                         h.step();
                         assert!(
@@ -1482,3 +1482,266 @@ fn alt_click_on_a_breakpoint_deletes_it() {
         assert_eq!(h.state().engine.session().track(track).unwrap().clips(), clips.as_slice(), "{tool}");
     }
 }
+
+const ALONE: ClipCut = ClipCut { left: false, right: false };
+
+#[test]
+fn butted_pair_shares_one_seam() {
+    // 0..100 then 100..200: the first drops its right edge, the second draws the seam on its left.
+    assert_eq!(clip_cut(0, 100, [(100, 200)]), ClipCut { left: false, right: true });
+    assert_eq!(clip_cut(100, 200, [(0, 100)]), ClipCut { left: true, right: false });
+}
+
+#[test]
+fn middle_clip_of_a_run_is_butted_on_both_sides() {
+    let cut = clip_cut(100, 200, [(0, 100), (200, 300)]);
+    assert_eq!(cut, ClipCut { left: true, right: true });
+    let c = cut.corners();
+    assert_eq!((c.nw, c.sw, c.ne, c.se), (0, 0, 0, 0));
+}
+
+#[test]
+fn gap_overlap_and_lone_clips_keep_their_own_outline() {
+    // First and last clips: nothing on the open side.
+    assert_eq!(clip_cut(0, 100, []), ALONE);
+    // A gap of one sample is not a cut.
+    assert_eq!(clip_cut(101, 200, [(0, 100), (201, 300)]), ALONE);
+    // Overlaps (crossfades, layers) keep both outlines.
+    assert_eq!(clip_cut(90, 200, [(0, 100), (150, 300)]), ALONE);
+    assert_eq!(clip_cut(0, 100, [(60, 160)]), ALONE);
+    // A third clip lying across the cut hides it: no shared seam.
+    assert_eq!(clip_cut(100, 200, [(0, 100), (50, 150)]), ALONE);
+    // Empty clips share nothing, and are not neighbours either.
+    assert_eq!(clip_cut(100, 100, [(0, 100), (100, 200)]), ALONE);
+    assert_eq!(clip_cut(100, 200, [(100, 100)]), ALONE);
+}
+
+#[test]
+fn sorted_playlists_find_the_same_cuts_without_a_scan() {
+    let layouts: [&[(Samples, Samples)]; 8] = [
+        &[(0, 100), (100, 100)],
+        &[(0, 100), (100, 100), (200, 100)],
+        &[(0, 100), (101, 99), (201, 99)],
+        &[(0, 100), (90, 110), (150, 150)],
+        &[(0, 100), (50, 100), (100, 100)],
+        &[(0, 100), (100, 0), (100, 100)],
+        &[(0, 1000), (100, 100), (200, 100)],
+        &[(0, 100), (100, 50), (100, 100), (200, 100)],
+    ];
+    for spans in layouts {
+        let clips: Vec<Clip> = spans.iter().zip(1..).map(|(&(start, len), id)| test_clip(id, start, len)).collect();
+        let cut_of = clip_cuts(&clips);
+        for c in &clips {
+            assert_eq!(cut_of(c), clip_cut_in(&clips, c), "{spans:?}, clip at {}", c.start);
+        }
+    }
+    // Not vacuous: the middle clip of a run is butted on both sides.
+    let run: Vec<Clip> = (0..3).map(|i| test_clip(i + 1, i as Samples * 100, 100)).collect();
+    assert_eq!(clip_cuts(&run)(&run[1]), ClipCut { left: true, right: true });
+}
+
+#[test]
+fn only_butted_sides_lose_their_round_corners() {
+    let round = ALONE.corners();
+    assert_eq!((round.nw, round.sw, round.ne, round.se), (2, 2, 2, 2));
+    let first = ClipCut { left: false, right: true }.corners();
+    assert_eq!((first.nw, first.sw, first.ne, first.se), (2, 2, 0, 0));
+    let second = ClipCut { left: true, right: false }.corners();
+    assert_eq!((second.nw, second.sw, second.ne, second.se), (0, 0, 2, 2));
+}
+
+#[test]
+fn name_pill_is_as_wide_as_its_text_and_cut_cleanly_on_short_clips() {
+    // Room to spare: the text plus its padding on both sides.
+    assert_eq!(name_pill_width(200.0, 50.0), Some(50.0 + 2.0 * PILL_PAD));
+    // A short clip caps the pill at the room it leaves; the text is then cut at the pill's edge.
+    assert_eq!(name_pill_width(30.0, 80.0), Some(30.0));
+    assert_eq!(name_pill_width(PILL_MIN_WIDTH, 80.0), Some(PILL_MIN_WIDTH));
+    // Too narrow to show anything useful, or nonsense numbers: no pill.
+    assert_eq!(name_pill_width(PILL_MIN_WIDTH - 0.1, 80.0), None);
+    assert_eq!(name_pill_width(-5.0, 80.0), None);
+    assert_eq!(name_pill_width(f32::NAN, 80.0), None);
+    // A bogus text width never makes a negative or NaN pill.
+    assert_eq!(name_pill_width(100.0, f32::NAN), Some(2.0 * PILL_PAD));
+    assert_eq!(name_pill_width(100.0, -40.0), Some(2.0 * PILL_PAD));
+}
+
+#[test]
+fn marks_list_the_rating_and_the_lock() {
+    let mut clip = test_clip(1, 0, 100);
+    assert_eq!(clip_marks(&clip, true), "");
+    clip.rating = 3;
+    assert_eq!(clip_marks(&clip, true), "★★★");
+    assert_eq!(clip_marks(&clip, false), "");
+    clip.edit_locked = true;
+    assert_eq!(clip_marks(&clip, true), "★★★ 🔒");
+    assert_eq!(clip_marks(&clip, false), "🔒");
+    clip.rating = 200;
+    assert_eq!(clip_marks(&clip, true), "★★★★★ 🔒");
+}
+
+#[test]
+fn the_clip_name_is_a_pill_over_the_waveform_not_a_bar() {
+    for show_name in [true, false] {
+        let mut h = smart_demo();
+        h.state_mut().engine.execute("view.clip_name", &json!({"value": show_name})).unwrap();
+        h.step();
+        let tl = timeline(&h);
+        let (clip, lane) = clip_on_screen(&h, |xs, xe| xs > tl.min.x + 30.0 && xe < tl.max.x - 100.0 && xe - xs > 100.0);
+        let s = h.state().engine.session();
+        let (xs, xe) = (x_of(s, tl, clip.start), x_of(s, tl, clip.end()));
+        let clip_rect = Rect::from_min_max(pos2(xs, lane.min.y), pos2(xe, lane.max.y));
+        let shapes = &h.output().shapes;
+        let pills: Vec<Rect> = shapes
+            .iter()
+            .filter_map(|shape| match &shape.shape {
+                Shape::Rect(r) if r.fill == PILL_BG && clip_rect.contains_rect(r.rect) => Some(r.rect),
+                _ => None,
+            })
+            .collect();
+        if !show_name {
+            assert!(pills.is_empty(), "names are off");
+            continue;
+        }
+        assert_eq!(pills.len(), 1, "one pill per clip");
+        let pill = pills[0];
+        // At the top-left, 1 pt inside the outline, as wide as its text plus padding: far from a full-width bar.
+        assert_eq!(pill.min, pos2(xs + PILL_INSET, lane.min.y + 1.0 + PILL_INSET));
+        assert_eq!(pill.height(), PILL_HEIGHT);
+        let text_w = shapes
+            .iter()
+            .find_map(|shape| match &shape.shape {
+                Shape::Text(t) if t.galley.text() == clip.name && pill.contains(t.pos) => Some(t.galley.size().x),
+                _ => None,
+            })
+            .expect("the name is drawn on the pill");
+        assert_eq!(pill.width(), text_w + 2.0 * PILL_PAD);
+        assert!(pill.width() < clip_rect.width() * 0.5);
+    }
+}
+
+#[test]
+fn the_rating_stars_follow_the_rating_switch_whatever_the_name_switch_says() {
+    for (show_name, show_rating) in [(false, true), (true, true), (true, false), (false, false)] {
+        let mut h = smart_demo();
+        for (cmd, on) in [("view.clip_name", show_name), ("view.clip_rating", show_rating)] {
+            h.state_mut().engine.execute(cmd, &json!({"value": on})).unwrap();
+        }
+        h.state_mut().engine.session_mut().tracks.iter_mut().flat_map(|t| &mut t.playlists).flat_map(|p| &mut p.clips).for_each(|c| c.rating = 3);
+        h.step();
+        let stars = h.output().shapes.iter().filter(|c| matches!(&c.shape, Shape::Text(t) if t.galley.text() == "★★★")).count();
+        assert_eq!(stars > 0, show_rating, "names {show_name}, rating {show_rating}: {stars} rated clips drew their stars");
+    }
+}
+
+#[test]
+fn alternate_playlist_lanes_draw_only_the_clips_on_screen() {
+    let mut h = harness(1);
+    let tl = timeline(&h);
+    let s = h.state().engine.session();
+    let (view, color) = (Range::new(sample_at(s, tl, tl.min.x), sample_at(s, tl, tl.max.x)), s.tracks[0].color);
+    let len = (view.end - view.start) / 20;
+    // One clip well inside the view, one running past its right edge, two far beyond it.
+    let starts = [view.start + 4 * len, view.end - len / 2, view.end + 20 * len, view.end + 2_000 * len];
+    let s = h.state_mut().engine.session_mut();
+    let mut alt = soundcraft_model::Playlist::new("alt");
+    alt.clips = starts.iter().map(|&at| test_clip(s.new_clip_id().0, at, len)).collect();
+    let track = s.tracks.first_mut().unwrap();
+    (track.view, track.active_playlist) = ("playlists".into(), 0);
+    track.playlists.push(alt);
+    h.run_steps(3);
+    // The main playlist is empty: every clip body in the track's rows belongs to the alternate lane.
+    let bodies: Vec<Rect> = h
+        .output()
+        .shapes
+        .iter()
+        .filter_map(|c| match &c.shape {
+            Shape::Rect(r) if r.fill == clip_colors(color, false).0 => Some(r.rect),
+            _ => None,
+        })
+        .collect();
+    assert_eq!(bodies.len(), 2, "only the two clips that show are drawn: {bodies:?}");
+    assert!(bodies.iter().all(|r| r.max.x > tl.min.x && r.min.x < tl.max.x), "{bodies:?} are not all on screen");
+}
+
+fn test_clip(id: u64, start: Samples, len: Samples) -> Clip {
+    Clip::audio(ClipId(id), "c", soundcraft_model::SourceId(0), 0, start, len)
+}
+
+#[test]
+fn cut_is_found_among_the_playlist_neighbours() {
+    // Selection never enters: it recolours the body, so a selected clip gets the same edges as any other.
+    let clips = [test_clip(1, 0, 100), test_clip(2, 100, 100), test_clip(3, 200, 100), test_clip(4, 400, 50)];
+    let cuts: Vec<ClipCut> = clips.iter().map(|c| clip_cut_in(&clips, c)).collect();
+    assert_eq!(cuts, [ClipCut { left: false, right: true }, ClipCut { left: true, right: true }, ClipCut { left: true, right: false }, ALONE]);
+}
+
+#[test]
+fn cut_does_not_depend_on_the_playlist_order() {
+    // Bring to Front / Send to Back reorder a playlist (its order is the stacking order): the seam stays.
+    let (a, b) = (test_clip(1, 0, 100), test_clip(2, 100, 100));
+    let reordered = [b.clone(), a.clone()];
+    assert_eq!(clip_cut_in(&reordered, &a), ClipCut { left: false, right: true });
+    assert_eq!(clip_cut_in(&reordered, &b), ClipCut { left: true, right: false });
+    let run = [test_clip(3, 200, 100), test_clip(1, 0, 100), test_clip(2, 100, 100)];
+    let cuts: Vec<ClipCut> = run.iter().map(|c| clip_cut_in(&run, c)).collect();
+    assert_eq!(cuts, [ClipCut { left: true, right: false }, ClipCut { left: false, right: true }, ClipCut { left: true, right: true }]);
+}
+
+#[test]
+fn layered_clips_share_a_seam_only_where_they_really_meet() {
+    // A short clip inside a long one does not hide the seam between the long clip and the next.
+    let clips = [test_clip(1, 0, 100), test_clip(2, 10, 10), test_clip(3, 100, 100)];
+    assert_eq!(clip_cut_in(&clips, &clips[2]), ClipCut { left: true, right: false });
+    assert_eq!(clip_cut_in(&clips, &clips[0]), ClipCut { left: false, right: true });
+    assert_eq!(clip_cut_in(&clips, &clips[1]), ALONE);
+    // Two clips on one start, and a lone clip.
+    let same = [test_clip(1, 0, 100), test_clip(2, 0, 100), test_clip(3, 300, 100)];
+    assert_eq!(clip_cut_in(&same, &same[1]), ALONE);
+    assert_eq!(clip_cut_in(&same, &same[2]), ALONE);
+    assert_eq!(clip_cut_in(&[], &test_clip(9, 0, 100)), ALONE);
+}
+
+#[test]
+fn marks_pill_is_hidden_when_the_clip_is_too_narrow_for_it() {
+    // A 60 pt clip from x=100 (pills start at 102) holds a 30 pt pill at its right end.
+    assert_eq!(marks_pill_left(102.0, 160.0, 30.0), Some(128.0));
+    // Exactly full: the pill's left edge meets the leftmost x.
+    assert_eq!(marks_pill_left(102.0, 134.0, 30.0), Some(102.0));
+    // Any narrower and it would veil the clip: hidden.
+    assert_eq!(marks_pill_left(102.0, 133.0, 30.0), None);
+    assert_eq!(marks_pill_left(102.0, f32::NAN, 30.0), None);
+}
+
+#[test]
+fn an_overlapping_clip_covers_the_pill_of_the_clip_below() {
+    let mut h = smart_demo();
+    h.step();
+    let tl = timeline(&h);
+    let (below, lane) = clip_on_screen(&h, |xs, xe| xs > tl.min.x + 30.0 && xe < tl.max.x - 100.0 && xe - xs > 100.0);
+    // A copy on top (last in the playlist), starting at the same place.
+    let s = h.state_mut().engine.session_mut();
+    let (track, _) = s.find_clip(below.id).unwrap();
+    let id = s.new_clip_id();
+    let top = Clip { id, name: "Top".into(), ..below.clone() };
+    s.track_mut(track).unwrap().playlist_mut().unwrap().clips.push(top);
+    h.step();
+    h.step();
+    let s = h.state().engine.session();
+    let at = pos2(x_of(s, tl, below.start) + PILL_INSET, lane.min.y + 1.0 + PILL_INSET);
+    let shapes = &h.output().shapes;
+    let pills: Vec<(usize, Rect)> = shapes
+        .iter()
+        .enumerate()
+        .filter_map(|(i, shape)| match &shape.shape {
+            Shape::Rect(r) if r.fill == PILL_BG && r.rect.min == at => Some((i, r.rect)),
+            _ => None,
+        })
+        .collect();
+    assert_eq!(pills.len(), 2, "both clips draw a pill at the same spot");
+    let ((lower, pill), (upper, _)) = (pills[0], pills[1]);
+    assert!(
+        shapes[lower + 1..upper].iter().any(|shape| matches!(&shape.shape, Shape::Rect(r) if r.fill != PILL_BG && r.rect.contains_rect(pill))),
+        "the top clip's body is painted between the two pills, so it hides the lower pill"
+    );
+}
diff --git a/crates/ui-egui/src/edit_window/tracks.rs b/crates/ui-egui/src/edit_window/tracks.rs
--- a/crates/ui-egui/src/edit_window/tracks.rs
+++ b/crates/ui-egui/src/edit_window/tracks.rs
@@ -86,11 +86,12 @@ pub(super) fn track_row(app: &mut SoundApp, ui: &mut Ui, id: TrackId, row: Rect,
     let view = Range::new(sample_at(&s, tl, tl.min.x), sample_at(&s, tl, tl.max.x));
     let auto_view = AutoParam::parse(&track.view);
     let sorted = track.clips().is_sorted_by_key(|c| c.start);
+    let cut_of = clip_cuts(track.clips());
     for clip in track.clips() {
         if !clip.range().overlaps(&view) {
             continue;
         }
-        draw_clip(app, &painter, &s, track, sorted, clip, tl, lane, auto_view.is_some());
+        draw_clip(app, &painter, &s, track, sorted, cut_of(clip), clip, tl, lane, auto_view.is_some());
     }
     draw_live_take(app, &painter, &s, track, tl, lane);
     crate::video_track::draw_lane(app, &painter, &s, track, tl, lane);
@@ -139,8 +140,9 @@ fn playlist_lanes(app: &mut SoundApp, ui: &mut Ui, track: &Track, row: Rect, mai
         let painter = ui.painter().with_clip_rect(lane.intersect(ui.clip_rect()));
         let sorted = track.clips().is_sorted_by_key(|c| c.start);
         let view = Range::new(sample_at(&s, tl, tl.min.x), sample_at(&s, tl, tl.max.x));
+        let cut_of = clip_cuts(&pl.clips);
         for c in pl.clips.iter().filter(|c| c.range().overlaps(&view)) {
-            draw_clip(app, &painter, &s, track, sorted, c, tl, lane, false);
+            draw_clip(app, &painter, &s, track, sorted, cut_of(c), c, tl, lane, false);
         }
         let resp = ui.interact(lane, ui.scope_id().with(("pl_lane", track.id.0, i)), Sense::click());
         if resp.clicked()
diff --git a/crates/ui-egui/src/video_track.rs b/crates/ui-egui/src/video_track.rs
--- a/crates/ui-egui/src/video_track.rs
+++ b/crates/ui-egui/src/video_track.rs
@@ -432,62 +432,65 @@ pub fn draw_lane(app: &mut SoundApp, painter: &Painter, s: &Session, track: &Tra
     let ctx = painter.ctx().clone();
     app.video.poll(&ctx);
     let on = online(s);
-    let name_h = if lane.height() >= 40.0 && s.edit.flag("view.clip.name") { 13.0 } else { 0.0 };
     let ppp = ctx.pixels_per_point();
     for clip in track.clips() {
         let ClipContent::Video { source, offset } = clip.content else { continue };
-        let x0 = x_of(s, tl, clip.start);
-        let x1 = x_of(s, tl, clip.end()).max(x0 + 1.0);
-        let body = Rect::from_min_max(pos2(x0 + 1.0, lane.min.y + 2.0 + name_h), pos2(x1 - 1.0, lane.max.y - 2.0));
-        let vis = body.intersect(lane);
-        if vis.width() < 2.0 || vis.height() < 6.0 {
-            continue;
-        }
-        let p = painter.with_clip_rect(vis);
-        let Some(v) = s.video(source) else {
-            p.text(vis.center(), egui::Align2::CENTER_CENTER, "movie missing from session", regular(10.0), Color32::from_rgb(220, 120, 120));
-            continue;
-        };
-        if !on {
-            p.text(vis.center(), egui::Align2::CENTER_CENTER, "Video Track Offline", regular(10.0), Color32::from_gray(170));
-            continue;
-        }
-        if let Some(why) = &v.offline {
-            p.text(vis.center(), egui::Align2::CENTER_CENTER, format!("media offline: {why}"), regular(10.0), Color32::from_rgb(220, 120, 120));
-            continue;
-        }
-        let key = MovieKey::new(source, &v.path);
-        if let Some(Err(e)) = app.video.info(key) {
-            p.text(vis.center(), egui::Align2::CENTER_CENTER, format!("media offline: {e}"), regular(10.0), Color32::from_rgb(220, 120, 120));
-            continue;
-        }
-        let aspect = app.video.info(key).and_then(|i| i.as_ref().ok()).map_or(v.aspect(), MovieInfo::display_aspect) as f32;
-        let th = body.height();
-        let tw = (th * aspect).clamp(8.0, 600.0);
-        // Pixel height bucket (multiples of 16) so zooming the track does not re-decode constantly.
-        let hpx = (((th * ppp) / 16.0).ceil() * 16.0).clamp(16.0, 256.0) as u32;
-        let first = ((vis.min.x - body.min.x) / tw).floor().max(0.0) as i64;
-        let mut k = first;
-        loop {
-            let tx = body.min.x + k as f32 * tw;
-            if tx > vis.max.x || k > first + 400 {
-                break;
+        'thumbs: {
+            let x0 = x_of(s, tl, clip.start);
+            let x1 = x_of(s, tl, clip.end()).max(x0 + 1.0);
+            let body = Rect::from_min_max(pos2(x0 + 1.0, lane.min.y + 2.0), pos2(x1 - 1.0, lane.max.y - 2.0));
+            let vis = body.intersect(lane);
+            if vis.width() < 2.0 || vis.height() < 6.0 {
+                break 'thumbs;
             }
-            let at = sample_at(s, tl, tx);
-            let secs = movie_secs(s, clip.start, offset, at.max(clip.start));
-            let r = Rect::from_min_max(pos2(tx, body.min.y), pos2((tx + tw).min(body.max.x), body.max.y));
-            match app.video.thumb(&ctx, key, &v.path, secs.max(0.0), hpx) {
-                Some(tex) => {
-                    let full = Rect::from_min_size(r.min, egui::vec2(tw, th));
-                    let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2((r.width() / tw).clamp(0.0, 1.0), 1.0));
-                    p.image(tex.id(), Rect::from_min_max(full.min, pos2(r.max.x, full.max.y)), uv, Color32::WHITE);
+            let p = painter.with_clip_rect(vis);
+            let Some(v) = s.video(source) else {
+                p.text(vis.center(), egui::Align2::CENTER_CENTER, "movie missing from session", regular(10.0), Color32::from_rgb(220, 120, 120));
+                break 'thumbs;
+            };
+            if !on {
+                p.text(vis.center(), egui::Align2::CENTER_CENTER, "Video Track Offline", regular(10.0), Color32::from_gray(170));
+                break 'thumbs;
+            }
+            if let Some(why) = &v.offline {
+                p.text(vis.center(), egui::Align2::CENTER_CENTER, format!("media offline: {why}"), regular(10.0), Color32::from_rgb(220, 120, 120));
+                break 'thumbs;
+            }
+            let key = MovieKey::new(source, &v.path);
+            if let Some(Err(e)) = app.video.info(key) {
+                p.text(vis.center(), egui::Align2::CENTER_CENTER, format!("media offline: {e}"), regular(10.0), Color32::from_rgb(220, 120, 120));
+                break 'thumbs;
+            }
+            let aspect = app.video.info(key).and_then(|i| i.as_ref().ok()).map_or(v.aspect(), MovieInfo::display_aspect) as f32;
+            let th = body.height();
+            let tw = (th * aspect).clamp(8.0, 600.0);
+            // Pixel height bucket (multiples of 16) so zooming the track does not re-decode constantly.
+            let hpx = (((th * ppp) / 16.0).ceil() * 16.0).clamp(16.0, 256.0) as u32;
+            let first = ((vis.min.x - body.min.x) / tw).floor().max(0.0) as i64;
+            let mut k = first;
+            loop {
+                let tx = body.min.x + k as f32 * tw;
+                if tx > vis.max.x || k > first + 400 {
+                    break;
                 }
-                None => {
-                    p.rect_filled(r.shrink(1.0), 0.0, Color32::from_rgb(24, 24, 26));
+                let at = sample_at(s, tl, tx);
+                let secs = movie_secs(s, clip.start, offset, at.max(clip.start));
+                let r = Rect::from_min_max(pos2(tx, body.min.y), pos2((tx + tw).min(body.max.x), body.max.y));
+                match app.video.thumb(&ctx, key, &v.path, secs.max(0.0), hpx) {
+                    Some(tex) => {
+                        let full = Rect::from_min_size(r.min, egui::vec2(tw, th));
+                        let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2((r.width() / tw).clamp(0.0, 1.0), 1.0));
+                        p.image(tex.id(), Rect::from_min_max(full.min, pos2(r.max.x, full.max.y)), uv, Color32::WHITE);
+                    }
+                    None => {
+                        p.rect_filled(r.shrink(1.0), 0.0, Color32::from_rgb(24, 24, 26));
+                    }
                 }
+                p.line_segment([pos2(tx, body.min.y), pos2(tx, body.max.y)], egui::Stroke::new(1.0, Color32::from_black_alpha(160)));
+                k += 1;
             }
-            p.line_segment([pos2(tx, body.min.y), pos2(tx, body.max.y)], egui::Stroke::new(1.0, Color32::from_black_alpha(160)));
-            k += 1;
         }
+        // The name pill goes over the thumbnails.
+        crate::edit_window::draw_clip_labels(painter, s, tl, lane, clip);
     }
 }
