diff --git a/crates/cms/src/gamut.rs b/crates/cms/src/gamut.rs
--- a/crates/cms/src/gamut.rs
+++ b/crates/cms/src/gamut.rs
@@ -9,21 +9,72 @@ use crate::{CmsError, Intent, check_len};
 /// Default ΔE76 threshold above which a colour counts as out of gamut.
 pub const DEFAULT_THRESHOLD: f32 = 4.0;
 
+/// Which colour-difference formula a gamut check measures the round trip with.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum Metric {
+    /// ΔE*ab (CIE 1976): the Euclidean distance in Lab. It overstates differences in saturated
+    /// colours, blues most, and understates them near neutrals, so one threshold flags too much
+    /// in one part of the space and too little in another.
+    #[default]
+    De76,
+    /// ΔE00 (CIEDE2000, CIE 142-2001): the weighted difference, which one threshold can be set
+    /// against across the whole space. About half the ΔE76 number for the same visual difference,
+    /// so the usual warning threshold is 2.0 rather than 4.0.
+    De2000,
+}
+
+impl Metric {
+    /// The threshold a gamut warning is usually drawn at in this metric.
+    pub const fn default_threshold(self) -> f32 {
+        match self {
+            Metric::De76 => DEFAULT_THRESHOLD,
+            Metric::De2000 => 2.0,
+        }
+    }
+}
+
 /// Precomputed out-of-gamut test for colours of `src` against `proof`.
 ///
 /// A colour is out of gamut when sending it to the proof device (relative colorimetric,
-/// clipping) and back to Lab moves it by more than the threshold (ΔE76). The ΔE is tabulated
-/// over the source device space (33³ grid for 3 channels, 17⁴ for 4, exact otherwise).
+/// clipping) and back to Lab moves it by more than the threshold. The difference is tabulated
+/// over the source device space (33³ grid for 3 channels, 17⁴ for 4, exact otherwise), in the
+/// [`Metric`] the check was built with.
 #[derive(Debug)]
 pub struct GamutCheck {
     inputs: usize,
     grid: Option<Clut>,
     exact: (Pipeline, Pipeline),
+    metric: Metric,
     pub threshold: f32,
 }
 
+/// The difference between two Lab colours in the ICC v4 encoding the pipelines here emit
+/// (L/100, (a+128)/255), in `metric`.
+fn difference(metric: Metric, a: &[f32], b: &[f32]) -> f32 {
+    match metric {
+        // The scaled differences, as before: the offsets cancel, and the result is the same
+        // number this reported before there was a choice of metric, bit for bit.
+        Metric::De76 => {
+            let d = |i: usize, s: f32| (a[i] - b[i]) * s;
+            (d(0, 100.0).powi(2) + d(1, 255.0).powi(2) + d(2, 255.0).powi(2)).sqrt()
+        }
+        // ΔE00 weights lightness, chroma and hue, so it needs the colours themselves.
+        Metric::De2000 => {
+            let lab = |v: &[f32]| [f64::from(v[0]) * 100.0, f64::from(v[1]) * 255.0 - 128.0, f64::from(v[2]) * 255.0 - 128.0];
+            crate::math::delta_e2000(lab(a), lab(b)) as f32
+        }
+    }
+}
+
 impl GamutCheck {
+    /// A check measuring in ΔE76, as this has always done.
     pub fn new(src: &Profile, proof: &Profile, threshold: f32) -> Result<GamutCheck, CmsError> {
+        GamutCheck::with_metric(src, proof, Metric::De76, threshold)
+    }
+
+    /// A check measuring in `metric`. [`Metric::default_threshold`] gives the threshold that
+    /// metric is usually read against.
+    pub fn with_metric(src: &Profile, proof: &Profile, metric: Metric, threshold: f32) -> Result<GamutCheck, CmsError> {
         let lab = crate::Builtin::LabD50.profile();
         let direct = Pipeline::new(src.channels(), link_stages(src, lab, Intent::RelativeColorimetric, false)?)?;
         let mut via = link_stages(src, proof, Intent::RelativeColorimetric, false)?;
@@ -34,8 +85,7 @@ impl GamutCheck {
             let (mut a, mut b) = ([0.0f32; 16], [0.0f32; 16]);
             direct.run(p, &mut a);
             via.run(p, &mut b);
-            let d = |i: usize, s: f32| (a[i] - b[i]) * s;
-            o[0] = (d(0, 100.0).powi(2) + d(1, 255.0).powi(2) + d(2, 255.0).powi(2)).sqrt();
+            o[0] = difference(metric, &a, &b);
         };
         let n = src.channels();
         let grid = match n {
@@ -44,14 +94,20 @@ impl GamutCheck {
             4 => Some(Clut::sample(vec![17; 4], 1, de)?),
             _ => None,
         };
-        Ok(GamutCheck { inputs: n, grid, exact: (direct, via), threshold })
+        Ok(GamutCheck { inputs: n, grid, exact: (direct, via), metric, threshold })
     }
 
     pub fn inputs(&self) -> usize {
         self.inputs
     }
 
-    /// ΔE76 between the colour (at least `inputs` normalised values) and its proof round trip.
+    /// The metric this check measures in.
+    pub fn metric(&self) -> Metric {
+        self.metric
+    }
+
+    /// The difference between the colour (at least `inputs` normalised values) and its proof
+    /// round trip, in [`GamutCheck::metric`].
     pub fn delta_e(&self, px: &[f32]) -> Result<f32, CmsError> {
         check_len("gamut check input", px.len(), self.inputs)?;
         let mut o = [0.0f32; 1];
@@ -67,7 +123,7 @@ impl GamutCheck {
                 let (mut a, mut b) = ([0.0f32; 16], [0.0f32; 16]);
                 self.exact.0.run(px, &mut a);
                 self.exact.1.run(px, &mut b);
-                o[0] = ((a[0] - b[0]) * 100.0).hypot((a[1] - b[1]) * 255.0).hypot((a[2] - b[2]) * 255.0);
+                o[0] = difference(self.metric, &a, &b);
             }
         }
         Ok(o[0])
diff --git a/crates/cms/src/lib.rs b/crates/cms/src/lib.rs
--- a/crates/cms/src/lib.rs
+++ b/crates/cms/src/lib.rs
@@ -32,7 +32,7 @@ mod write;
 pub use builtin::Builtin;
 pub use clut::Clut;
 pub use curve::Curve;
-pub use gamut::GamutCheck;
+pub use gamut::{GamutCheck, Metric};
 pub use lut3d::Lut3d;
 pub use profile::{ColorSpace, Pcs, Profile, ProfileClass};
 pub use transform::{SampleKind, Transform, TransformOptions, black_point, cached};
diff --git a/crates/cms/tests/cms.rs b/crates/cms/tests/cms.rs
--- a/crates/cms/tests/cms.rs
+++ b/crates/cms/tests/cms.rs
@@ -991,3 +991,61 @@ fn white_is_exact_on_every_path() {
         assert!(o[2] < 0.99, "{cmyk:?}: absolute paper {o:?}");
     }
 }
+
+/// `GamutCheck` measures in either metric (#62). ΔE76 overstates saturated colours — sRGB blue
+/// against the coated CMYK proof is 79.6 in ΔE76 and 15.0 in ΔE00 — and understates them near
+/// neutrals, so at the thresholds each metric is read against (4.0 and 2.0) the two disagree in
+/// both directions. DesignCraft computed its own warning in ΔE00 for that reason.
+#[test]
+fn gamut_check_measures_in_either_metric() {
+    use craft_cms::{GamutCheck, Metric};
+
+    assert_eq!(Metric::default(), Metric::De76, "the metric this has always used");
+    assert_eq!(Metric::De76.default_threshold(), 4.0);
+    assert_eq!(Metric::De2000.default_threshold(), 2.0);
+
+    let de76 = GamutCheck::with_metric(srgb(), cmyk(), Metric::De76, Metric::De76.default_threshold()).unwrap();
+    let de00 = GamutCheck::with_metric(srgb(), cmyk(), Metric::De2000, Metric::De2000.default_threshold()).unwrap();
+    assert_eq!(de76.metric(), Metric::De76);
+    assert_eq!(de00.metric(), Metric::De2000);
+
+    // `new` is the ΔE76 check it always was.
+    let old = GamutCheck::new(srgb(), cmyk(), 4.0).unwrap();
+    assert_eq!(old.metric(), Metric::De76);
+    for px in [[0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.5, 0.5, 0.5]] {
+        assert_eq!(old.delta_e(&px).unwrap(), de76.delta_e(&px).unwrap(), "{px:?}");
+    }
+
+    // Saturated blue, the colour ΔE76 overstates most: both call it out of gamut, by numbers that
+    // are five times apart.
+    let blue = [0.0, 0.0, 1.0];
+    let (a, b) = (de76.delta_e(&blue).unwrap(), de00.delta_e(&blue).unwrap());
+    assert!((70.0..90.0).contains(&a), "blue in dE76: {a}");
+    assert!((10.0..20.0).contains(&b), "blue in dE2000: {b}");
+    assert!(a / b > 4.0, "dE76 overstates blue: {a} / {b}");
+    assert!(de76.out_of_gamut(&blue).unwrap() && de00.out_of_gamut(&blue).unwrap());
+
+    // A dark near-neutral olive, where ΔE00 is the larger of the two.
+    let olive = [0.3125, 0.25, 0.1875];
+    let (a, b) = (de76.delta_e(&olive).unwrap(), de00.delta_e(&olive).unwrap());
+    assert!(b > a, "dE2000 understated near the neutral axis: {a} / {b}");
+
+    // Over the cube, each metric flags colours the other does not, at its own threshold: the one
+    // threshold ΔE76 is given cannot hold for both ends of the space.
+    let n = 17;
+    let (mut only76, mut only00) = (0, 0);
+    for i in 0..n {
+        for j in 0..n {
+            for k in 0..n {
+                let d = |v: usize| v as f32 / (n - 1) as f32;
+                let px = [d(i), d(j), d(k)];
+                match (de76.out_of_gamut(&px).unwrap(), de00.out_of_gamut(&px).unwrap()) {
+                    (true, false) => only76 += 1,
+                    (false, true) => only00 += 1,
+                    _ => {}
+                }
+            }
+        }
+    }
+    assert!(only76 > 0 && only00 > 0, "dE76 only {only76}, dE2000 only {only00} of {}", n * n * n);
+}
