diff --git a/crates/color/src/lib.rs b/crates/color/src/lib.rs
--- a/crates/color/src/lib.rs
+++ b/crates/color/src/lib.rs
@@ -97,7 +97,9 @@ pub fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
         return (0.0, 0.0, l);
     }
     let d = max - min;
-    let s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) };
+    let denom = if l > 0.5 { 2.0 - max - min } else { max + min };
+    // Over-range (HDR) or negative inputs can make the denominator zero or negative.
+    let s = if denom > 0.0 { (d / denom).min(1.0) } else { 1.0 };
     let h = if max == r {
         (((g - b) / d + if g < b { 6.0 } else { 0.0 }) / 6.0).rem_euclid(1.0)
     } else if max == g {
@@ -262,6 +264,17 @@ impl Label {
 mod tests {
     use super::*;
 
+    #[test]
+    fn hsl_round_trip_is_finite_for_out_of_range_colours() {
+        for (r, g, b) in [(1.5, 0.5, 0.5), (2.0, 0.1, 0.3), (-0.5, 0.2, 0.9)] {
+            let (h, s, l) = rgb_to_hsl(r, g, b);
+            assert!(h.is_finite() && s.is_finite() && l.is_finite(), "{h} {s} {l}");
+            assert!((0.0..=1.0).contains(&s), "saturation {s} for {r} {g} {b}");
+            let (r2, g2, b2) = hsl_to_rgb(h, s, l);
+            assert!(r2.is_finite() && g2.is_finite() && b2.is_finite(), "{r2} {g2} {b2}");
+        }
+    }
+
     #[test]
     fn hsv_to_rgb_tiny_negative_hue_is_red() {
         let (r, g, b) = hsv_to_rgb(-1e-9, 1.0, 1.0);
