diff --git a/apps/lightcraft/src/editors.rs b/apps/lightcraft/src/editors.rs
--- a/apps/lightcraft/src/editors.rs
+++ b/apps/lightcraft/src/editors.rs
@@ -111,6 +111,7 @@ fn windows_default_command(path: &str) -> Command {
     c
 }
 
+/// Resolve an external editor name to its platform launcher, reporting when PhotoCraft is missing.
 fn launcher(app: &str) -> Result<Launcher, String> {
     let app = app.trim();
     if app.is_empty() {
@@ -120,7 +121,7 @@ fn launcher(app: &str) -> Result<Launcher, String> {
         return Ok(if cfg!(target_os = "macos") { Launcher::MacApp(app.to_string()) } else { Launcher::Program(PathBuf::from(app)) });
     }
     find_photocraft().ok_or_else(|| {
-        "PhotoCraft isn't installed. Get it from getartcraft.com/apps/photocraft, or give where it is in Settings ▸ External Editing.".to_string()
+        "PhotoCraft isn't installed. Get it from getartcraft.com/apps/photocraft, or give where it is in Settings → External Editing.".to_string()
     })
 }
 
diff --git a/apps/lightcraft/src/launch.rs b/apps/lightcraft/src/launch.rs
--- a/apps/lightcraft/src/launch.rs
+++ b/apps/lightcraft/src/launch.rs
@@ -36,6 +36,8 @@ pub(crate) fn launch_imports(args: &[String]) -> (Vec<String>, Vec<String>) {
     launch_imports_in(args, cwd.as_deref(), home.as_deref())
 }
 
+/// Split launch arguments into importable paths and refusal notices. Relative paths use `cwd`;
+/// `home` identifies the whole home folder, which must not be imported.
 pub(crate) fn launch_imports_in(args: &[String], cwd: Option<&std::path::Path>, home: Option<&std::path::Path>) -> (Vec<String>, Vec<String>) {
     use lightcraft_engine::import::{LaunchPath, launch_path};
     let (mut files, mut notices) = (Vec::new(), Vec::new());
@@ -45,7 +47,7 @@ pub(crate) fn launch_imports_in(args: &[String], cwd: Option<&std::path::Path>,
             Some(LaunchPath::Refused { path, why }) => {
                 log::warn!("not importing {path} given at launch: {why}");
                 notices
-                    .push(format!("{path} was not imported: it is {why}. To import from it, choose a folder inside it with File ▸ Import Photos…"));
+                    .push(format!("{path} was not imported: it is {why}. To import from it, choose a folder inside it with File → Import Photos…"));
             }
             None => {}
         }
@@ -85,7 +87,7 @@ pub(crate) fn stopped_text(error: &str, lost: Option<&str>) -> String {
     match lost {
         Some(reason) => format!(
             "{error}. The graphics driver reset the GPU ({reason}). LightCraft renders on the CPU from its next start; \
-             Settings ▸ Performance ▸ Use the GPU for rendering turns the GPU back on"
+             Settings → Performance → Use the GPU for rendering turns the GPU back on"
         ),
         None => error.to_string(),
     }
diff --git a/apps/lightcraft/src/native_menu.rs b/apps/lightcraft/src/native_menu.rs
--- a/apps/lightcraft/src/native_menu.rs
+++ b/apps/lightcraft/src/native_menu.rs
@@ -489,9 +489,7 @@ impl NativeMenu {
             // events never reach `shortcuts::handle`, so this is the one guard for AppKit's
             // accelerators and clicks: while the blocking library window or a quit/settings notice
             // owns input, a queued command is dropped (Quit stays for the existing confirmation).
-            let (cmd, params) = if key == QUIT {
-                (key, serde_json::json!({}))
-            } else if key == "app.about" || key == SETTINGS {
+            let (cmd, params) = if key == QUIT || key == "app.about" || key == SETTINGS {
                 (key, serde_json::json!({}))
             } else if let Some(it) = self.items.get(&key) {
                 (it.cmd.clone(), it.params.clone())
diff --git a/crates/engine/src/camera_preview.rs b/crates/engine/src/camera_preview.rs
--- a/crates/engine/src/camera_preview.rs
+++ b/crates/engine/src/camera_preview.rs
@@ -50,7 +50,8 @@ use pairs::{
 pub(crate) use proxy::LensGeometry;
 #[cfg(test)]
 use proxy::{
-    Cr3Framing, MIN_PREVIEW_FRACTION, alignment, estimate_cr3_framing, into_lens_geometry, proxies, reference_at, sensor_proxy, structure_agrees,
+    Cr3Framing, MIN_PREVIEW_FRACTION, align_profile_geometry, alignment, estimate_cr3_framing, into_lens_geometry, proxies, reference_at,
+    resample_correspondence, sensor_proxy, structure_agrees,
 };
 pub(crate) use proxy::{fit_profile, lens_geometry, profile_pairs};
 #[cfg(test)]
@@ -79,7 +80,8 @@ use tone::{HighlightCheck, ToneFit, fit_tone, fit_tone_conditional, highlights};
 /// a tone-only fallback for near-black scenes with too few colour pairs (#1587); DNG tone fitting uses a
 /// lens-corrected proxy when the file carries lens opcodes; validated A7CR distortion, including the lens data
 /// stored with each smart preview.
-pub const LOOK_VERSION: u32 = 12;
+/// 14: geometry-gated external lens and camera framing, including CR3, for JPEG correspondences.
+pub const LOOK_VERSION: u32 = 14;
 
 #[derive(Clone, Debug)]
 pub(crate) struct CameraLook {
diff --git a/crates/engine/src/camera_preview/colour.rs b/crates/engine/src/camera_preview/colour.rs
--- a/crates/engine/src/camera_preview/colour.rs
+++ b/crates/engine/src/camera_preview/colour.rs
@@ -42,7 +42,8 @@ pub(crate) fn file_local_look(format: RawFormat) -> bool {
 /// as-shot white, the camera transform at it and the fitted look.
 /// `lens` is the file's own lens-correction warp ([`lens_geometry`], read before the caller removes it from
 /// `OpcodeList3`): the camera applied it to its JPEG, so the sensor proxy is warped the same way before pairing.
-/// `embedded` is the file's embedded lens vignetting, for the DNG tone fit.
+/// `embedded` is the resolved lens in the oriented default crop: geometry may align
+/// CR3 calibration correspondences; vignetting is used by the DNG tone fit.
 pub(crate) fn starting_colour(
     raw: &mut RawImage,
     bytes: &[u8],
@@ -51,13 +52,13 @@ pub(crate) fn starting_colour(
 ) -> (Xy, CameraTransform, Option<CameraLook>) {
     let (xy, t, look) = starting_colour_with(
         raw,
-        |raw, t| fit_preview(raw, bytes, t, lens),
-        |raw, t| monochrome_look(raw, bytes, t, lens),
+        |raw, t| fit_preview(raw, bytes, t, lens, embedded),
+        |raw, t| monochrome_look(raw, bytes, t, lens, embedded),
         |raw, t| {
             if !file_local_look(raw.format) {
                 return None;
             }
-            let (sensor, reference, clipped) = proxies(raw, bytes, t, PROXY, 0.0, lens)?;
+            let (sensor, reference, clipped) = proxies(raw, bytes, t, PROXY, 0.0, lens, embedded)?;
             fit_dark_tone(&sensor, &reference, &clipped)
         },
     );
@@ -130,11 +131,22 @@ pub(crate) fn starting_colour_tags(raw: &mut RawImage, bytes: &[u8]) -> Option<C
     if lightcraft_raw::color::has_matrix(&raw.color) {
         return None;
     }
+    let correction = if raw.format == RawFormat::Cr3 { crate::lens_profiles::for_raw(&raw.info(), || lightcraft_meta::extract(bytes)) } else { None };
+    starting_colour_tags_with_correction(raw, bytes, correction.as_ref())
+}
+
+/// Build DNG colour tags using `correction` only for CR3 camera-JPEG correspondence geometry.
+pub(super) fn starting_colour_tags_with_correction(
+    raw: &mut RawImage,
+    bytes: &[u8],
+    correction: Option<&lightcraft_develop::EmbeddedLens>,
+) -> Option<ColorData> {
+    let correction = correction.filter(|_| raw.format == RawFormat::Cr3);
     // the white the source shows, decided before `starting_colour` may fill in measured matrices
     let scale = crate::files::WbScaleOf::new(raw.format, &raw.color, raw.wb_multipliers, &raw.metadata);
     let white = scale.dng_white(&raw.color, lightcraft_raw::color::as_shot_white_xy(raw));
     // (a merge frame arrives without its lens warp: `lightcraft_merge` removes it before calling)
-    let (_, t, look) = starting_colour(raw, bytes, None, None);
+    let (_, t, look) = starting_colour(raw, bytes, None, correction);
     dng_colour(&raw.color, &white, &t, look.as_ref())
 }
 
diff --git a/crates/engine/src/camera_preview/look_fit.rs b/crates/engine/src/camera_preview/look_fit.rs
--- a/crates/engine/src/camera_preview/look_fit.rs
+++ b/crates/engine/src/camera_preview/look_fit.rs
@@ -5,12 +5,19 @@ use super::proxy::{CLIP_TOLERANCE, LensGeometry, MIN_PREVIEW_FRACTION, proxies,
 use super::tone::{HighlightCheck, highlights};
 use super::*;
 
-/// The look fitted to the file's camera JPEG, and whether that JPEG is black and white ([`monochrome`]).
-pub(crate) fn fit_preview(raw: &RawImage, bytes: &[u8], transform: &CameraTransform, lens: Option<&LensGeometry>) -> (Option<CameraLook>, bool) {
+/// Fit the camera JPEG's look, using `correction` to align CR3 sensor/JPEG geometry when eligible;
+/// also report whether the JPEG is black and white ([`monochrome`]).
+pub(crate) fn fit_preview(
+    raw: &RawImage,
+    bytes: &[u8],
+    transform: &CameraTransform,
+    lens: Option<&LensGeometry>,
+    correction: Option<&lightcraft_develop::EmbeddedLens>,
+) -> (Option<CameraLook>, bool) {
     if !transform.matrix_is_fallback || !file_local_look(raw.format) {
         return (None, false);
     }
-    let Some((sensor, reference, clipped)) = proxies(raw, bytes, transform, PROXY, 0.0, lens) else { return (None, false) };
+    let Some((sensor, reference, clipped)) = proxies(raw, bytes, transform, PROXY, 0.0, lens, correction) else { return (None, false) };
     let black_and_white = monochrome(&reference);
     // A camera profile pooled from many photos knows colours this photo shows too little of;
     // try its colour first and fit tone/chroma per photo (picture styles vary).
@@ -207,7 +214,9 @@ pub(super) fn fit_dng_tone(
         }
         None
     };
-    let Some((mut sensor, reference, clipped)) = proxies(raw, bytes, transform, PROXY, MIN_PREVIEW_FRACTION, None) else { return refuse("preview") };
+    let Some((mut sensor, reference, clipped)) = proxies(raw, bytes, transform, PROXY, MIN_PREVIEW_FRACTION, None, None) else {
+        return refuse("preview");
+    };
     // As the loader renders it: the profile's tables around the baseline exposure.
     let xy = lightcraft_raw::color::as_shot_white_xy(raw);
     if let Some(tables) = lightcraft_raw::profile::ProfileTables::new(&raw.color.profile, lightcraft_raw::color::illuminant_weight(&raw.color, xy)) {
diff --git a/crates/engine/src/camera_preview/mono.rs b/crates/engine/src/camera_preview/mono.rs
--- a/crates/engine/src/camera_preview/mono.rs
+++ b/crates/engine/src/camera_preview/mono.rs
@@ -33,12 +33,18 @@ pub(super) fn monochrome(reference: &Rgb32f) -> bool {
 /// and flatter than the same body's photos shot in colour. But the JPEG still shows the camera's tone curve.
 /// `transform` is the colour the photo gets otherwise (the spectral matrices or the fallback's); the look keeps it and
 /// adds the tone curve alone ([`fit_tone_only_with`]), as [`fit_look_with`] tries the photo's own colour after a profile's. The
-/// chroma curve stays neutral: the JPEG has no colourfulness to learn.
-pub(super) fn monochrome_look(raw: &RawImage, bytes: &[u8], transform: &CameraTransform, lens: Option<&LensGeometry>) -> Option<CameraLook> {
+/// chroma curve stays neutral: the JPEG has no colourfulness to learn. `correction` may align CR3 correspondence geometry.
+pub(super) fn monochrome_look(
+    raw: &RawImage,
+    bytes: &[u8],
+    transform: &CameraTransform,
+    lens: Option<&LensGeometry>,
+    correction: Option<&lightcraft_develop::EmbeddedLens>,
+) -> Option<CameraLook> {
     if !file_local_look(raw.format) {
         return None;
     }
-    let (sensor, reference, clipped) = proxies(raw, bytes, transform, PROXY, 0.0, lens)?;
+    let (sensor, reference, clipped) = proxies(raw, bytes, transform, PROXY, 0.0, lens, correction)?;
     if !monochrome(&reference) {
         return None;
     }
diff --git a/crates/engine/src/camera_preview/proxy.rs b/crates/engine/src/camera_preview/proxy.rs
--- a/crates/engine/src/camera_preview/proxy.rs
+++ b/crates/engine/src/camera_preview/proxy.rs
@@ -82,7 +82,8 @@ pub(super) fn structure_agrees(sensor: &Rgb32f, reference: &Rgb32f) -> bool {
 
 /// Same-size proxies of the sensor (white-balanced, baseline exposure, through `transform`'s
 /// matrix: the generic camera ≈ sRGB model) and of the file's embedded camera JPEG (linear Rec.2020),
-/// and per proxy pixel whether any sensor sample it covers is clipped.
+/// and per proxy pixel whether any sensor sample it covers is clipped. `correction` proposes CR3
+/// correspondence geometry; it must pass the independent geometry gates without worsening the framing fallback.
 pub(super) fn proxies(
     raw: &RawImage,
     bytes: &[u8],
@@ -90,6 +91,7 @@ pub(super) fn proxies(
     size: usize,
     min_fraction: f64,
     lens: Option<&LensGeometry>,
+    correction: Option<&lightcraft_develop::EmbeddedLens>,
 ) -> Option<(Rgb32f, Rgb32f, Vec<bool>)> {
     let edge = (2 * size).max(384) as u32;
     let decoded = crate::files::decode_raw_preview(
@@ -139,9 +141,14 @@ pub(super) fn proxies(
         clipped = clipped.into_oriented(raw.orientation);
         reference = reference.into_oriented(raw.orientation);
         sensor.map_in_place(to_working);
-        // Canon's JPEG can be cropped differently from the sensor. Estimate only that common
-        // framing from edge directions, independently of the subsequent colour fit.
-        (sensor, clipped) = align_cr3_framing(sensor, clipped, &reference);
+        // Judge the external warp against the established CR3 framing fallback, not
+        // against unregistered sensor pixels. This preserves an already better fit.
+        let (fallback, fallback_mask) = align_cr3_framing(sensor.clone(), clipped.clone(), &reference);
+        let proposed =
+            correction.filter(|correction| transform.matrix_is_fallback && lens.is_none() && correction.warp.is_some()).and_then(|correction| {
+                align_profile_geometry_against(&sensor, &clipped, &reference, correction, lightcraft_geom::Orientation::Normal, Some(&fallback))
+            });
+        (sensor, clipped) = proposed.unwrap_or((fallback, fallback_mask));
     }
     let mut sensor = fit(&sensor, size, size, Filter::Box);
     let mut reference_small = reference_at(&reference, &sensor);
@@ -180,7 +187,7 @@ pub(super) struct Cr3Framing {
 }
 
 impl Cr3Framing {
-    const IDENTITY: Self = Self { scale: 1.0, offset: [0.0; 2] };
+    pub(super) const IDENTITY: Self = Self { scale: 1.0, offset: [0.0; 2] };
 
     pub(super) fn source(self, x: f32, y: f32, w: usize, h: usize) -> (f32, f32) {
         let (w, h) = (w as f32, h as f32);
@@ -193,6 +200,37 @@ impl Cr3Framing {
 /// A separate third of the edges must confirm a substantial improvement and a close match.
 /// A weak, flat or unrelated preview leaves the sensor framing unchanged.
 pub(super) fn estimate_cr3_framing(sensor: &Rgb32f, reference: &Rgb32f) -> Option<Cr3Framing> {
+    estimate_framing_against(sensor, reference, sensor, None)
+}
+
+#[cfg(test)]
+thread_local! {
+    static LAST_GEOMETRY_SCORES: std::cell::Cell<Option<[f64; 4]>> = const { std::cell::Cell::new(None) };
+    static LAST_FRAMED_SCORES: std::cell::Cell<Option<[f64; 4]>> = const { std::cell::Cell::new(None) };
+}
+
+/// Return this test thread's last `[train_before, train_after, held_before, held_after]`
+/// scores against the original sensor geometry.
+#[cfg(test)]
+pub(super) fn geometry_gate_scores() -> Option<[f64; 4]> {
+    LAST_GEOMETRY_SCORES.get()
+}
+
+/// Return this test thread's last `[train_before, train_after, held_before, held_after]`
+/// scores comparing the CR3 proposal with the established framing fallback.
+#[cfg(test)]
+pub(super) fn geometry_framed_scores() -> Option<[f64; 4]> {
+    LAST_FRAMED_SCORES.get()
+}
+
+/// Judge a proposed lens warp and bounded framing against the original sensor on fixed
+/// training and independent held-out edges. Lens-only correction may be sufficient.
+fn estimate_framing_against(sensor: &Rgb32f, reference: &Rgb32f, original: &Rgb32f, framed: Option<&Rgb32f>) -> Option<Cr3Framing> {
+    #[cfg(test)]
+    {
+        LAST_GEOMETRY_SCORES.set(None);
+        LAST_FRAMED_SCORES.set(None);
+    }
     if sensor.width.checked_mul(sensor.height)? != sensor.data.len() || reference.width.checked_mul(reference.height)? != reference.data.len() {
         return None;
     }
@@ -205,7 +243,17 @@ pub(super) fn estimate_cr3_framing(sensor: &Rgb32f, reference: &Rgb32f) -> Optio
     if (reference.width, reference.height) != (w, h) {
         return None;
     }
+    let original = fit(original, PROXY, PROXY, Filter::Box);
+    if (original.width, original.height) != (w, h) || original.data.len() != sensor.data.len() {
+        return None;
+    }
+    let framed = framed.map(|image| fit(image, PROXY, PROXY, Filter::Box));
+    if framed.as_ref().is_some_and(|image| (image.width, image.height) != (w, h) || image.data.len() != sensor.data.len()) {
+        return None;
+    }
+    let mut framed_gradients = framed.as_ref().map(|_| Rgb32f::new(w, h));
     let mut gradients = Rgb32f::new(w, h);
+    let mut original_gradients = Rgb32f::new(w, h);
     let mut edges = Vec::new();
     for y in 1..h - 1 {
         for x in 1..w - 1 {
@@ -217,6 +265,12 @@ pub(super) fn estimate_cr3_framing(sensor: &Rgb32f, reference: &Rgb32f) -> Optio
             };
             let d = derivative(&sensor);
             gradients.data[y * w + x] = [d[0], d[1], 0.0];
+            let d = derivative(&original);
+            original_gradients.data[y * w + x] = [d[0], d[1], 0.0];
+            if let (Some(image), Some(gradients)) = (&framed, &mut framed_gradients) {
+                let d = derivative(image);
+                gradients.data[y * w + x] = [d[0], d[1], 0.0];
+            }
             let d = derivative(&reference);
             let magnitude = d[0].hypot(d[1]);
             let value = luminance_2020(reference.data[y * w + x]);
@@ -228,7 +282,7 @@ pub(super) fn estimate_cr3_framing(sensor: &Rgb32f, reference: &Rgb32f) -> Optio
     if edges.len() < 192 {
         return None;
     }
-    let score = |framing: Cr3Framing, held_out: bool| -> Option<f64> {
+    let score = |gradients: &Rgb32f, framing: Cr3Framing, held_out: bool, support: Option<(&Rgb32f, Cr3Framing)>| -> Option<f64> {
         let (mut agreement, mut weight, mut valid, mut total) = (0.0, 0.0, 0usize, 0usize);
         for &(x, y, target, magnitude, holdout) in &edges {
             if holdout != held_out {
@@ -244,6 +298,19 @@ pub(super) fn estimate_cr3_framing(sensor: &Rgb32f, reference: &Rgb32f) -> Optio
             if !length.is_finite() || length <= 0.01 {
                 continue;
             }
+            // Comparing against framed CR3 fallback uses exactly the same valid
+            // edges on both sides, including the same reference weights.
+            if let Some((other, map)) = support {
+                let (ox, oy) = map.source(x, y, w, h);
+                if ox < 1.5 || oy < 1.5 || ox > w as f32 - 1.5 || oy > h as f32 - 1.5 {
+                    continue;
+                }
+                let p = other.sample_bilinear(ox, oy);
+                let length = p[0].hypot(p[1]);
+                if !length.is_finite() || length <= 0.01 {
+                    continue;
+                }
+            }
             let k = f64::from(magnitude.min(0.25));
             agreement += k * f64::from((sample[0] * target[0] + sample[1] * target[1]) / (length * magnitude));
             weight += k;
@@ -251,15 +318,15 @@ pub(super) fn estimate_cr3_framing(sensor: &Rgb32f, reference: &Rgb32f) -> Optio
         }
         (valid >= 64 && valid * 5 >= total * 4 && weight > 0.0).then_some(agreement / weight)
     };
-    let before = score(Cr3Framing::IDENTITY, false)?;
-    let mut best = (before, Cr3Framing::IDENTITY);
+    let before = score(&original_gradients, Cr3Framing::IDENTITY, false, None)?;
+    let mut best = (score(&gradients, Cr3Framing::IDENTITY, false, None).unwrap_or(-1.0), Cr3Framing::IDENTITY);
     // At most 10% zoom and two proxy pixels of translation; a coarse search followed by one
     // local refinement prevents unbounded optimisation on the photograph's contents.
     for s in -10..=10 {
         for tx in -4..=4 {
             for ty in -4..=4 {
                 let framing = Cr3Framing { scale: 1.0 + s as f32 * 0.01, offset: [tx as f32 * 0.5 / w as f32, ty as f32 * 0.5 / h as f32] };
-                if let Some(value) = score(framing, false)
+                if let Some(value) = score(&gradients, framing, false, None)
                     && value > best.0
                 {
                     best = (value, framing);
@@ -278,7 +345,7 @@ pub(super) fn estimate_cr3_framing(sensor: &Rgb32f, reference: &Rgb32f) -> Optio
                 if !(0.9..=1.1).contains(&framing.scale) || framing.offset[0].abs() > 2.0 / w as f32 || framing.offset[1].abs() > 2.0 / h as f32 {
                     continue;
                 }
-                if let Some(value) = score(framing, false)
+                if let Some(value) = score(&gradients, framing, false, None)
                     && value > best.0
                 {
                     best = (value, framing);
@@ -286,12 +353,25 @@ pub(super) fn estimate_cr3_framing(sensor: &Rgb32f, reference: &Rgb32f) -> Optio
             }
         }
     }
-    let (held_before, held_after) = (score(Cr3Framing::IDENTITY, true)?, score(best.1, true)?);
+    let (held_before, held_after) = (score(&original_gradients, Cr3Framing::IDENTITY, true, None)?, score(&gradients, best.1, true, None)?);
+    #[cfg(test)]
+    LAST_GEOMETRY_SCORES.set(Some([before, best.0, held_before, held_after]));
     if best.0 < 0.9 || best.0 < before + 0.04 || held_after < 0.9 || held_after < held_before + 0.04 {
         return None;
     }
+    if let Some(framed) = &framed_gradients {
+        let before = score(framed, Cr3Framing::IDENTITY, false, Some((&gradients, best.1)))?;
+        let after = score(&gradients, best.1, false, Some((framed, Cr3Framing::IDENTITY)))?;
+        let held_before = score(framed, Cr3Framing::IDENTITY, true, Some((&gradients, best.1)))?;
+        let held_after = score(&gradients, best.1, true, Some((framed, Cr3Framing::IDENTITY)))?;
+        #[cfg(test)]
+        LAST_FRAMED_SCORES.set(Some([before, after, held_before, held_after]));
+        if after < before || held_after < held_before {
+            return None;
+        }
+    }
     if lightcraft_pipeline::profiling() {
-        eprintln!("[profile] CR3 JPEG framing {:?}, held-out edge agreement {held_before:.4} -> {held_after:.4}", best.1);
+        eprintln!("[profile] camera JPEG framing {:?}, held-out edge agreement {held_before:.4} -> {held_after:.4}", best.1);
     }
     Some(best.1)
 }
@@ -311,15 +391,82 @@ fn align_cr3_framing(sensor: Rgb32f, clipped: Rgb32f, reference: &Rgb32f) -> (Rg
     (apply(&sensor), apply(&clipped))
 }
 
+/// Apply a shared source map to colour or a nonnegative clip mask. Invalid borders
+/// stay NaN; any bilinear contribution from a clipped source stays positive.
+pub(super) fn resample_correspondence(image: &Rgb32f, source: impl Fn(f64, f64) -> lightcraft_geom::Point) -> Rgb32f {
+    let (w, h) = (image.width, image.height);
+    let mut out = Rgb32f::new(w, h);
+    if w == 0 || h == 0 {
+        return out;
+    }
+    for (i, pixel) in out.data.iter_mut().enumerate() {
+        let p = source((i % w) as f64 + 0.5, (i / w) as f64 + 0.5);
+        *pixel = if p.x.is_finite() && p.y.is_finite() && p.x >= 0.5 && p.y >= 0.5 && p.x <= w as f64 - 0.5 && p.y <= h as f64 - 0.5 {
+            image.sample_bilinear(p.x as f32, p.y as f32)
+        } else {
+            [f32::NAN; 3]
+        };
+    }
+    out
+}
+
+/// Propose correspondence geometry only. A resolved lens describes the oriented
+/// default crop; undo its orientation to match this un-oriented sensor proxy.
+#[cfg(test)]
+pub(super) fn align_profile_geometry(
+    sensor: &Rgb32f,
+    clipped: &Rgb32f,
+    reference: &Rgb32f,
+    correction: &lightcraft_develop::EmbeddedLens,
+    orientation: lightcraft_geom::Orientation,
+) -> Option<(Rgb32f, Rgb32f)> {
+    align_profile_geometry_against(sensor, clipped, reference, correction, orientation, None)
+}
+
+/// Propose lens geometry plus bounded framing and require the original training/held-out gates.
+/// When supplied, the framed CR3 baseline must also be preserved or improved on both splits.
+fn align_profile_geometry_against(
+    sensor: &Rgb32f,
+    clipped: &Rgb32f,
+    reference: &Rgb32f,
+    correction: &lightcraft_develop::EmbeddedLens,
+    orientation: lightcraft_geom::Orientation,
+    baseline: Option<&Rgb32f>,
+) -> Option<(Rgb32f, Rgb32f)> {
+    if correction.warp.is_none() || (sensor.width, sensor.height) != (clipped.width, clipped.height) {
+        return None;
+    }
+    let (w, h) = (sensor.width as f64, sensor.height as f64);
+    let (ow, oh) = if orientation.swaps_axes() { (h, w) } else { (w, h) };
+    let correction = lightcraft_pipeline::optics::reorient_lens(correction, orientation.inverse(), ow, oh);
+    let mut warp = lightcraft_pipeline::optics::Warp::identity(w, h);
+    warp.lens = Some(correction);
+    warp.lens_dist = 1.0;
+    let source = |x, y| warp.corrected_to_source(lightcraft_geom::Point::new(x, y), 1);
+    let candidate = resample_correspondence(sensor, source);
+    let framing = estimate_framing_against(&candidate, reference, sensor, baseline)?;
+    let mask = resample_correspondence(clipped, source);
+    let frame = |x: f64, y: f64| {
+        let (x, y) = framing.source(x as f32, y as f32, sensor.width, sensor.height);
+        lightcraft_geom::Point::new(f64::from(x), f64::from(y))
+    };
+    Some((resample_correspondence(&candidate, frame), resample_correspondence(&mask, frame)))
+}
+
 /// Colour training pairs of one raw for a camera profile: white-balanced camera RGB (with the
 /// baseline exposure) → its camera JPEG (linear Rec.2020). `None` for formats with their own
-/// colour matrices, other files or unusable previews.
-pub(crate) fn profile_pairs(raw: &RawImage, bytes: &[u8], lens: Option<&LensGeometry>) -> Option<Vec<([f64; 3], [f64; 3])>> {
+/// colour matrices, other files or unusable previews. `correction` may supply accepted CR3 geometry.
+pub(crate) fn profile_pairs(
+    raw: &RawImage,
+    bytes: &[u8],
+    lens: Option<&LensGeometry>,
+    correction: Option<&lightcraft_develop::EmbeddedLens>,
+) -> Option<Vec<([f64; 3], [f64; 3])>> {
     if !file_local_look(raw.format) || lightcraft_raw::color::has_matrix(&raw.color) {
         return None;
     }
     let transform = lightcraft_raw::color::camera_transform(raw, lightcraft_raw::color::as_shot_white_xy(raw));
-    let (sensor, reference, clipped) = proxies(raw, bytes, &transform, PROFILE_PROXY, 0.0, lens)?;
+    let (sensor, reference, clipped) = proxies(raw, bytes, &transform, PROFILE_PROXY, 0.0, lens, correction)?;
     let to_camera = transform.matrix.inverse()?;
     // clipped sensor pixels don't record the scene's colour (see `fit_look`)
     let sensor = without_clipped(&sensor, &clipped).unwrap_or(sensor);
diff --git a/crates/engine/src/camera_preview/tests.rs b/crates/engine/src/camera_preview/tests.rs
--- a/crates/engine/src/camera_preview/tests.rs
+++ b/crates/engine/src/camera_preview/tests.rs
@@ -661,7 +661,7 @@ fn corpus_lens_corrected_jpegs_pair_with_the_warped_sensor() {
         raw.opcodes.list3.retain(|op| !op.is_lens_correction());
         let t = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
         let (Some((plain, reference, _)), Some((chosen, chosen_reference, _))) =
-            (proxies(&raw, &bytes, &t, PROXY, 0.0, None), proxies(&raw, &bytes, &t, PROXY, 0.0, Some(&lens)))
+            (proxies(&raw, &bytes, &t, PROXY, 0.0, None, None), proxies(&raw, &bytes, &t, PROXY, 0.0, Some(&lens), None))
         else {
             continue;
         };
@@ -928,7 +928,7 @@ fn corpus_cr3_gets_a_framing_aligned_camera_look() {
     assert_eq!(raw.format, RawFormat::Cr3);
     assert_eq!(raw.info().developed_size(), (6000, 4000));
     let transform = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
-    let look = fit_preview(&raw, &bytes, &transform, None).0;
+    let look = fit_preview(&raw, &bytes, &transform, None, None).0;
     eprintln!("R100 guarded colour fit accepted: {}", look.is_some());
     assert!(look.is_some(), "this R100 corpus photo has enough matching colour after camera-framing alignment");
     let (_, info) = crate::files::load_bytes(&bytes, 400).unwrap();
@@ -965,7 +965,7 @@ fn corpus_uses_the_bundled_profile(file: &str, model: &str) {
     let lens = lens_geometry(&raw.opcodes.list3);
     raw.opcodes.list3.retain(|op| !op.is_lens_correction());
     let transform = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
-    let look = fit_preview(&raw, &bytes, &transform, lens.as_ref()).0.expect("fit accepted");
+    let look = fit_preview(&raw, &bytes, &transform, lens.as_ref(), None).0.expect("fit accepted");
     let expected = bundled.matrix().mul(&transform.matrix.inverse().unwrap());
     for (row, want) in look.matrix.0.iter().zip(expected.0) {
         for (got, want) in row.iter().zip(want) {
@@ -1346,9 +1346,9 @@ fn corpus_arw_with_dro_is_lowered_to_the_dro_off_tone() {
         let mut raw = lightcraft_raw::decode(&bytes).unwrap();
         raw.opcodes.list3.retain(|op| !op.is_lens_correction());
         let transform = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
-        let look = fit_preview(&raw, &bytes, &transform, None).0.expect("a camera look");
+        let look = fit_preview(&raw, &bytes, &transform, None, None).0.expect("a camera look");
         // the look fitted to the JPEG, as before this change
-        let (sensor, reference, clipped) = proxies(&raw, &bytes, &transform, PROXY, 0.0, None).unwrap();
+        let (sensor, reference, clipped) = proxies(&raw, &bytes, &transform, PROXY, 0.0, None, None).unwrap();
         let profile = raw.metadata.model.as_deref().and_then(crate::camera_profiles::get);
         let colour = profile.as_ref().and_then(|p| Some((p.matrix().mul(&transform.matrix.inverse()?), p.hue_sat.clone())));
         let fitted = fit_look(&sensor, &reference, &clipped, colour).unwrap();
@@ -2033,12 +2033,12 @@ fn monochrome_look_reads_the_files_black_and_white_jpeg() {
         tiff
     };
     let t = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
-    let look = monochrome_look(&raw, &file([1.0; 3]), &t, None).expect("a tone curve from the black-and-white JPEG");
+    let look = monochrome_look(&raw, &file([1.0; 3]), &t, None, None).expect("a tone curve from the black-and-white JPEG");
     assert!(look.matrix == Mat3::IDENTITY && look.hue_sat.is_none(), "on the transform's colour");
     let curve = ToneMap::camera(&look.tone, 0.0, 0.0, 0.0);
     let mid = curve.apply(0.25);
     assert!((mid - (1.0 - (-0.75f32).exp())).abs() < 0.05, "the camera's curve at 0.25: {mid}");
-    assert!(monochrome_look(&raw, &file([1.0, 0.9, 0.8]), &t, None).is_none(), "a tinted JPEG is not black and white");
+    assert!(monochrome_look(&raw, &file([1.0, 0.9, 0.8]), &t, None, None).is_none(), "a tinted JPEG is not black and white");
 }
 
 /// The black-and-white fit has the partial look's checks: a JPEG of another picture, or one the default curve
@@ -2427,3 +2427,519 @@ fn a_narrow_black_and_white_curve_that_darkens_the_bright_areas_is_refused() {
         assert!(sky > 0.8, "an accepted curve must not darken what the camera renders at 0.93: {sky}");
     }
 }
+
+/// Generate a deterministic textured sensor image for profile-warp correspondence tests.
+fn profile_geometry_scene(w: usize, h: usize) -> Rgb32f {
+    let mut sensor = Rgb32f::new(w, h);
+    for (i, p) in sensor.data.iter_mut().enumerate() {
+        let (x, y) = ((i % w) as f32 * 192.0 / w as f32, (i / w) as f32 * 128.0 / h as f32);
+        let v = 0.35 + 0.13 * (x * 0.15 + y * 0.22).sin() + 0.1 * (x * 0.33 - y * 0.14).sin() + 0.07 * (x * 0.09 - y * 0.31).cos();
+        *p = [v * (0.9 + 0.15 * (y * 0.023).sin()), v, v * (0.9 + 0.15 * (x * 0.019).cos())];
+    }
+    sensor
+}
+
+/// Return a deliberately strong radial warp for synthetic correspondence tests.
+fn synthetic_profile_lens() -> lightcraft_develop::EmbeddedLens {
+    lightcraft_develop::EmbeddedLens {
+        warp: Some(lightcraft_develop::EmbeddedWarp {
+            planes: [[1.04, -0.24, 0.08, 0.0, 0.0, 0.0]; 3],
+            center: lightcraft_geom::Point::new(0.48, 0.53),
+            radius: 0.61,
+        }),
+        ..Default::default()
+    }
+}
+
+/// Render the known lens and frame mapping used as ground truth by profile geometry tests.
+fn profile_geometry_target(sensor: &Rgb32f, lens: &lightcraft_develop::EmbeddedLens, framing: Cr3Framing) -> Rgb32f {
+    let mut warp = lightcraft_pipeline::optics::Warp::identity(sensor.width as f64, sensor.height as f64);
+    warp.lens = Some(*lens);
+    warp.lens_dist = 1.0;
+    let warped = resample_correspondence(sensor, |x, y| warp.corrected_to_source(lightcraft_geom::Point::new(x, y), 1));
+    resample_correspondence(&warped, |x, y| {
+        let (x, y) = framing.source(x as f32, y as f32, sensor.width, sensor.height);
+        lightcraft_geom::Point::new(f64::from(x), f64::from(y))
+    })
+}
+
+/// Measure RMS colour error over finite, mutually valid proxy samples.
+fn geometry_rms(a: &Rgb32f, b: &Rgb32f) -> f64 {
+    let mut error = 0.0;
+    let mut n = 0;
+    for (a, b) in a.data.iter().zip(&b.data) {
+        for (&a, &b) in a.iter().zip(b) {
+            if a.is_finite() && b.is_finite() {
+                error += f64::from(a - b).powi(2);
+                n += 1;
+            }
+        }
+    }
+    assert!(n > 256);
+    (error / f64::from(n)).sqrt()
+}
+
+/// Accept matching lens and framing geometry even when the JPEG has a nonlinear tone curve.
+#[test]
+fn external_profile_geometry_aligns_lens_and_framing_before_colour() {
+    let sensor = profile_geometry_scene(384, 256);
+    let lens = synthetic_profile_lens();
+    let expected = profile_geometry_target(&sensor, &lens, Cr3Framing { scale: 0.95, offset: [0.006, -0.004] });
+    let mut jpeg = expected.clone();
+    jpeg.map_in_place(|p| p.map(|v| v.powf(0.7)));
+    let mask = Rgb32f::new(sensor.width, sensor.height);
+    let (aligned, _) = align_profile_geometry(&sensor, &mask, &jpeg, &lens, lightcraft_geom::Orientation::Normal)
+        .expect("lens and bounded framing pass independent geometry gates");
+    assert!(geometry_rms(&aligned, &expected) < 0.008, "the training sensor is correctly registered despite a nonlinear camera tone");
+    assert!(geometry_rms(&sensor, &expected) > 0.05, "synthetic mismatch is material");
+}
+
+/// Allow lens-only alignment while refusing unrelated, already aligned, or absent geometry.
+#[test]
+fn external_profile_geometry_accepts_lens_only_and_rejects_unrelated_or_already_aligned() {
+    let sensor = profile_geometry_scene(384, 256);
+    let lens = synthetic_profile_lens();
+    let expected = profile_geometry_target(&sensor, &lens, Cr3Framing::IDENTITY);
+    let mask = Rgb32f::new(sensor.width, sensor.height);
+    let (aligned, _) = align_profile_geometry(&sensor, &mask, &expected, &lens, lightcraft_geom::Orientation::Normal)
+        .expect("lens alone aligns; no improvement over its own framing is required");
+    assert!(geometry_rms(&aligned, &expected) < 0.001);
+    assert!(
+        align_profile_geometry(&sensor, &mask, &sensor, &lens, lightcraft_geom::Orientation::Normal).is_none(),
+        "already aligned original is preserved"
+    );
+    let mut unrelated = sensor.clone();
+    for (i, p) in unrelated.data.iter_mut().enumerate() {
+        let (x, y) = ((i % sensor.width) as f32, (i / sensor.width) as f32);
+        *p = [0.35 + 0.13 * (x * 0.41 - y * 0.07).sin() + 0.1 * (x * 0.04 + y * 0.39).cos(); 3];
+    }
+    assert!(align_profile_geometry(&sensor, &mask, &unrelated, &lens, lightcraft_geom::Orientation::Normal).is_none());
+    unrelated.data.fill([0.2; 3]);
+    assert!(align_profile_geometry(&sensor, &mask, &unrelated, &lens, lightcraft_geom::Orientation::Normal).is_none());
+    assert!(
+        align_profile_geometry(&sensor, &mask, &expected, &Default::default(), lightcraft_geom::Orientation::Normal).is_none(),
+        "no geometric profile leaves the established path alone"
+    );
+}
+
+/// Preserve the developed crop and recover correspondences at both proxy sizes and all EXIF orientations.
+#[test]
+fn external_profile_geometry_preserves_oriented_default_crop_and_resolution() {
+    let lens = synthetic_profile_lens();
+    for (w, h) in [(192, 128), (384, 256)] {
+        // This is the developed default crop: no active-area offset is applied a second time.
+        let sensor = profile_geometry_scene(w, h);
+        let target = profile_geometry_target(&sensor, &lens, Cr3Framing::IDENTITY);
+        let mask = Rgb32f::new(w, h);
+        for exif in 1..=8 {
+            let orientation = lightcraft_geom::Orientation::from_exif(exif);
+            let oriented = lightcraft_pipeline::optics::reorient_lens(&lens, orientation, w as f64, h as f64);
+            let (aligned, _) =
+                align_profile_geometry(&sensor, &mask, &target, &oriented, orientation).expect("oriented resolved lens maps back to the sensor crop");
+            assert!(geometry_rms(&aligned, &target) < 0.001, "orientation {exif}, {w}x{h}");
+        }
+    }
+}
+
+/// Forward accepted CR3 geometry through file-local fits, pooled pairs and DNG colour tags,
+/// while preserving the same outputs for other RAW formats.
+#[test]
+fn cr3_profile_geometry_reaches_file_and_pooled_pairs_in_the_default_crop() {
+    use lightcraft_raw::{BlackLevel, RawData, Rect};
+    let (w, h) = (416, 288);
+    let scene = profile_geometry_scene(w, h);
+    let mut raw = raw_of(RawFormat::Cr3, "Synthetic", "Uncalibrated", Default::default());
+    raw.width = w;
+    raw.height = h;
+    raw.cpp = 3;
+    raw.data = RawData::F32(scene.data.iter().flatten().copied().collect());
+    raw.black = BlackLevel::uniform(0.0);
+    raw.white = vec![1.0];
+    raw.active_area = Rect::new(0, 0, w, h);
+    raw.crop = Rect::new(16, 16, 384, 256);
+    raw.linearized = true;
+    raw.wb_multipliers = Some([1.0; 3]);
+    let transform = lightcraft_raw::color::camera_transform(&raw, D65);
+    assert!(transform.matrix_is_fallback);
+    let mut sensor = sensor_proxy(&raw, 2, 384).unwrap();
+    assert_eq!((sensor.width, sensor.height), (384, 256));
+    sensor.map_in_place(|p| transform.matrix.apply_f32(p));
+    let lens = synthetic_profile_lens();
+    let mut target = profile_geometry_target(&sensor, &lens, Cr3Framing { scale: 0.95, offset: [0.006, -0.004] });
+    target.map_in_place(|p| p.map(|v| v.powf(0.7)));
+    let to_srgb = lightcraft_color::SRGB.from_xyz().mul(&REC2020.to_xyz());
+    let srgb = |v: f32| if v <= 0.003_130_8 { 12.92 * v } else { 1.055 * v.powf(1.0 / 2.4) - 0.055 };
+    let pixels: Vec<u8> = target.data.iter().flat_map(|p| to_srgb.apply_f32(*p).map(|v| (srgb(v).clamp(0.0, 1.0) * 255.0).round() as u8)).collect();
+    let image = lightcraft_codecs::EncodeImage::new(384, 256, 3, lightcraft_codecs::Samples::U8(&pixels));
+    let jpeg = lightcraft_codecs::encode_jpeg(&image, 98, lightcraft_codecs::ChromaSubsampling::S444, &Default::default()).unwrap();
+    let mut file = b"II*\0\x08\0\0\0\x02\0".to_vec();
+    for (tag, value) in [(0x0201u16, 38u32), (0x0202, jpeg.len() as u32)] {
+        file.extend(tag.to_le_bytes());
+        file.extend(4u16.to_le_bytes());
+        file.extend(1u32.to_le_bytes());
+        file.extend(value.to_le_bytes());
+    }
+    file.extend(0u32.to_le_bytes());
+    file.extend(jpeg);
+    // This follow-up is CR3-only: even a coherent lens proposal cannot change
+    // correspondences for other formats or leak into their pooled profile pairs.
+    for format in [RawFormat::Cr2, RawFormat::Arw, RawFormat::Nef, RawFormat::Raf, RawFormat::Rw2, RawFormat::Dng] {
+        let mut other = raw.clone();
+        other.format = format;
+        let plain = proxies(&other, &file, &transform, PROXY, 0.0, None, None).unwrap();
+        let proposed = proxies(&other, &file, &transform, PROXY, 0.0, None, Some(&lens)).unwrap();
+        let bits = |image: &Rgb32f| image.data.iter().flatten().map(|v| v.to_bits()).collect::<Vec<_>>();
+        assert_eq!(bits(&plain.0), bits(&proposed.0), "{format:?}: sensor stays identical");
+        assert_eq!(bits(&plain.1), bits(&proposed.1), "{format:?}: JPEG stays identical");
+        assert_eq!(plain.2, proposed.2, "{format:?}: clipping stays identical");
+        assert_eq!(
+            profile_pairs(&other, &file, None, None),
+            profile_pairs(&other, &file, None, Some(&lens)),
+            "{format:?}: pooled pairs stay identical"
+        );
+        let mut plain_tags = other.clone();
+        let mut proposed_tags = other;
+        assert_eq!(
+            super::colour::starting_colour_tags_with_correction(&mut plain_tags, &file, None),
+            super::colour::starting_colour_tags_with_correction(&mut proposed_tags, &file, Some(&lens)),
+            "{format:?}: merge/DNG colour tags stay identical"
+        );
+    }
+    let (plain, reference, _) = proxies(&raw, &file, &transform, PROXY, 0.0, None, None).unwrap();
+    let (mut aligned, _, _) = proxies(&raw, &file, &transform, PROXY, 0.0, None, Some(&lens)).unwrap();
+    let mut plain_render = plain;
+    plain_render.map_in_place(|p| p.map(|v| v.powf(0.7)));
+    aligned.map_in_place(|p| p.map(|v| v.powf(0.7)));
+    assert!(
+        geometry_rms(&aligned, &reference) < geometry_rms(&plain_render, &reference) * 0.3,
+        "the file-local route uses the resolved crop-relative correction"
+    );
+    let error = |pairs: Vec<([f64; 3], [f64; 3])>| {
+        let sum: f64 = pairs.iter().map(|(x, y)| transform.matrix.apply(*x).iter().zip(y).map(|(x, y)| (x.powf(0.7) - y).powi(2)).sum::<f64>()).sum();
+        sum / pairs.len() as f64
+    };
+    let before = error(profile_pairs(&raw, &file, None, None).unwrap());
+    let after = error(profile_pairs(&raw, &file, None, Some(&lens)).unwrap());
+    assert!(after < before * 0.1, "pooled colour calibration uses the same geometry: {before} -> {after}");
+    // Merge / DNG conversion must encode the same calibrated colour as opening the RAW.
+    let mut opened = raw.clone();
+    let (_, t, look) = starting_colour(&mut opened, &file, None, Some(&lens));
+    assert!(look.is_some(), "the synthetic corrected preview supplies a camera look");
+    let scale = crate::files::WbScaleOf::new(raw.format, &raw.color, raw.wb_multipliers, &raw.metadata);
+    let white = scale.dng_white(&raw.color, lightcraft_raw::color::as_shot_white_xy(&raw));
+    let expected_tags = dng_colour(&raw.color, &white, &t, look.as_ref()).unwrap();
+    let tags = super::colour::starting_colour_tags_with_correction(&mut raw, &file, Some(&lens)).unwrap();
+    assert_eq!(tags, expected_tags, "DNG tags use the resolved correspondence geometry too");
+}
+
+/// Keep clipped contributors in the resampled mask and mark out-of-frame samples invalid.
+#[test]
+fn correspondence_mask_keeps_every_contributing_clipped_sample_and_invalid_border() {
+    let mut mask = Rgb32f::new(8, 6);
+    mask.data[2 * 8 + 3] = [1.0; 3];
+    let result = resample_correspondence(&mask, |x, y| lightcraft_geom::Point::new(x + 0.75, y + 0.5));
+    assert!(result.data[8 + 2][0] > 0.0 && result.data[8 + 3][0] > 0.0);
+    assert!(result.data[2 * 8 + 2][0] > 0.0 && result.data[2 * 8 + 3][0] > 0.0);
+    assert_eq!(result.data[0][0], 0.0);
+    assert!(result.data[7][0].is_nan() && result.data[5 * 8][0].is_nan());
+    // The final box reduction retains fractional positive masks as clipped too.
+    let reduced = resize(&result, 4, 3, Filter::Box);
+    assert!(reduced.data.iter().any(|p| p[0] > 0.0));
+}
+
+/// Apply oriented CR3 geometry once, with white balance applied once, across all EXIF orientations.
+#[test]
+fn cr3_profile_geometry_shares_orientation_and_applies_white_balance_once() {
+    use lightcraft_raw::{BlackLevel, RawData, Rect};
+    let (w, h) = (416, 288);
+    let scene = profile_geometry_scene(w, h);
+    let mut raw = raw_of(RawFormat::Cr3, "Synthetic", "Uncalibrated", Default::default());
+    raw.width = w;
+    raw.height = h;
+    raw.cpp = 3;
+    raw.data = RawData::F32(scene.data.iter().flatten().copied().collect());
+    raw.black = BlackLevel::uniform(0.0);
+    raw.white = vec![1.0];
+    raw.active_area = Rect::new(0, 0, w, h);
+    raw.crop = Rect::new(16, 16, 384, 256);
+    raw.linearized = true;
+    raw.wb_multipliers = Some([1.25, 1.0, 0.8]);
+    let transform = lightcraft_raw::color::camera_transform(&raw, D65);
+    assert!(transform.matrix_is_fallback);
+    let mut sensor = sensor_proxy(&raw, 2, 384).unwrap();
+    assert_eq!((sensor.width, sensor.height), (384, 256));
+    sensor.map_in_place(|p| {
+        transform.matrix.apply_f32(std::array::from_fn(|i| p[i] * transform.wb[i] * 2f32.powf(transform.baseline_exposure as f32)))
+    });
+    let lens = synthetic_profile_lens();
+    let mut target = profile_geometry_target(&sensor, &lens, Cr3Framing { scale: 0.95, offset: [0.006, -0.004] });
+    target.map_in_place(|p| p.map(|v| v.powf(0.7)));
+    let to_srgb = lightcraft_color::SRGB.from_xyz().mul(&REC2020.to_xyz());
+    let srgb = |v: f32| if v <= 0.003_130_8 { 12.92 * v } else { 1.055 * v.powf(1.0 / 2.4) - 0.055 };
+    let pixels: Vec<u8> = target.data.iter().flat_map(|p| to_srgb.apply_f32(*p).map(|v| (srgb(v).clamp(0.0, 1.0) * 255.0).round() as u8)).collect();
+    let image = lightcraft_codecs::EncodeImage::new(384, 256, 3, lightcraft_codecs::Samples::U8(&pixels));
+    let jpeg = lightcraft_codecs::encode_jpeg(&image, 98, lightcraft_codecs::ChromaSubsampling::S444, &Default::default()).unwrap();
+    let mut file = b"II*\0\x08\0\0\0\x02\0".to_vec();
+    for (tag, value) in [(0x0201u16, 38u32), (0x0202, jpeg.len() as u32)] {
+        file.extend(tag.to_le_bytes());
+        file.extend(4u16.to_le_bytes());
+        file.extend(1u32.to_le_bytes());
+        file.extend(value.to_le_bytes());
+    }
+    file.extend(0u32.to_le_bytes());
+    file.extend(jpeg);
+    for exif in 1..=8 {
+        let orientation = lightcraft_geom::Orientation::from_exif(exif);
+        raw.orientation = orientation;
+        let oriented_lens = lightcraft_pipeline::optics::reorient_lens(&lens, orientation, 384.0, 256.0);
+        let (aligned, reference, _) = proxies(&raw, &file, &transform, PROFILE_PROXY, 0.0, None, Some(&oriented_lens)).unwrap();
+        let mut displayed = aligned.clone();
+        displayed.map_in_place(|p| p.map(|v| v.powf(0.7)));
+        assert!(geometry_rms(&displayed, &reference) < 0.009, "orientation {exif}: WB exactly once and matching frames");
+        let oriented_target = target.clone().into_oriented(orientation);
+        let expected = reference_at(&oriented_target, &aligned);
+        assert!(geometry_rms(&displayed, &expected) < 0.009, "orientation {exif}: absolute oriented ground truth");
+        // Missing geometry must take the same established CR3 framing path as a rejected profile.
+        let (plain, plain_ref, plain_clip) = proxies(&raw, &file, &transform, PROXY, 0.0, None, None).unwrap();
+        let (fallback, fallback_ref, fallback_clip) = proxies(&raw, &file, &transform, PROXY, 0.0, None, Some(&Default::default())).unwrap();
+        assert_eq!(plain.data, fallback.data);
+        assert_eq!(plain_ref.data, fallback_ref.data);
+        assert_eq!(plain_clip, fallback_clip);
+    }
+}
+
+/// Sensor, JPEG and clipping samples returned by the diagnostic proxy calls.
+type DiagnosticProxy = (Rgb32f, Rgb32f, Vec<bool>);
+
+/// Preserve missing-proxy cases in the diagnostic TSV without inventing geometry or fit results.
+/// `proxies` exposes no rejection reason, so the row records only which call returned `None`.
+fn diagnostic_proxies_or_report(
+    name: &str,
+    raw: &RawImage,
+    profile_warp: bool,
+    plain: Option<DiagnosticProxy>,
+    chosen: Option<DiagnosticProxy>,
+    report: &mut String,
+) -> Option<(DiagnosticProxy, DiagnosticProxy)> {
+    let status = match (plain, chosen) {
+        (Some(plain), Some(chosen)) => return Some((plain, chosen)),
+        (None, Some(_)) => "missing_plain_proxy",
+        (Some(_), None) => "missing_chosen_proxy",
+        (None, None) => "missing_both_proxies",
+    };
+    let mut fields = vec![String::new(); 17];
+    fields[0] = name.to_owned();
+    fields[1] = raw.metadata.model.clone().unwrap_or_default();
+    fields[2] = format!("{:?}", raw.orientation);
+    fields[3] = profile_warp.to_string();
+    fields[15] = status.to_owned();
+    fields[16] = "proxies returned None; underlying cause unavailable".to_owned();
+    report.push_str(&fields.join("\t"));
+    report.push('\n');
+    None
+}
+
+/// Build a small RGB CR3-shaped raw and embedded JPEG without opening any real photo.
+fn diagnostic_proxy_fixture() -> (RawImage, Vec<u8>) {
+    let (w, h) = (96, 64);
+    let scene = profile_geometry_scene(w, h);
+    let mut raw = raw_of(RawFormat::Cr3, "Synthetic", "Uncalibrated", Default::default());
+    raw.width = w;
+    raw.height = h;
+    raw.cpp = 3;
+    raw.data = lightcraft_raw::RawData::F32(scene.data.iter().flatten().copied().collect());
+    raw.black = lightcraft_raw::BlackLevel::uniform(0.0);
+    raw.white = vec![1.0];
+    raw.active_area = lightcraft_raw::Rect::new(0, 0, w, h);
+    raw.crop = raw.active_area;
+    raw.linearized = true;
+    raw.wb_multipliers = Some([1.0; 3]);
+    let pixels: Vec<u8> = scene.data.iter().flatten().map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8).collect();
+    let image = lightcraft_codecs::EncodeImage::new(w as u32, h as u32, 3, lightcraft_codecs::Samples::U8(&pixels));
+    let jpeg = lightcraft_codecs::encode_jpeg(&image, 98, lightcraft_codecs::ChromaSubsampling::S444, &Default::default()).unwrap();
+    let mut file = b"II*\0\x08\0\0\0\x02\0".to_vec();
+    for (tag, value) in [(0x0201u16, 38u32), (0x0202, jpeg.len() as u32)] {
+        file.extend(tag.to_le_bytes());
+        file.extend(4u16.to_le_bytes());
+        file.extend(1u32.to_le_bytes());
+        file.extend(value.to_le_bytes());
+    }
+    file.extend(0u32.to_le_bytes());
+    file.extend(jpeg);
+    (raw, file)
+}
+
+/// Exercise the corpus diagnostic's actual proxy-result handling across missing and available inputs.
+#[test]
+fn cr3_diagnostics_report_missing_proxies_and_continue() {
+    let (mut raw, bytes) = diagnostic_proxy_fixture();
+    let transform = lightcraft_raw::color::camera_transform(&raw, D65);
+    let available = proxies(&raw, &bytes, &transform, PROXY, 0.0, None, None).unwrap();
+    // A valid spectral transform does not guarantee a usable sensor proxy. This synthetic
+    // single-channel raw lacks a CFA, so the established proxy path deliberately returns None.
+    let camera = lightcraft_raw::spectral::find(Some("Canon"), "Canon EOS R8").unwrap();
+    raw.color.color_matrix = camera.color_matrix.map(Some);
+    raw.color.forward_matrix = camera.forward_matrix.map(Some);
+    raw.color.illuminant = [17, 21];
+    raw.cpp = 1;
+    raw.data = lightcraft_raw::RawData::U16(vec![0; raw.width * raw.height]);
+    let calibrated = lightcraft_raw::color::camera_transform(&raw, D65);
+    assert!(!calibrated.matrix_is_fallback);
+    let missing = proxies(&raw, &bytes, &calibrated, PROXY, 0.0, None, None);
+    assert!(missing.is_none());
+    let mut report = String::new();
+    for (plain, chosen, status) in [
+        (missing.clone(), missing.clone(), "missing_both_proxies"),
+        (missing.clone(), Some(available.clone()), "missing_plain_proxy"),
+        (Some(available.clone()), missing, "missing_chosen_proxy"),
+    ] {
+        assert!(diagnostic_proxies_or_report(status, &raw, true, plain, chosen, &mut report).is_none());
+    }
+    let (plain, chosen) = diagnostic_proxies_or_report("available", &raw, false, Some(available.clone()), Some(available.clone()), &mut report)
+        .expect("a later usable file must still reach measurement and image export");
+    let bits = |image: &Rgb32f| image.data.iter().flatten().map(|v| v.to_bits()).collect::<Vec<_>>();
+    assert_eq!(bits(&plain.0), bits(&available.0));
+    assert_eq!(chosen.1.data, available.1.data);
+    let rows: Vec<_> = report.lines().collect();
+    assert_eq!(rows.len(), 3, "available proxies must not create an unavailable row");
+    for row in rows {
+        let fields: Vec<_> = row.split('\t').collect();
+        assert_eq!(fields.len(), 17);
+        assert_eq!(fields[0], fields[15], "all three missing cases retain distinct statuses");
+        assert!(fields[4..15].iter().all(|v| v.is_empty()), "unavailable metrics, gates and fits stay empty");
+        assert_eq!(fields[16], "proxies returned None; underlying cause unavailable");
+    }
+}
+
+/// Opt-in diagnostic only: never opens the app or changes a catalogue. It exports the actual
+/// selected correspondences, and reports rejection as rejection rather than decoder success.
+#[test]
+#[ignore = "explicit local corpus diagnostic; requires LIGHTCRAFT_CR3_DIAGNOSTICS and LIGHTCRAFT_CORPUS"]
+fn corpus_cr3_profile_geometry_diagnostics() {
+    let output = std::env::var_os("LIGHTCRAFT_CR3_DIAGNOSTICS").expect("explicit diagnostics require an output directory");
+    let corpus = std::path::PathBuf::from(std::env::var_os("LIGHTCRAFT_CORPUS").expect("diagnostics require a corpus"));
+    let output = std::path::PathBuf::from(output);
+    std::fs::create_dir_all(&output).unwrap();
+    let mut report = String::from(
+        "file\tmodel\torientation\tprofile_warp\tgeometry_accepted\tplain_alignment\tchosen_alignment\tplain_structure\tchosen_structure\tplain_pairs\tchosen_pairs\tplain_look\tchosen_look\tframed_gate_scores\toriginal_gate_scores\tstatus\treason\n",
+    );
+    let mut paths: Vec<_> = std::fs::read_dir(corpus.join("raw"))
+        .unwrap()
+        .flatten()
+        .map(|e| e.path())
+        .filter(|p| p.extension().is_some_and(|x| x.eq_ignore_ascii_case("cr3")))
+        .collect();
+    paths.sort();
+    assert!(!paths.is_empty(), "diagnostic corpus must contain CR3 files");
+    for path in paths {
+        let bytes = std::fs::read(&path).unwrap();
+        let mut raw = lightcraft_raw::decode(&bytes).unwrap();
+        let correction = crate::lens_profiles::for_raw(&raw.info(), || lightcraft_meta::extract(&bytes));
+        let lens = lens_geometry(&raw.opcodes.list3);
+        raw.opcodes.list3.retain(|op| !op.is_lens_correction());
+        let transform = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
+        let plain = proxies(&raw, &bytes, &transform, PROXY, 0.0, lens.as_ref(), None);
+        let chosen = proxies(&raw, &bytes, &transform, PROXY, 0.0, lens.as_ref(), correction.as_ref());
+        let name = path.file_name().unwrap().to_string_lossy();
+        let Some((plain, chosen)) =
+            diagnostic_proxies_or_report(&name, &raw, correction.as_ref().is_some_and(|l| l.warp.is_some()), plain, chosen, &mut report)
+        else {
+            std::fs::write(output.join("geometry.tsv"), &report).unwrap();
+            continue;
+        };
+        let framed_scores = super::proxy::geometry_framed_scores();
+        let original_scores = super::proxy::geometry_gate_scores();
+        let accepted = plain.0.data.iter().flatten().zip(chosen.0.data.iter().flatten()).any(|(a, b)| a.to_bits() != b.to_bits());
+        let pairs = |v: &(Rgb32f, Rgb32f, Vec<bool>)| {
+            let sensor = without_clipped(&v.0, &v.2).unwrap_or_else(|| v.0.clone());
+            collect_pairs(&sensor, &v.1, 0.05, None, false).map_or(0, |v| v.0.len())
+        };
+        let before = fit_preview(&raw, &bytes, &transform, lens.as_ref(), None).0.is_some();
+        let after = fit_preview(&raw, &bytes, &transform, lens.as_ref(), correction.as_ref()).0.is_some();
+        report.push_str(&format!(
+            "{name}\t{}\t{:?}\t{}\t{accepted}\t{:.6}\t{:.6}\t{}\t{}\t{}\t{}\t{before}\t{after}\t{framed_scores:?}\t{original_scores:?}\tmeasured\t\n",
+            raw.metadata.model.as_deref().unwrap_or(""),
+            raw.orientation,
+            correction.as_ref().is_some_and(|l| l.warp.is_some()),
+            alignment(&plain.0, &plain.1),
+            alignment(&chosen.0, &chosen.1),
+            structure_agrees(&plain.0, &plain.1),
+            structure_agrees(&chosen.0, &chosen.1),
+            pairs(&plain),
+            pairs(&chosen)
+        ));
+        for (label, image) in [("plain", &plain.0), ("chosen", &chosen.0), ("jpeg", &chosen.1)] {
+            let to_srgb = lightcraft_color::SRGB.from_xyz().mul(&REC2020.to_xyz());
+            let pixels: Vec<u8> = image
+                .data
+                .iter()
+                .flat_map(|p| {
+                    to_srgb.apply_f32(*p).map(|v| {
+                        let v = if v.is_finite() { v.max(0.0) } else { 0.0 };
+                        let v = if v <= 0.0031308 { 12.92 * v } else { 1.055 * v.powf(1.0 / 2.4) - 0.055 };
+                        (v.clamp(0.0, 1.0) * 255.0).round() as u8
+                    })
+                })
+                .collect();
+            let image = lightcraft_codecs::EncodeImage::new(image.width as u32, image.height as u32, 3, lightcraft_codecs::Samples::U8(&pixels));
+            std::fs::write(output.join(format!("{name}-{label}.png")), lightcraft_codecs::encode_png(&image, &Default::default()).unwrap()).unwrap();
+        }
+        std::fs::write(output.join("geometry.tsv"), &report).unwrap();
+    }
+}
+
+/// Keep the accepted CR3 framing fallback when a proposed profile warp fails its paired comparison.
+#[test]
+fn cr3_rejected_profile_preserves_accepted_framing_fallback() {
+    use lightcraft_raw::{BlackLevel, RawData, Rect};
+    let (w, h) = (416, 288);
+    let scene = profile_geometry_scene(w, h);
+    let mut raw = raw_of(RawFormat::Cr3, "Synthetic", "Uncalibrated", Default::default());
+    raw.width = w;
+    raw.height = h;
+    raw.cpp = 3;
+    raw.data = RawData::F32(scene.data.iter().flatten().copied().collect());
+    raw.black = BlackLevel::uniform(0.0);
+    raw.white = vec![1.0];
+    raw.active_area = Rect::new(0, 0, w, h);
+    raw.crop = Rect::new(16, 16, 384, 256);
+    raw.linearized = true;
+    raw.wb_multipliers = Some([1.25, 1.0, 0.8]);
+    let transform = lightcraft_raw::color::camera_transform(&raw, D65);
+    assert!(transform.matrix_is_fallback);
+    let mut sensor = sensor_proxy(&raw, 2, 384).unwrap();
+    assert_eq!((sensor.width, sensor.height), (384, 256));
+    sensor.map_in_place(|p| {
+        transform.matrix.apply_f32(std::array::from_fn(|i| p[i] * transform.wb[i] * 2f32.powf(transform.baseline_exposure as f32)))
+    });
+    let lens = synthetic_profile_lens();
+    let mut target = profile_geometry_target(&sensor, &Default::default(), Cr3Framing { scale: 0.95, offset: [0.006, -0.004] });
+    target.map_in_place(|p| p.map(|v| v.powf(0.7)));
+    let to_srgb = lightcraft_color::SRGB.from_xyz().mul(&REC2020.to_xyz());
+    let srgb = |v: f32| if v <= 0.003_130_8 { 12.92 * v } else { 1.055 * v.powf(1.0 / 2.4) - 0.055 };
+    let pixels: Vec<u8> = target.data.iter().flat_map(|p| to_srgb.apply_f32(*p).map(|v| (srgb(v).clamp(0.0, 1.0) * 255.0).round() as u8)).collect();
+    let image = lightcraft_codecs::EncodeImage::new(384, 256, 3, lightcraft_codecs::Samples::U8(&pixels));
+    let jpeg = lightcraft_codecs::encode_jpeg(&image, 98, lightcraft_codecs::ChromaSubsampling::S444, &Default::default()).unwrap();
+    let mut file = b"II*\0\x08\0\0\0\x02\0".to_vec();
+    for (tag, value) in [(0x0201u16, 38u32), (0x0202, jpeg.len() as u32)] {
+        file.extend(tag.to_le_bytes());
+        file.extend(4u16.to_le_bytes());
+        file.extend(1u32.to_le_bytes());
+        file.extend(value.to_le_bytes());
+    }
+    file.extend(0u32.to_le_bytes());
+    file.extend(jpeg);
+    let original_only = align_profile_geometry(&sensor, &Rgb32f::new(384, 256), &target, &lens, lightcraft_geom::Orientation::Normal);
+    assert!(original_only.is_some(), "this moderate wrong-lens proposal clears original-only gates, so the framed guard is necessary");
+    let (plain, reference, mask) = proxies(&raw, &file, &transform, PROXY, 0.0, None, None).unwrap();
+    let (fallback, fallback_reference, fallback_mask) = proxies(&raw, &file, &transform, PROXY, 0.0, None, Some(&lens)).unwrap();
+    let mut comparison = plain.clone();
+    comparison.map_in_place(|p| p.map(|v| v.powf(0.7)));
+    assert!(geometry_rms(&comparison, &reference) < 0.009, "existing framing fallback still aligns this JPEG");
+    assert!(
+        plain.data.iter().flatten().zip(fallback.data.iter().flatten()).all(|(a, b)| a.to_bits() == b.to_bits()),
+        "wrong lens must be rejected before established framing"
+    );
+    assert_eq!(reference.data, fallback_reference.data);
+    assert_eq!(mask, fallback_mask);
+}
diff --git a/crates/engine/src/camera_profiles.rs b/crates/engine/src/camera_profiles.rs
--- a/crates/engine/src/camera_profiles.rs
+++ b/crates/engine/src/camera_profiles.rs
@@ -198,15 +198,20 @@ const PAIRS_PER_FILE: usize = 4000;
 
 impl Pool {
     /// Add one raw file's colour pairs. `Ok(None)` when the file can't contribute (not an ARW,
-    /// NEF or RAF without colour matrices, no usable camera JPEG, too little colour).
+    /// NEF, RAF, CR2/CR3 or another file-local format without colour matrices, no usable camera JPEG, too little colour).
     pub fn add(&mut self, bytes: &[u8]) -> Result<Option<String>, String> {
         let mut raw = lightcraft_raw::decode(bytes).map_err(|e| e.to_string())?;
         // colour only: geometric lens corrections would stop the sensor proxy from binning (the pairs still follow
         // the warp the camera applied to its JPEG)
+        let correction = if raw.format == lightcraft_raw::RawFormat::Cr3 {
+            crate::lens_profiles::for_raw(&raw.info(), || lightcraft_meta::extract(bytes))
+        } else {
+            None
+        };
         let lens = crate::camera_preview::lens_geometry(&raw.opcodes.list3);
         raw.opcodes.list3.retain(|op| !op.is_lens_correction());
         let Some(model) = raw.metadata.model.as_deref().map(str::trim).filter(|m| !m.is_empty()) else { return Ok(None) };
-        let Some(pairs) = crate::camera_preview::profile_pairs(&raw, bytes, lens.as_ref()) else { return Ok(None) };
+        let Some(pairs) = crate::camera_preview::profile_pairs(&raw, bytes, lens.as_ref(), correction.as_ref()) else { return Ok(None) };
         let step = pairs.len().div_ceil(PAIRS_PER_FILE).max(1);
         let entry = self.models.entry(model.to_owned()).or_default();
         entry.0 += 1;
diff --git a/crates/engine/src/cmd/export.rs b/crates/engine/src/cmd/export.rs
--- a/crates/engine/src/cmd/export.rs
+++ b/crates/engine/src/cmd/export.rs
@@ -38,7 +38,7 @@ impl Session {
         }
         // the preset's depth is judged against the format written, known only now (`path`); a
         // depth the call passes itself is the caller's, and validated as such
-        if !p.get("bitDepth").is_some_and(|d| !d.is_null()) {
+        if p.get("bitDepth").is_none_or(|d| d.is_null()) {
             crate::export::ExportOptions::drop_unfit_depth(&mut out, fallback);
         }
         Ok(out)
diff --git a/crates/engine/src/cmd/face_detect.rs b/crates/engine/src/cmd/face_detect.rs
--- a/crates/engine/src/cmd/face_detect.rs
+++ b/crates/engine/src/cmd/face_detect.rs
@@ -297,6 +297,7 @@ fn detect(s: &mut Session, p: &Value) -> Result<Value> {
     Ok(json!({"detector": LABEL, "applied": applied, "photos": results, "ms": started.elapsed().as_secs_f64() * 1000.0}))
 }
 
+/// Describe the face detection and face-model device query commands.
 pub fn specs() -> Vec<CommandSpec> {
     vec![
         cmd!(
@@ -313,7 +314,7 @@ pub fn specs() -> Vec<CommandSpec> {
             "Face Models on the GPU",
             [],
             None,
-            "{cpuOnly?: bool} → {cpuOnly, detection, recognition}: where the face models run, each as {checking, device, why} (null until it is loaded). The GPU is used only after a background check shows it gives the CPU's results faster; `cpuOnly: true` keeps every face model on the CPU (Settings ▸ Faces), `false` checks the GPU again",
+            "{cpuOnly?: bool} → {cpuOnly, detection, recognition}: where the face models run, each as {checking, device, why} (null until it is loaded). The GPU is used only after a background check shows it gives the CPU's results faster; `cpuOnly: true` keeps every face model on the CPU (Settings → Faces), `false` checks the GPU again",
             always,
             gpu
         ),
diff --git a/crates/engine/src/cmd/face_models.rs b/crates/engine/src/cmd/face_models.rs
--- a/crates/engine/src/cmd/face_models.rs
+++ b/crates/engine/src/cmd/face_models.rs
@@ -501,6 +501,7 @@ fn select(s: &mut Session, p: &Value) -> Result<Value> {
     Ok(json!({"embedder": st.embedder}))
 }
 
+/// Enable or disable face recognition, requiring an installed recognition model when enabling it.
 fn enable(s: &mut Session, p: &Value) -> Result<Value> {
     const C: &str = "faces.enable";
     let dir = models_dir(s, C)?;
@@ -511,7 +512,7 @@ fn enable(s: &mut Session, p: &Value) -> Result<Value> {
     let usable =
         st.embedder.as_deref().is_some_and(|id| installed_models(&dir).iter().any(|i| i.manifest.id == id && i.manifest.role == Role::Embedder));
     if on && !usable {
-        return Err(bad(C, "choose a recognition model first (Settings ▸ Faces ▸ Use)"));
+        return Err(bad(C, "choose a recognition model first (Settings → Faces → Use)"));
     }
     st.enabled = on;
     write_settings(&dir, &st)?;
diff --git a/crates/engine/src/cmd/geocode.rs b/crates/engine/src/cmd/geocode.rs
--- a/crates/engine/src/cmd/geocode.rs
+++ b/crates/engine/src/cmd/geocode.rs
@@ -119,6 +119,8 @@ fn fill(meta: &mut lightcraft_catalog::Meta, place: &lightcraft_geocode::Place,
     }
 }
 
+/// Populate place metadata for targeted photos with valid GPS coordinates using the local database.
+/// Existing fields are preserved unless `overwrite` is requested.
 fn reverse(s: &mut Session, p: &Value) -> Result<Value> {
     const C: &str = "metadata.reverseGeocode";
     let overwrite = bool_or(p, "overwrite", false);
@@ -141,7 +143,7 @@ fn reverse(s: &mut Session, p: &Value) -> Result<Value> {
         return Err(bad(C, "none of these photos has a GPS position"));
     }
     let dir = dataset_dir(p).ok_or_else(|| bad(C, "no settings folder: give `dataset`"))?;
-    let names = s.place_names.load(&dir).map_err(|e| bad(C, format!("{e}: download the place names first (Photo ▸ Download Place Names)")))?;
+    let names = s.place_names.load(&dir).map_err(|e| bad(C, format!("{e}: download the place names first (Photo → Download Place Names)")))?;
     // a large selection shows in the activity stack and can be stopped
     let task = (todo.len() > 1).then(|| s.activity.start("geocode", "Looking up places", Cancel::Yes));
     let mut ops = Vec::new();
diff --git a/crates/engine/src/denoise/model.rs b/crates/engine/src/denoise/model.rs
--- a/crates/engine/src/denoise/model.rs
+++ b/crates/engine/src/denoise/model.rs
@@ -409,7 +409,7 @@ pub fn serving(dir: Option<&Path>, id: Option<&str>, run_on: RunOn) -> Result<Se
     let want = match id {
         Some(i) => i.to_string(),
         None => read_settings(&dir).model.ok_or_else(|| {
-            format!("no denoise model is selected in {} (install one in Settings ▸ AI Denoise, or name it with --model)", dir.display())
+            format!("no denoise model is selected in {} (install one in Settings → AI Denoise, or name it with --model)", dir.display())
         })?,
     };
     let installed = installed_models(&dir)
diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs
--- a/crates/engine/src/lib.rs
+++ b/crates/engine/src/lib.rs
@@ -115,7 +115,7 @@ pub enum EngineError {
     /// The folder (or file) asked for is a Lightroom Classic catalog or part of one, not a
     /// LightCraft library (issue #891): nothing was created or opened. `catalog`: the `.lrcat`
     /// to import with `library.importLightroom`, when one was found.
-    #[error("{} is a Lightroom Classic catalog, not a LightCraft library; import it into a LightCraft library with File ▸ Import Lightroom Catalog…{}", path.display(), catalog.as_ref().map_or(String::new(), |c| format!(" ({})", c.display())))]
+    #[error("{} is a Lightroom Classic catalog, not a LightCraft library; import it into a LightCraft library with File → Import Lightroom Catalog…{}", path.display(), catalog.as_ref().map_or(String::new(), |c| format!(" ({})", c.display())))]
     LightroomCatalog { path: std::path::PathBuf, catalog: Option<std::path::PathBuf> },
     /// The work was cancelled (by the person, an agent or a newer request): not a failure.
     #[error("{0}")]
diff --git a/crates/engine/src/media/source.rs b/crates/engine/src/media/source.rs
--- a/crates/engine/src/media/source.rs
+++ b/crates/engine/src/media/source.rs
@@ -1,7 +1,7 @@
 use super::*;
 
 /// Bump when the pipeline's output changes, to invalidate cached thumbnails.
-pub const RENDER_CACHE_VERSION: u64 = 70;
+pub const RENDER_CACHE_VERSION: u64 = 71;
 
 /// Thumbnails render at one of these long edges (so window/cell size changes reuse the cache).
 pub const THUMB_SIZES: [usize; 4] = [128, 256, 384, 512];
diff --git a/crates/engine/src/segment/worker.rs b/crates/engine/src/segment/worker.rs
--- a/crates/engine/src/segment/worker.rs
+++ b/crates/engine/src/segment/worker.rs
@@ -379,7 +379,7 @@ fn render(job: &mut Job) -> Result<Input, String> {
         Err(e) => {
             return Err(match &job.missing {
                 Some(path) => format!(
-                    "This photo's file is missing (moved or deleted), so it can't be analyzed for AI masks: {path}. Library ▸ Find Missing Photos can relink it."
+                    "This photo's file is missing (moved or deleted), so it can't be analyzed for AI masks: {path}. Library → Find Missing Photos can relink it."
                 ),
                 None => format!("couldn't render the photo for AI masks: {e}"),
             });
diff --git a/crates/ui-egui/locales/cs-formats.json b/crates/ui-egui/locales/cs-formats.json
--- a/crates/ui-egui/locales/cs-formats.json
+++ b/crates/ui-egui/locales/cs-formats.json
@@ -35,7 +35,7 @@
   "Added “{keyword}” to {n} photo{}": "K fotografiím přidáno „{keyword}“: {n}{:.0}",
   "Adding photos… {} of {}": "Přidávání fotografií… {} z {}",
   "All {n} selected photos move by the same amount.": "Všech {n} vybraných fotografií se posune o stejnou hodnotu.",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Již v knihovně, v nedávno odstraněných: vybrané fotografie: {}{:.0} · pravé tlačítko → Obnovit, nebo Trvale odstranit pro nový import",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "Již v knihovně, v nedávno odstraněných: vybrané fotografie: {}{:.0} · pravé tlačítko → Obnovit, nebo Trvale odstranit pro nový import",
   "Already in the library: {} photo{} selected in All Photos": "Již v knihovně: ve všech fotografiích vybráno: {}{:.0}",
   "Auto ({})": "Automaticky ({})",
   "Auto Import: added {} photo{}": "Automatický import: přidáno fotografií: {}{:.0}",
@@ -80,7 +80,7 @@
   "Imported {n} point curve preset{}": "Importovaná přednastavení bodové křivky: {n}{:.0}",
   "Imported {n} preset{}": "Importovaná přednastavení: {n}{:.0}",
   "Imported {n} preset{}, {f} file{} not readable": "Importovaná přednastavení: {n}{0:.0}, nečitelné soubory: {f}{1:.0}",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "Importované profily: {profiles}{:.0} (Prohlížeč profilů → jejich skupiny)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "Importované profily: {profiles}{:.0} (Prohlížeč profilů → jejich skupiny)",
   "Imported {} photo{}": "Importováno fotografií: {}{:.0}",
   "In line for AI Denoise ({ahead} ahead)…": "Ve frontě na redukci šumu pomocí AI (před vámi: {ahead})…",
   "Insert at the cursor (also written {})": "Vložit na pozici kurzoru (také se zapisuje {})",
@@ -206,7 +206,7 @@
   "{n} photo{}": "fotografií: {n}{:.0}",
   "{n} photo{} match · updates automatically as photos change": "Odpovídající fotografie: {n}{:.0} · aktualizuje se automaticky při změnách fotografií",
   "{n} photo{} now · updates automatically as photos change": "Aktuální fotografie: {n}{:.0} · aktualizuje se automaticky při změnách fotografií",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "Podobné fotografie: {n}{:.0} · Zobrazení → Zrušit filtry zobrazí všechny",
+  "{n} similar photo{} · View → Clear Filters to see all": "Podobné fotografie: {n}{:.0} · Zobrazení → Zrušit filtry zobrazí všechny",
   "{n} tasks are still running: {names}. Quitting stops them.": "Stále běží úlohy: {n}: {names}. Ukončení aplikace je zastaví.",
   "{n} {noun}": "{noun}: {n}",
   "{pct} %": "{pct} %",
@@ -252,7 +252,7 @@
   "Create the keyword “{name}”": "Vytvořit klíčové slovo „{name}“",
   "new, inside {parent}": "nové, pod {parent}",
   "Keywords of {n} selected photos": "Klíčová slova vybraných fotografií (počet: {n})",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "Klíčové slovo „{name}“ bylo odebráno z fotografií v počtu {n}. Úpravy → Zpět jej vrátí.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "Klíčové slovo „{name}“ bylo odebráno z fotografií v počtu {n}. Úpravy → Zpět jej vrátí.",
   "Inside: {names}": "Uvnitř: {names}",
   " in {g} group{}": " ve skupinách: {g}{:.0}",
   " · kept, not applied: {}": " · zachováno, nepoužito: {}",
@@ -263,7 +263,7 @@
   "Delete {n} effect preset{}? This can't be undone.": "Odstranit přednastavení efektů v počtu {n}{:.0}? Tuto akci nelze vrátit zpět.",
   "Effect: {name}": "Efekt: {name}",
   "Exported {} effect preset{}": "Exportováno přednastavení efektů: {0}{1:.0}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "Importováno efektů masek: {e}{:.0} (Maskování → Efekt)",
+  "Imported {e} mask effect{} (Masking → Effect)": "Importováno efektů masek: {e}{:.0} (Maskování → Efekt)",
   "Update “{name}” with Current Settings": "Aktualizovat „{name}“ podle aktuálního nastavení",
   "{n} preset{}": "Přednastavení: {n}{:.0}",
   "{count} more warnings.": "Další varování: {count}.",
diff --git a/crates/ui-egui/locales/cs.json b/crates/ui-egui/locales/cs.json
--- a/crates/ui-egui/locales/cs.json
+++ b/crates/ui-egui/locales/cs.json
@@ -1358,7 +1358,7 @@
   "Photo Grid": "Mřížka fotografií",
   "Photo Grid (G)": "Mřížka fotografií (G)",
   "Photo Merge": "Sloučení fotografií",
-  "Photo ▸ Edit In ▸ Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "Fotografie → Upravit v → Upravit v PhotoCraft (⇧⌘E nebo pravé tlačítko na fotografii) vykreslí kopii s vašimi úpravami, přidá ji do sady s originálem a otevře v PhotoCraft. Po uložení se kopie zde aktualizuje. Do PSD ukládá PhotoCraft přímo. Umístění ponechte prázdné, pokud nepoužíváte přenosnou kopii.",
+  "Photo → Edit In → Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "Fotografie → Upravit v → Upravit v PhotoCraft (⇧⌘E nebo pravé tlačítko na fotografii) vykreslí kopii s vašimi úpravami, přidá ji do sady s originálem a otevře v PhotoCraft. Po uložení se kopie zde aktualizuje. Do PSD ukládá PhotoCraft přímo. Umístění ponechte prázdné, pokud nepoužíváte přenosnou kopii.",
   "Photo ▸ Edit in External Editor (⇧⌘E) renders a 16-bit TIFF copy, stacks it with the original and opens it here (an app name on macOS, a program path elsewhere).": "Fotografie → Upravit v externím editoru (⇧⌘E) vykreslí 16bitovou kopii TIFF, přidá ji do sady s originálem a otevře v této aplikaci (název aplikace v macOS, jinde cesta k programu).",
   "PhotoCraft": "PhotoCraft",
   "Photos": "Fotografie",
@@ -1544,7 +1544,7 @@
   "Remove location info": "Odebrat informace o poloze",
   "Remove missing photos from the library": "Odebrat chybějící fotografie z knihovny",
   "Remove shortcut": "Odebrat zkratku",
-  "Remove this face box (undo with Edit ▸ Undo)": "Odebrat tento rámeček obličeje (vrátit přes Úpravy → Zpět)",
+  "Remove this face box (undo with Edit → Undo)": "Odebrat tento rámeček obličeje (vrátit přes Úpravy → Zpět)",
   "Remove this filter": "Odebrat tento filtr",
   "Remove this group": "Odebrat tuto skupinu",
   "Remove this rule": "Odebrat toto pravidlo",
@@ -1584,7 +1584,7 @@
   "Reset all curves: point curves (all channels) and parametric (double-click a channel to reset only that one)": "Obnoví všechny křivky: bodové (všechny kanály) i parametrickou (dvojklik na kanál obnoví pouze tento kanál)",
   "Reset all edits": "Obnovit všechny úpravy",
   "Reset all edits (Cmd+Shift+R)": "Obnovit všechny úpravy (Cmd+Shift+R)",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "Inteligentní alba se aktualizují automaticky: změňte jejich pravidla nebo použijte Fotografie → Odstranit fotografii",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "Inteligentní alba se aktualizují automaticky: změňte jejich pravidla nebo použijte Fotografie → Odstranit fotografii",
   "LightCraft stopped": "LightCraft se neočekávaně ukončil",
   "Zoom to Focus Point": "Přiblížit na zaostřovací bod",
   "Focus": "Zaostření",
@@ -1630,7 +1630,7 @@
   "Revert": "Vrátit změny",
   "Review": "Zkontrolovat",
   "Review Import": "Zkontrolovat import",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Pravé tlačítko na fotografii → Nastavit jako referenční fotografii, pak Zobrazení → Referenční zobrazení (Shift+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Pravé tlačítko na fotografii → Nastavit jako referenční fotografii, pak Zobrazení → Referenční zobrazení (Shift+R)",
   "Right-click a preset to delete it": "Přednastavení odstraníte kliknutím pravým tlačítkem",
   "Rights Usage Terms": "Podmínky použití práv",
   "Rotate": "Otočit",
diff --git a/crates/ui-egui/locales/de-formats.json b/crates/ui-egui/locales/de-formats.json
--- a/crates/ui-egui/locales/de-formats.json
+++ b/crates/ui-egui/locales/de-formats.json
@@ -25,7 +25,7 @@
   "Added {what} to {name}": "{what} zu {name} hinzugefügt",
   "Adding photos… {} of {}": "Fotos werden hinzugefügt… {} von {}",
   "All {n} selected photos move by the same amount.": "Alle {n} ausgewählten Fotos werden um denselben Betrag verschoben.",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Bereits in der Bibliothek unter „Zuletzt gelöscht“: {} Foto{} ausgewählt · Rechtsklick → Wiederherstellen, oder endgültig löschen, um neu zu importieren",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "Bereits in der Bibliothek unter „Zuletzt gelöscht“: {} Foto{} ausgewählt · Rechtsklick → Wiederherstellen, oder endgültig löschen, um neu zu importieren",
   "Already in the library: {} photo{} selected in All Photos": "Bereits in der Bibliothek: {} Foto{} in „Alle Fotos“ ausgewählt",
   "Auto ({})": "Automatisch ({})",
   "Auto Import: added {} photo{}": "Automatischer Import: {} Foto{} hinzugefügt",
@@ -72,7 +72,7 @@
   "Imported {n} point curve preset{}": "Importierte Punktkurvenvorgaben: {n}{:.0}",
   "Imported {n} preset{}": "Importierte Vorgaben: {n}{:.0}",
   "Imported {n} preset{}, {f} file{} not readable": "Importierte Vorgaben: {n}{0:.0}, nicht lesbare Dateien: {f}{1:.0}",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "Importierte Profile: {profiles}{:.0} (Profilbrowser → jeweilige Gruppen)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "Importierte Profile: {profiles}{:.0} (Profilbrowser → jeweilige Gruppen)",
   "Imported {} photo{}": "{} Foto{} importiert",
   "Insert at the cursor (also written {})": "An Cursorposition einfügen (auch als {} geschrieben)",
   "Label Set: {name}": "Farbmarkierungssatz: {name}",
@@ -195,7 +195,7 @@
   "… and {more} more": "… und {more} weitere",
   "LightCraft can't decode this raw variant yet ({}). You're editing the camera's embedded JPEG preview, which already includes the camera's picture style (e.g. Monochrome) and white balance.": "LightCraft kann diese RAW-Variante noch nicht dekodieren ({}). Du bearbeitest die eingebettete JPEG-Vorschau der Kamera. Sie enthält bereits deren Bildstil (z. B. Monochrom) und Weißabgleich.",
   "Filtered: {label}": "Gefiltert: {label}",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "Ähnliche Fotos: {n}{:.0} · Ansicht → Filter löschen zeigt alle Fotos",
+  "{n} similar photo{} · View → Clear Filters to see all": "Ähnliche Fotos: {n}{:.0} · Ansicht → Filter löschen zeigt alle Fotos",
   "Moved; {} photo(s) relinked": "Verschoben; {} Foto(s) neu verknüpft",
   "{} label (click more labels to show any of them)": "Farbmarkierung {} (weitere Farbmarkierungen auswählen, um alle passenden Fotos zu zeigen)",
   ", {} star{}": ", Sterne: {}{:.0}",
@@ -242,7 +242,7 @@
   "Create the keyword “{name}”": "Stichwort „{name}“ erstellen",
   "new, inside {parent}": "neu, in {parent}",
   "Keywords of {n} selected photos": "Stichwörter von {n} ausgewählten Fotos",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "„{name}“ von {n} Fotos entfernt. Bearbeiten → Rückgängig stellt es wieder her.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "„{name}“ von {n} Fotos entfernt. Bearbeiten → Rückgängig stellt es wieder her.",
   "1 task is still running: {names}. Quitting stops it.": "1 Aufgabe läuft noch: {names}. Beenden bricht sie ab.",
   "{n} tasks are still running: {names}. Quitting stops them.": "{n} Aufgaben laufen noch: {names}. Beenden bricht sie ab.",
   "{what} merge cancelled": "{what}-Zusammenfügung abgebrochen",
@@ -262,7 +262,7 @@
   "Delete {n} effect preset{}? This can't be undone.": "{n} Effektvorgabe(n) löschen?{:.0} Dies kann nicht rückgängig gemacht werden.",
   "Effect: {name}": "Effekt: {name}",
   "Exported {} effect preset{}": "Exportierte Effektvorgaben: {}{:.0}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "{e} Maskeneffekt(e) importiert{:.0} (Maskieren → Effekt)",
+  "Imported {e} mask effect{} (Masking → Effect)": "{e} Maskeneffekt(e) importiert{:.0} (Maskieren → Effekt)",
   "Update “{name}” with Current Settings": "„{name}“ mit aktuellen Einstellungen aktualisieren",
   "{n} preset{}": "{n} Vorgabe(n){:.0}",
   "{name} opened in the other editor": "{name} im anderen Editor geöffnet",
diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json
--- a/crates/ui-egui/locales/de.json
+++ b/crates/ui-egui/locales/de.json
@@ -1239,7 +1239,7 @@
   "Remove from survey": "Aus Übersicht entfernen",
   "Remove location info": "Ortsinformationen entfernen",
   "Remove shortcut": "Tastenkürzel entfernen",
-  "Remove this face box (undo with Edit ▸ Undo)": "Diesen Gesichtsrahmen entfernen (rückgängig über Bearbeiten → Rückgängig)",
+  "Remove this face box (undo with Edit → Undo)": "Diesen Gesichtsrahmen entfernen (rückgängig über Bearbeiten → Rückgängig)",
   "Remove this filter": "Diesen Filter entfernen",
   "Remove this group": "Diese Gruppe entfernen",
   "Remove this rule": "Diese Regel entfernen",
@@ -1295,7 +1295,7 @@
   "Revert": "Zurück zum letzten Stand",
   "Review": "Überprüfung",
   "Review Import": "Import überprüfen",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Rechtsklick auf ein Foto → Als Referenzfoto festlegen, dann Ansicht → Referenzansicht (Umschalt+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Rechtsklick auf ein Foto → Als Referenzfoto festlegen, dann Ansicht → Referenzansicht (Umschalt+R)",
   "Right-click a preset to delete it": "Rechtsklick auf eine Vorgabe, um sie zu löschen",
   "Rights Usage Terms": "Nutzungsbedingungen",
   "Rotate": "Drehen",
@@ -2313,7 +2313,7 @@
   "Same folder as original photo": "Im selben Ordner wie das Originalfoto",
   "Tidying the thumbnail cache": "Miniaturcache wird aufgeräumt",
   "Reset all edits (Cmd+Shift+R)": "Alle Bearbeitungen zurücksetzen (⌘⇧R)",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "Smart-Alben aktualisieren sich selbst: Ändere ihre Regeln oder verwende Foto → Foto löschen",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "Smart-Alben aktualisieren sich selbst: Ändere ihre Regeln oder verwende Foto → Foto löschen",
   "LightCraft stopped": "LightCraft wurde beendet",
   "Zoom to Focus Point": "Auf Fokuspunkt zoomen",
   "Focus": "Fokus",
diff --git a/crates/ui-egui/locales/es-formats.json b/crates/ui-egui/locales/es-formats.json
--- a/crates/ui-egui/locales/es-formats.json
+++ b/crates/ui-egui/locales/es-formats.json
@@ -25,7 +25,7 @@
   "Added {what} to {name}": "{what} añadido a {name}",
   "Adding photos… {} of {}": "Añadiendo fotos… {} de {}",
   "All {n} selected photos move by the same amount.": "Las {n} fotos seleccionadas se desplazan la misma cantidad.",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Ya en la biblioteca, en Eliminadas recientemente: {0} foto{1} seleccionada{1} · clic derecho → Restaurar, o Eliminar definitivamente para importar de nuevo",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "Ya en la biblioteca, en Eliminadas recientemente: {0} foto{1} seleccionada{1} · clic derecho → Restaurar, o Eliminar definitivamente para importar de nuevo",
   "Already in the library: {} photo{} selected in All Photos": "Ya en la biblioteca: {0} foto{1} seleccionada{1} en Todas las fotos",
   "Auto ({})": "Automático ({})",
   "Auto Import: added {} photo{}": "Importación automática: {0} foto{1} añadida{1}",
@@ -72,7 +72,7 @@
   "Imported {n} point curve preset{}": "{n} ajuste{0} preestablecido{0} de curva de puntos importado{0}",
   "Imported {n} preset{}": "{n} ajuste{0} preestablecido{0} importado{0}",
   "Imported {n} preset{}, {f} file{} not readable": "{n} ajuste{0} preestablecido{0} importado{0}, {f} archivo{1} ilegible{1}",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "Perfiles importados: {profiles}{:.0} (Explorador de perfiles → sus grupos)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "Perfiles importados: {profiles}{:.0} (Explorador de perfiles → sus grupos)",
   "Imported {} photo{}": "{0} foto{1} importada{1}",
   "Insert at the cursor (also written {})": "Insertar en el cursor (también se escribe {})",
   "Label Set: {name}": "Conjunto de etiquetas: {name}",
@@ -195,7 +195,7 @@
   "… and {more} more": "… y {more} más",
   "LightCraft can't decode this raw variant yet ({}). You're editing the camera's embedded JPEG preview, which already includes the camera's picture style (e.g. Monochrome) and white balance.": "LightCraft aún no puede decodificar esta variante RAW ({}). Se está editando la vista previa JPEG incrustada de la cámara, que ya incluye el estilo de imagen de la cámara (p. ej. Monocromo) y el balance de blancos.",
   "Filtered: {label}": "Filtrado: {label}",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "Fotos similares: {n}{:.0} · Vista → Borrar filtros para ver todas",
+  "{n} similar photo{} · View → Clear Filters to see all": "Fotos similares: {n}{:.0} · Vista → Borrar filtros para ver todas",
   "Moved; {} photo(s) relinked": "Movida; {} foto(s) vinculada(s) de nuevo",
   "{} label (click more labels to show any of them)": "Etiqueta {} (hacer clic en más etiquetas para mostrar cualquiera de ellas)",
   ", {} star{}": ", {} estrella{}",
@@ -242,7 +242,7 @@
   "Create the keyword “{name}”": "Crear la palabra clave “{name}”",
   "new, inside {parent}": "nueva, dentro de {parent}",
   "Keywords of {n} selected photos": "Palabras clave de {n} fotos seleccionadas",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "“{name}” quitada de {n} fotos. Editar → Deshacer la devuelve.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "“{name}” quitada de {n} fotos. Editar → Deshacer la devuelve.",
   "1 task is still running: {names}. Quitting stops it.": "1 tarea sigue en curso: {names}. Salir la detiene.",
   "{n} tasks are still running: {names}. Quitting stops them.": "{n} tareas siguen en curso: {names}. Salir las detiene.",
   "{what} merge cancelled": "Combinación {what} cancelada",
@@ -262,7 +262,7 @@
   "Delete {n} effect preset{}? This can't be undone.": "¿Eliminar {n} ajuste(s) preestablecido(s) de efecto?{:.0} No se puede deshacer.",
   "Effect: {name}": "Efecto: {name}",
   "Exported {} effect preset{}": "Ajustes de efecto exportados: {}{:.0}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "{e} efecto(s) de máscara importado(s){:.0} (Máscaras → Efecto)",
+  "Imported {e} mask effect{} (Masking → Effect)": "{e} efecto(s) de máscara importado(s){:.0} (Máscaras → Efecto)",
   "Update “{name}” with Current Settings": "Actualizar «{name}» con los ajustes actuales",
   "{n} preset{}": "{n} ajuste(s){:.0}",
   "{name} opened in the other editor": "{name} abierto en el otro editor",
diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json
--- a/crates/ui-egui/locales/es.json
+++ b/crates/ui-egui/locales/es.json
@@ -1246,7 +1246,7 @@
   "Remove from survey": "Quitar de Encuesta",
   "Remove location info": "Quitar información de ubicación",
   "Remove shortcut": "Quitar método abreviado",
-  "Remove this face box (undo with Edit ▸ Undo)": "Quitar este recuadro de cara (deshacer con Editar → Deshacer)",
+  "Remove this face box (undo with Edit → Undo)": "Quitar este recuadro de cara (deshacer con Editar → Deshacer)",
   "Remove this filter": "Quitar este filtro",
   "Remove this group": "Quitar este grupo",
   "Remove this rule": "Quitar esta regla",
@@ -1302,7 +1302,7 @@
   "Revert": "Revertir",
   "Review": "Revisión",
   "Review Import": "Revisar importación",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Clic derecho en una foto → Definir como foto de referencia y, después, Vista → Vista de referencia (Mayús+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Clic derecho en una foto → Definir como foto de referencia y, después, Vista → Vista de referencia (Mayús+R)",
   "Right-click a preset to delete it": "Clic derecho en un ajuste preestablecido para eliminarlo",
   "Rights Usage Terms": "Condiciones de uso de los derechos",
   "Rotate": "Rotar",
@@ -2316,7 +2316,7 @@
   "Same folder as original photo": "Misma carpeta que la foto original",
   "Tidying the thumbnail cache": "Ordenando la caché de miniaturas",
   "Reset all edits (Cmd+Shift+R)": "Restablecer todas las ediciones (Cmd+Mayús+R)",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "Los álbumes inteligentes se actualizan solos: cambie sus reglas o use Foto → Eliminar foto",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "Los álbumes inteligentes se actualizan solos: cambie sus reglas o use Foto → Eliminar foto",
   "LightCraft stopped": "LightCraft se detuvo",
   "Focus Point": "Punto de enfoque",
   "Zoom to Focus Point": "Zoom al punto de enfoque",
diff --git a/crates/ui-egui/locales/fi-formats.json b/crates/ui-egui/locales/fi-formats.json
--- a/crates/ui-egui/locales/fi-formats.json
+++ b/crates/ui-egui/locales/fi-formats.json
@@ -39,7 +39,7 @@
   "Added “{keyword}” to {n} photo{}": "Avainsana ”{keyword}” lisätty kuviin: {n}{:.0}",
   "Adding photos… {} of {}": "Lisätään kuvia… {} / {}",
   "All {n} selected photos move by the same amount.": "Kaikkia valittuja kuvia ({n}) siirretään saman verran.",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Jo kirjastossa, Äskettäin poistetuissa: valittuja kuvia {}{:.0} · napsauta hiiren kakkospainikkeella → Palauta tai Poista pysyvästi tuodaksesi uudelleen",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "Jo kirjastossa, Äskettäin poistetuissa: valittuja kuvia {}{:.0} · napsauta hiiren kakkospainikkeella → Palauta tai Poista pysyvästi tuodaksesi uudelleen",
   "Already in the library: {} photo{} selected in All Photos": "Jo kirjastossa (Kaikki kuvat): valittuja kuvia {}{:.0}",
   "Already in “{}”": "Jo kohteessa ”{}”",
   "Applied {name}": "{name} käytetty",
@@ -93,12 +93,12 @@
   "Import cancelled · {} photo{} added": "Tuonti peruttu · lisättyjä kuvia: {}{:.0}",
   "Import failed: {e}": "Tuonti epäonnistui: {e}",
   "Import warnings ({n}):\n{warnings}": "Tuontivaroitukset ({n}):\n{warnings}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "Tuotuja maskitehosteita: {e}{:.0} (Maskaus → Tehoste)",
+  "Imported {e} mask effect{} (Masking → Effect)": "Tuotuja maskitehosteita: {e}{:.0} (Maskaus → Tehoste)",
   "Imported {name}": "{name} tuotu",
   "Imported {n} point curve preset{}": "Tuotuja pistekäyrän esiasetuksia: {n}{:.0}",
   "Imported {n} preset{}": "Tuotuja esiasetuksia: {n}{:.0}",
   "Imported {n} preset{}, {f} file{} not readable": "Tuotuja esiasetuksia: {n}{:.0}, ei luettavissa olevia tiedostoja: {f}{:.0}",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "Tuotuja profiileja: {profiles}{:.0} (Profiiliselain → niiden ryhmät)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "Tuotuja profiileja: {profiles}{:.0} (Profiiliselain → niiden ryhmät)",
   "Imported {} photo{}": "Tuotuja kuvia: {}{:.0}",
   "In line for AI Denoise ({ahead} ahead)…": "Jonossa tekoälykohinanpoistoon ({ahead} edellä)…",
   "Insert at the cursor (also written {})": "Lisää kohdistimen kohtaan (kirjoitetaan myös {})",
@@ -157,7 +157,7 @@
   "Remove “{}” from Favorites": "Poista ”{}” suosikeista",
   "Removed from {}": "Poistettu kohteesta {}",
   "Removed {what} from {name}": "{what} poistettu kohteesta {name}",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "”{name}” poistettu {n} kuvalta. Muokkaa → Kumoa palauttaa sen.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "”{name}” poistettu {n} kuvalta. Muokkaa → Kumoa palauttaa sen.",
   "Rename {n} Photos…": "Nimeä uudelleen {n} kuvaa…",
   "Rename “{name}”": "Nimeä uudelleen ”{name}”",
   "Renames {n} face{} across the library. The files are not changed.": "Nimeää uudelleen kasvoja koko kirjastossa: {n}{:.0}. Tiedostoja ei muuteta.",
@@ -249,7 +249,7 @@
   "{n} preset{}": "Esiasetuksia: {n}{:.0}",
   "{n} raws can now be developed from their raw data": "Raw-kuvia, jotka voidaan nyt kehittää raakadatastaan: {n}",
   "{n} selected photos · {pages} pages": "Valittuja kuvia: {n} · sivuja: {pages}",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "Samankaltaisia kuvia: {n}{:.0} · Näytä → Tyhjennä suodattimet näyttääksesi kaikki",
+  "{n} similar photo{} · View → Clear Filters to see all": "Samankaltaisia kuvia: {n}{:.0} · Näytä → Tyhjennä suodattimet näyttääksesi kaikki",
   "{n} tasks are still running: {names}. Quitting stops them.": "Käynnissä olevia tehtäviä: {n}: {names}. Lopettaminen pysäyttää ne.",
   "{n} {noun}": "{n} {noun}",
   "{pct} %": "{pct} %",
diff --git a/crates/ui-egui/locales/fi.json b/crates/ui-egui/locales/fi.json
--- a/crates/ui-egui/locales/fi.json
+++ b/crates/ui-egui/locales/fi.json
@@ -1414,7 +1414,7 @@
   "Photo Grid": "Kuvaruudukko",
   "Photo Grid (G)": "Kuvaruudukko (G)",
   "Photo Merge": "Kuvien yhdistäminen",
-  "Photo ▸ Edit In ▸ Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "Kuva → Muokkaa ohjelmassa → Muokkaa PhotoCraftissa (⇧⌘E tai napsauta kuvaa hiiren kakkospainikkeella) renderöi kopion muokkauksineen, pinoaa sen alkuperäisen kanssa ja avaa sen PhotoCraftissa. Tallenna siellä, niin kopio päivittyy täällä. PSD-muodossa PhotoCraft tallentaa suoraan siihen. Jätä sijainti tyhjäksi, ellet käytä kannettavaa kopiota.",
+  "Photo → Edit In → Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "Kuva → Muokkaa ohjelmassa → Muokkaa PhotoCraftissa (⇧⌘E tai napsauta kuvaa hiiren kakkospainikkeella) renderöi kopion muokkauksineen, pinoaa sen alkuperäisen kanssa ja avaa sen PhotoCraftissa. Tallenna siellä, niin kopio päivittyy täällä. PSD-muodossa PhotoCraft tallentaa suoraan siihen. Jätä sijainti tyhjäksi, ellet käytä kannettavaa kopiota.",
   "Photo ▸ Edit in External Editor (⇧⌘E) renders a 16-bit TIFF copy, stacks it with the original and opens it here (an app name on macOS, a program path elsewhere).": "Kuva → Muokkaa ulkoisessa muokkausohjelmassa (⇧⌘E) renderöi 16-bittisen TIFF-kopion, pinoaa sen alkuperäisen kanssa ja avaa sen täällä (macOS:ssä sovelluksen nimi, muualla ohjelman polku).",
   "PhotoCraft": "PhotoCraft",
   "Photos": "Kuvat",
@@ -1613,7 +1613,7 @@
   "Remove missing photos from the library": "Poista puuttuvat kuvat kirjastosta",
   "Remove region": "Poista alue",
   "Remove shortcut": "Poista pikakuvake",
-  "Remove this face box (undo with Edit ▸ Undo)": "Poista tämä kasvorajaus (kumoa kohdasta Muokkaa → Kumoa)",
+  "Remove this face box (undo with Edit → Undo)": "Poista tämä kasvorajaus (kumoa kohdasta Muokkaa → Kumoa)",
   "Remove this filter": "Poista tämä suodatin",
   "Remove this group": "Poista tämä ryhmä",
   "Remove this rule": "Poista tämä sääntö",
@@ -1677,7 +1677,7 @@
   "Review": "Tarkistus",
   "Review Import": "Tarkista tuonti",
   "Right": "Oikea",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Napsauta kuvaa hiiren kakkospainikkeella → Aseta vertailukuvaksi, sitten Näytä → Vertailunäkymä (Shift+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Napsauta kuvaa hiiren kakkospainikkeella → Aseta vertailukuvaksi, sitten Näytä → Vertailunäkymä (Shift+R)",
   "Right-click a preset to delete it": "Poista esiasetus napsauttamalla sitä hiiren kakkospainikkeella",
   "Rights Usage Terms": "Oikeuksien käyttöehdot",
   "Rotate": "Kierrä",
@@ -1888,7 +1888,7 @@
   "Smart Album Rule Fields": "Älyalbumin sääntökentät",
   "Smart Preview Status": "Älyesikatselun tila",
   "Smart Previews Location": "Älyesikatseluiden sijainti",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "Älyalbumit päivittyvät automaattisesti: muuta niiden sääntöjä tai käytä Kuva → Poista kuva",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "Älyalbumit päivittyvät automaattisesti: muuta niiden sääntöjä tai käytä Kuva → Poista kuva",
   "Smart preview available": "Älyesikatselu saatavilla",
   "Smart previews": "Älyesikatselut",
   "Smart previews folder changed": "Älyesikatseluiden kansio muuttui",
diff --git a/crates/ui-egui/locales/fr-formats.json b/crates/ui-egui/locales/fr-formats.json
--- a/crates/ui-egui/locales/fr-formats.json
+++ b/crates/ui-egui/locales/fr-formats.json
@@ -25,7 +25,7 @@
   "Added {what} to {name}": "{what} ajouté à {name}",
   "Adding photos… {} of {}": "Ajout des photos… {} sur {}",
   "All {n} selected photos move by the same amount.": "Les {n} photos sélectionnées se déplacent de la même valeur.",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Déjà dans la bibliothèque, dans « Supprimés récemment » : {} photo{} sélectionnée(s) · clic droit → Restaurer, ou Supprimer définitivement pour importer à nouveau",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "Déjà dans la bibliothèque, dans « Supprimés récemment » : {} photo{} sélectionnée(s) · clic droit → Restaurer, ou Supprimer définitivement pour importer à nouveau",
   "Already in the library: {} photo{} selected in All Photos": "Déjà dans la bibliothèque : {} photo{} sélectionnée(s) dans Toutes les photos",
   "Auto ({})": "Automatique ({})",
   "Auto Import: added {} photo{}": "Importation automatique : {} photo{} ajoutée(s)",
@@ -65,7 +65,7 @@
   "Imported {n} point curve preset{}": "{n} préréglage{} de courbe à points importé(s)",
   "Imported {n} preset{}": "{n} préréglage{} importé(s)",
   "Imported {n} preset{}, {f} file{} not readable": "{n} préréglage{} importé(s), {f} fichier{} illisible(s)",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "{profiles} profil{} importé(s) (Navigateur de profils → leurs groupes)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "{profiles} profil{} importé(s) (Navigateur de profils → leurs groupes)",
   "The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.": "Les fichiers annexes XMP de {n} photos n'ont pas pu être écrits (leur dossier est-il hors ligne ?). LightCraft continue d'essayer.",
   "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "Le profil XMP « {profile} » n’a pas été appliqué à {path} ; les couleurs peuvent différer de Lightroom.",
   "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "La géométrie Lightroom de {path} n’a pas été conservée ; {mode} sera recalculé.",
@@ -195,7 +195,7 @@
   "… and {more} more": "… et {more} de plus",
   "LightCraft can't decode this raw variant yet ({}). You're editing the camera's embedded JPEG preview, which already includes the camera's picture style (e.g. Monochrome) and white balance.": "LightCraft ne peut pas encore décoder cette variante RAW ({}). Vous modifiez l’aperçu JPEG intégré de l’appareil photo, qui inclut déjà le style d’image (par ex. monochrome) et la balance des blancs de l’appareil.",
   "Filtered: {label}": "Filtré : {label}",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "{n} photo{} similaire(s) · Affichage → Effacer les filtres pour tout voir",
+  "{n} similar photo{} · View → Clear Filters to see all": "{n} photo{} similaire(s) · Affichage → Effacer les filtres pour tout voir",
   "Moved; {} photo(s) relinked": "Déplacé ; {} photo(s) de nouveau associée(s)",
   "{} label (click more labels to show any of them)": "Étiquette {} (cliquez sur « autres étiquettes » pour afficher celles de votre choix)",
   ", {} star{}": ", {} étoile{}",
@@ -242,7 +242,7 @@
   "Create the keyword “{name}”": "Créer le mot-clé « {name} »",
   "new, inside {parent}": "nouveau, dans {parent}",
   "Keywords of {n} selected photos": "Mots-clés de {n} photos sélectionnées",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "« {name} » retiré de {n} photos. Modifier → Annuler le rétablit.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "« {name} » retiré de {n} photos. Modifier → Annuler le rétablit.",
   "1 task is still running: {names}. Quitting stops it.": "1 tâche est encore en cours : {names}. Quitter l'arrête.",
   "{n} tasks are still running: {names}. Quitting stops them.": "{n} tâches sont encore en cours : {names}. Quitter les arrête.",
   "{what} merge cancelled": "Fusion {what} annulée",
@@ -262,7 +262,7 @@
   "Delete {n} effect preset{}? This can't be undone.": "Supprimer {n} paramètre(s) prédéfini(s) d’effet ?{:.0} Cette action est irréversible.",
   "Effect: {name}": "Effet : {name}",
   "Exported {} effect preset{}": "Paramètres d’effet exportés : {}{:.0}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "{e} effet(s) de masque importé(s){:.0} (Masquage → Effet)",
+  "Imported {e} mask effect{} (Masking → Effect)": "{e} effet(s) de masque importé(s){:.0} (Masquage → Effet)",
   "Update “{name}” with Current Settings": "Mettre à jour « {name} » avec les réglages actuels",
   "{n} preset{}": "{n} paramètre(s){:.0}",
   "{name} opened in the other editor": "{name} ouvert dans l’autre éditeur",
diff --git a/crates/ui-egui/locales/fr.json b/crates/ui-egui/locales/fr.json
--- a/crates/ui-egui/locales/fr.json
+++ b/crates/ui-egui/locales/fr.json
@@ -1180,7 +1180,7 @@
   "Remove from survey": "Retirer de la vue multiple",
   "Remove location info": "Supprimer les informations de lieu",
   "Remove shortcut": "Supprimer le raccourci",
-  "Remove this face box (undo with Edit ▸ Undo)": "Retirer ce cadre de visage (annuler avec Édition → Annuler)",
+  "Remove this face box (undo with Edit → Undo)": "Retirer ce cadre de visage (annuler avec Édition → Annuler)",
   "Remove this filter": "Retirer ce filtre",
   "Remove this group": "Retirer ce groupe",
   "Remove this rule": "Retirer cette règle",
@@ -1230,7 +1230,7 @@
   "Revert": "Rétablir l'original",
   "Review": "Révision",
   "Review Import": "Vérifier l'importation",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Clic droit sur une photo → Définir comme photo de référence, puis Affichage → Vue de référence (Maj+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Clic droit sur une photo → Définir comme photo de référence, puis Affichage → Vue de référence (Maj+R)",
   "Right-click a preset to delete it": "Clic droit sur un préréglage pour le supprimer",
   "Rights Usage Terms": "Conditions d'utilisation des droits",
   "Rotate": "Pivoter",
@@ -2312,7 +2312,7 @@
   "Same folder as original photo": "Même dossier que la photo d’origine",
   "Tidying the thumbnail cache": "Rangement du cache des miniatures",
   "Reset all edits (Cmd+Shift+R)": "Réinitialiser toutes les retouches (Cmd+Maj+R)",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "Les albums intelligents se mettent à jour d'eux-mêmes : modifiez leurs règles ou utilisez Photo → Supprimer la photo",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "Les albums intelligents se mettent à jour d'eux-mêmes : modifiez leurs règles ou utilisez Photo → Supprimer la photo",
   "LightCraft stopped": "LightCraft s’est arrêté",
   "Focus Point": "Point de mise au point",
   "Zoom to Focus Point": "Zoomer sur le point de mise au point",
diff --git a/crates/ui-egui/locales/id-formats.json b/crates/ui-egui/locales/id-formats.json
--- a/crates/ui-egui/locales/id-formats.json
+++ b/crates/ui-egui/locales/id-formats.json
@@ -120,7 +120,7 @@
   "Adding photos… {} of {}": "Menambahkan foto… {} dari {}",
   " ({} in Recently Deleted)": " ({} di Baru dihapus)",
   "Already in the library: {} photo{} selected in All Photos": "Sudah ada di pustaka: {} foto{:.0} dipilih di Semua foto",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Sudah ada di pustaka, di Baru dihapus: {} foto{:.0} dipilih · klik kanan → Pulihkan, atau Hapus permanen untuk mengimpor ulang",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "Sudah ada di pustaka, di Baru dihapus: {} foto{:.0} dipilih · klik kanan → Pulihkan, atau Hapus permanen untuk mengimpor ulang",
   "Auto ({})": "Otomatis ({})",
   "Build 1:1 Previews ({scope})": "Buat pratinjau 1:1 ({scope})",
   "Build Smart Previews ({scope})": "Buat pratinjau pintar ({scope})",
@@ -169,7 +169,7 @@
   "Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "Geometri Lightroom untuk {path} tidak dipertahankan; transformasi Tegakkan terpandu yang tersimpan tidak didukung.",
   "Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "Geometri Lightroom untuk {path} tidak dipertahankan; transformasi Tegakkan yang tersimpan tidak didukung.",
   "XMP adjustments were not imported for {path}: {fields}.": "Penyesuaian XMP untuk {path} tidak diimpor: {fields}.",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "{profiles} profil diimpor{:.0} (Penjelajah profil → grup masing-masing)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "{profiles} profil diimpor{:.0} (Penjelajah profil → grup masing-masing)",
   "Insert at the cursor (also written {})": "Sisipkan di posisi kursor (dapat juga ditulis {})",
   "MIT OR Apache-2.0. Fonts: {} (OFL). Icons: original.": "MIT OR Apache-2.0. Font: {} (OFL). Ikon: orisinal.",
   "Painting “{k}”: click photos to add or remove it · Esc stops": "Menerapkan “{k}”: klik foto untuk menambahkan atau menghapusnya · Esc untuk berhenti",
@@ -199,7 +199,7 @@
   "{} — show their photos": "{} — tampilkan foto orang ini",
   "LightCraft can't decode this raw variant yet ({}). You're editing the camera's embedded JPEG preview, which already includes the camera's picture style (e.g. Monochrome) and white balance.": "LightCraft belum dapat mendekode varian raw ini ({}). Anda sedang mengedit pratinjau JPEG bawaan kamera, yang sudah menyertakan gaya gambar kamera (mis. Monokrom) dan keseimbangan putihnya.",
   "Filtered: {label}": "Difilter: {label}",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "{n} foto serupa{:.0} · Tampilan → Hapus filter untuk melihat semua",
+  "{n} similar photo{} · View → Clear Filters to see all": "{n} foto serupa{:.0} · Tampilan → Hapus filter untuk melihat semua",
   "Moved; {} photo(s) relinked": "Dipindahkan; {} foto ditautkan ulang",
   "{} label (click more labels to show any of them)": "Label {} (klik label lain untuk ikut menampilkan foto berlabel tersebut)",
   ", {} star{}": ", {} bintang{:.0}",
@@ -245,7 +245,7 @@
   "Create the keyword “{name}”": "Buat kata kunci “{name}”",
   "new, inside {parent}": "baru, di dalam {parent}",
   "Keywords of {n} selected photos": "Kata kunci {n} foto yang dipilih",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "“{name}” dihapus dari {n} foto. Edit → Urungkan akan mengembalikannya.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "“{name}” dihapus dari {n} foto. Edit → Urungkan akan mengembalikannya.",
   "1 task is still running: {names}. Quitting stops it.": "1 tugas masih berjalan: {names}. Jika keluar sekarang, tugas ini akan dihentikan.",
   "{n} tasks are still running: {names}. Quitting stops them.": "{n} tugas masih berjalan: {names}. Jika keluar sekarang, semua tugas ini akan dihentikan.",
   "{what} merge cancelled": "Penggabungan {what} dibatalkan",
@@ -265,7 +265,7 @@
   "Delete {n} effect preset{}? This can't be undone.": "Hapus {n} preset efek{:.0}? Tindakan ini tidak dapat diurungkan.",
   "Effect: {name}": "Efek: {name}",
   "Exported {} effect preset{}": "{} preset efek diekspor{:.0}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "{e} efek masker{:.0} diimpor (Masker → Efek)",
+  "Imported {e} mask effect{} (Masking → Effect)": "{e} efek masker{:.0} diimpor (Masker → Efek)",
   "Update “{name}” with Current Settings": "Perbarui “{name}” dengan pengaturan saat ini",
   "{n} preset{}": "{n} preset{:.0}",
   "{name} opened in the other editor": "{name} dibuka di editor lain",
diff --git a/crates/ui-egui/locales/id.json b/crates/ui-egui/locales/id.json
--- a/crates/ui-egui/locales/id.json
+++ b/crates/ui-egui/locales/id.json
@@ -1397,7 +1397,7 @@
   "Photo Grid": "Grid foto",
   "Photo Grid (G)": "Grid foto (G)",
   "Photo Merge": "Gabung foto",
-  "Photo ▸ Edit In ▸ Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "Foto → Edit di → Edit di PhotoCraft (⇧⌘E, atau klik kanan foto) merender salinan beserta hasil edit Anda, menumpuknya dengan foto asli, lalu membukanya di PhotoCraft. Simpan di sana, dan salinan di sini ikut diperbarui. Jika berformat PSD, PhotoCraft menyimpan langsung ke file tersebut. Kosongkan lokasi kecuali Anda menggunakan salinan portabel.",
+  "Photo → Edit In → Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "Foto → Edit di → Edit di PhotoCraft (⇧⌘E, atau klik kanan foto) merender salinan beserta hasil edit Anda, menumpuknya dengan foto asli, lalu membukanya di PhotoCraft. Simpan di sana, dan salinan di sini ikut diperbarui. Jika berformat PSD, PhotoCraft menyimpan langsung ke file tersebut. Kosongkan lokasi kecuali Anda menggunakan salinan portabel.",
   "Photo ▸ Edit in External Editor (⇧⌘E) renders a 16-bit TIFF copy, stacks it with the original and opens it here (an app name on macOS, a program path elsewhere).": "Foto → Edit di editor eksternal (⇧⌘E) membuat salinan TIFF 16-bit, menumpuknya dengan file asli, lalu membukanya dengan program yang diisi di sini (nama aplikasi di macOS, jalur program di sistem lain).",
   "PhotoCraft": "PhotoCraft",
   "Photos": "Foto",
@@ -1601,7 +1601,7 @@
   "Remove location info": "Hapus info lokasi",
   "Remove missing photos from the library": "Keluarkan foto yang hilang dari pustaka",
   "Remove shortcut": "Hapus pintasan",
-  "Remove this face box (undo with Edit ▸ Undo)": "Hapus kotak wajah ini (urungkan lewat Edit → Urungkan)",
+  "Remove this face box (undo with Edit → Undo)": "Hapus kotak wajah ini (urungkan lewat Edit → Urungkan)",
   "Remove this filter": "Hapus filter ini",
   "Remove this group": "Hapus grup ini",
   "Remove this rule": "Hapus aturan ini",
@@ -1668,7 +1668,7 @@
   "Review": "Tinjauan",
   "Review Import": "Tinjau impor",
   "Right": "Kanan",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Klik kanan foto → Jadikan foto referensi, lalu Tampilan → Tampilan referensi (Shift+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Klik kanan foto → Jadikan foto referensi, lalu Tampilan → Tampilan referensi (Shift+R)",
   "Right-click a preset to delete it": "Klik kanan preset untuk menghapusnya",
   "Rights Usage Terms": "Ketentuan penggunaan",
   "Rotate": "Putar",
@@ -1887,7 +1887,7 @@
   "Smart Album Rule Fields": "Kolom aturan album pintar",
   "Smart Preview Status": "Status pratinjau pintar",
   "Smart Previews Location": "Lokasi pratinjau pintar",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "Album pintar diperbarui secara otomatis: ubah aturannya, atau gunakan Foto → Hapus foto",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "Album pintar diperbarui secara otomatis: ubah aturannya, atau gunakan Foto → Hapus foto",
   "Smart preview available": "Pratinjau pintar tersedia",
   "Smart previews": "Pratinjau pintar",
   "Smart previews folder changed": "Folder pratinjau pintar diubah",
diff --git a/crates/ui-egui/locales/it-formats.json b/crates/ui-egui/locales/it-formats.json
--- a/crates/ui-egui/locales/it-formats.json
+++ b/crates/ui-egui/locales/it-formats.json
@@ -25,7 +25,7 @@
   "Added {what} to {name}": "Aggiunto a {name}: {what}",
   "Adding photos… {} of {}": "Aggiunta delle foto… {} di {}",
   "All {n} selected photos move by the same amount.": "Tutte le {n} foto selezionate si spostano della stessa quantità.",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Già nella libreria, in Eliminate di recente: foto selezionate {}{:.0} · clic destro → Ripristina, oppure Elimina definitivamente per importarle da capo",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "Già nella libreria, in Eliminate di recente: foto selezionate {}{:.0} · clic destro → Ripristina, oppure Elimina definitivamente per importarle da capo",
   "Already in the library: {} photo{} selected in All Photos": "Già nella libreria (selezionate in Tutte le foto): {}{:.0}",
   "Auto ({})": "Automatico ({})",
   "Auto Import: added {} photo{}": "Importazione automatica · foto aggiunte: {}{:.0}",
@@ -70,7 +70,7 @@
   "Imported {n} point curve preset{}": "Preset di curva a punti importati: {n}{:.0}",
   "Imported {n} preset{}": "Preset importati: {n}{:.0}",
   "Imported {n} preset{}, {f} file{} not readable": "Preset importati: {n}{:.0}, file illeggibili: {f}{:.0}",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "Profili importati: {profiles}{:.0} (Browser dei profili → i loro gruppi)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "Profili importati: {profiles}{:.0} (Browser dei profili → i loro gruppi)",
   "Imported {} photo{}": "Foto importate: {}{:.0}",
   "Insert at the cursor (also written {})": "Inserisci al cursore (si scrive anche {})",
   "Label Set: {name}": "Set di etichette: {name}",
@@ -192,7 +192,7 @@
   "… and {more} more": "… e altre {more}",
   "LightCraft can't decode this raw variant yet ({}). You're editing the camera's embedded JPEG preview, which already includes the camera's picture style (e.g. Monochrome) and white balance.": "LightCraft non sa ancora decodificare questa variante raw ({}). Stai modificando l'anteprima JPEG incorporata dalla fotocamera, che contiene già lo stile immagine della fotocamera (ad es. Monocromatico) e il bilanciamento del bianco.",
   "Filtered: {label}": "Filtrate: {label}",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "Foto simili: {n}{:.0} · Visualizza → Cancella filtri per vederle tutte",
+  "{n} similar photo{} · View → Clear Filters to see all": "Foto simili: {n}{:.0} · Visualizza → Cancella filtri per vederle tutte",
   "Moved; {} photo(s) relinked": "Spostata; foto ricollegate: {}",
   "{} label (click more labels to show any of them)": "Etichetta {} (fai clic su altre etichette per mostrarne una qualsiasi)",
   ", {} star{}": ", stelle: {}{:.0}",
@@ -264,7 +264,7 @@
   "Keywords of {n} selected photos": "Parole chiave delle foto selezionate: {n}",
   "new, inside {parent}": "nuova, dentro {parent}",
   "Delete these {n} effect presets?": "Eliminare questi preset effetto ({n})?",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "Imported {e} mask effect{} (Masking → Effect)",
+  "Imported {e} mask effect{} (Masking → Effect)": "Imported {e} mask effect{} (Masking → Effect)",
   "{photos} catalog records processed; {collections} collections.": "Record del catalogo elaborati: {photos}; raccolte: {collections}.",
   "Skipped {problem}": "Saltato: {problem}",
   "Updated {name}": "{name} aggiornato",
@@ -282,7 +282,7 @@
   " · kept, not applied: {}": " · conservato, non applicato: {}",
   "Delete the effect preset “{name}”?": "Eliminare il preset effetto “{name}”?",
   "{n} raws can now be developed from their raw data": "Raw che ora si possono sviluppare dai loro dati raw: {n}",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "Removed “{name}” from {n} photos. Edit → Undo puts it back.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "Removed “{name}” from {n} photos. Edit → Undo puts it back.",
   "Inside: {names}": "Al suo interno: {names}",
   "Template preview: {name}": "Anteprima del modello: {name}",
   "{n} photos in view · {slides} slides · {time}": "Foto nella vista: {n} · diapositive: {slides} · {time}",
diff --git a/crates/ui-egui/locales/it.json b/crates/ui-egui/locales/it.json
--- a/crates/ui-egui/locales/it.json
+++ b/crates/ui-egui/locales/it.json
@@ -1191,7 +1191,7 @@
   "Remove from survey": "Rimuovi dalla panoramica",
   "Remove location info": "Rimuovi le informazioni sulla posizione",
   "Remove shortcut": "Rimuovi scorciatoia",
-  "Remove this face box (undo with Edit ▸ Undo)": "Rimuovi questo riquadro del volto (annulla con Modifica → Annulla)",
+  "Remove this face box (undo with Edit → Undo)": "Rimuovi questo riquadro del volto (annulla con Modifica → Annulla)",
   "Remove this filter": "Rimuovi questo filtro",
   "Remove this group": "Rimuovi questo gruppo",
   "Remove this rule": "Rimuovi questa regola",
@@ -1228,7 +1228,7 @@
   "Reset all curves: point curves (all channels) and parametric (double-click a channel to reset only that one)": "Reimposta tutte le curve: curve a punti (tutti i canali) e parametrica (doppio clic su un canale per reimpostare solo quello)",
   "Reset all edits": "Ripristina tutte le modifiche",
   "Reset all edits (Cmd+Shift+R)": "Reimposta tutte le modifiche (Cmd+Maiusc+R)",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "Gli album smart si aggiornano da soli: cambia le loro regole, oppure usa Foto → Elimina foto",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "Gli album smart si aggiornano da soli: cambia le loro regole, oppure usa Foto → Elimina foto",
   "LightCraft stopped": "LightCraft si è chiuso",
   "Zoom to Focus Point": "Zoom sul punto di messa a fuoco",
   "Focus": "Messa a fuoco",
@@ -1266,7 +1266,7 @@
   "Revert": "Ripristina",
   "Review": "Revisione",
   "Review Import": "Rivedi importazione",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Clic destro su una foto → Imposta come foto di riferimento, poi Visualizza → Vista di riferimento (Maiusc+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Clic destro su una foto → Imposta come foto di riferimento, poi Visualizza → Vista di riferimento (Maiusc+R)",
   "Right-click a preset to delete it": "Clic destro su un preset per eliminarlo",
   "Rights Usage Terms": "Condizioni d'uso dei diritti",
   "Rotate": "Ruota",
@@ -1838,7 +1838,7 @@
   "Monitor profile": "Profilo monitor",
   "None (the display is treated as sRGB)": "Nessuno (lo schermo viene trattato come sRGB)",
   "Other editor": "Altro editor",
-  "Photo ▸ Edit In ▸ Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "Foto → Modifica in → Modifica in PhotoCraft (⇧⌘E, o clic destro su una foto) esegue il rendering di una copia con le tue modifiche, la impila con l'originale e la apre in PhotoCraft. Salva lì e la copia si aggiorna qui. Come PSD, PhotoCraft salva direttamente su di essa. Lascia vuota la posizione a meno che tu non usi una copia portatile.",
+  "Photo → Edit In → Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "Foto → Modifica in → Modifica in PhotoCraft (⇧⌘E, o clic destro su una foto) esegue il rendering di una copia con le tue modifiche, la impila con l'originale e la apre in PhotoCraft. Salva lì e la copia si aggiorna qui. Come PSD, PhotoCraft salva direttamente su di essa. Lascia vuota la posizione a meno che tu non usi una copia portatile.",
   "PhotoCraft": "PhotoCraft",
   "Photos are shown through your monitor's ICC profile, so colours are right on wide-gamut and calibrated displays, and the Detail view uses the display's whole gamut. The histogram, the preview caches and exports are not affected.": "Le foto vengono mostrate attraverso il profilo ICC del monitor, così i colori sono corretti su schermi calibrati e a gamut esteso, e la vista Dettaglio usa tutto il gamut dello schermo. Istogramma, cache delle anteprime ed esportazioni non cambiano.",
   "Photos stay in All Photos. Deleting a folder also deletes its albums. You can undo this.": "Le foto restano in Tutte le foto. Eliminando una cartella si eliminano anche i suoi album. Puoi annullare l'operazione.",
diff --git a/crates/ui-egui/locales/ja-formats.json b/crates/ui-egui/locales/ja-formats.json
--- a/crates/ui-egui/locales/ja-formats.json
+++ b/crates/ui-egui/locales/ja-formats.json
@@ -120,7 +120,7 @@
   "Adding photos… {} of {}": "写真を追加中：{1}枚中{0}枚",
   " ({} in Recently Deleted)": "（うち{}枚は最近削除した写真）",
   "Already in the library: {} photo{} selected in All Photos": "ライブラリに登録済み：すべての写真で{}枚を選択しました{:.0}",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "ライブラリの「最近削除した写真」にあります：{}枚を選択しました{:.0}・右クリック → 復元、または完全に削除してから読み込み直し",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "ライブラリの「最近削除した写真」にあります：{}枚を選択しました{:.0}・右クリック → 復元、または完全に削除してから読み込み直し",
   "Auto ({})": "自動 ({})",
   "Build 1:1 Previews ({scope})": "1:1プレビューを作成（{scope}）",
   "Build Smart Previews ({scope})": "スマートプレビューを作成（{scope}）",
@@ -169,7 +169,7 @@
   "Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "{path} の Lightroom ジオメトリは保持されませんでした。ガイド付き Upright の保存された変換はサポートされていません。",
   "Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "{path} の Lightroom ジオメトリは保持されませんでした。保存された Upright の変換はサポートされていません。",
   "XMP adjustments were not imported for {path}: {fields}.": "{path} の XMP 調整は読み込まれませんでした：{fields}。",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "{profiles}個のプロファイルを読み込みました{:.0}（プロファイルブラウザー → 各グループ）",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "{profiles}個のプロファイルを読み込みました{:.0}（プロファイルブラウザー → 各グループ）",
   "Insert at the cursor (also written {})": "カーソル位置に挿入（{}とも表記可）",
   "MIT OR Apache-2.0. Fonts: {} (OFL). Icons: original.": "MIT OR Apache-2.0。フォント：{}（OFL）。アイコン：オリジナル",
   "Painting “{k}”: click photos to add or remove it · Esc stops": "「{k}」を適用中：写真をクリックして付け外し・Escで終了",
@@ -199,7 +199,7 @@
   "{} — show their photos": "{} — この人物の写真を表示",
   "LightCraft can't decode this raw variant yet ({}). You're editing the camera's embedded JPEG preview, which already includes the camera's picture style (e.g. Monochrome) and white balance.": "LightCraftはこのRAWの形式（{}）をまだデコードできません。カメラの埋め込みJPEGプレビューを編集しています。このプレビューには、カメラのピクチャースタイル（モノクロなど）とホワイトバランスがすでに反映されています",
   "Filtered: {label}": "フィルター適用中：{label}",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "似た写真{n}枚{:.0}・すべて表示するには「表示 → フィルターを解除」",
+  "{n} similar photo{} · View → Clear Filters to see all": "似た写真{n}枚{:.0}・すべて表示するには「表示 → フィルターを解除」",
   "Moved; {} photo(s) relinked": "移動しました。{}枚の写真の参照先を更新しました",
   "{} label (click more labels to show any of them)": "{}ラベル（ほかのラベルもクリックすると、いずれかに一致する写真を表示）",
   ", {} star{}": "、星{}つ{:.0}",
@@ -246,7 +246,7 @@
   "Create the keyword “{name}”": "キーワード「{name}」を作成",
   "new, inside {parent}": "新規、{parent} の中",
   "Keywords of {n} selected photos": "選択した写真{n}枚のキーワード",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "「{name}」を写真{n}枚から削除しました。「編集」→「取り消す」で元に戻せます。",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "「{name}」を写真{n}枚から削除しました。「編集」→「取り消す」で元に戻せます。",
   "1 task is still running: {names}. Quitting stops it.": "1件のタスクが実行中です：{names}。終了すると停止します。",
   "{n} tasks are still running: {names}. Quitting stops them.": "{n}件のタスクが実行中です：{names}。終了すると停止します。",
   "{what} merge cancelled": "{what}合成をキャンセルしました",
@@ -266,7 +266,7 @@
   "Delete {n} effect preset{}? This can't be undone.": "効果プリセット{n}件を削除しますか？{:.0}元に戻せません。",
   "Effect: {name}": "効果：{name}",
   "Exported {} effect preset{}": "効果プリセットを{}件書き出しました{:.0}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "マスク効果を{e}件読み込みました{:.0}（マスク → 効果）",
+  "Imported {e} mask effect{} (Masking → Effect)": "マスク効果を{e}件読み込みました{:.0}（マスク → 効果）",
   "Update “{name}” with Current Settings": "「{name}」を現在の設定で更新",
   "{n} preset{}": "{n}件のプリセット{:.0}",
   "{name} opened in the other editor": "{name}を別のエディタで開きました",
diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json
--- a/crates/ui-egui/locales/ja.json
+++ b/crates/ui-egui/locales/ja.json
@@ -720,7 +720,7 @@
   "Remove a filter above, or choose Clear all": "上のフィルターを解除するか、「すべて解除」を選択してください",
   "Remove from Target Album": "追加先アルバムから除去",
   "Remove shortcut": "ショートカットを削除",
-  "Remove this face box (undo with Edit ▸ Undo)": "この顔の枠を除去（「編集 → 取り消す」で元に戻せます）",
+  "Remove this face box (undo with Edit → Undo)": "この顔の枠を除去（「編集 → 取り消す」で元に戻せます）",
   "Rename Component": "構成要素の名前を変更",
   "Rename Folder": "フォルダ名を変更",
   "Rename Mask": "マスク名を変更",
@@ -1180,7 +1180,7 @@
   "Reverse the order": "逆順にする",
   "Review": "レビュー",
   "Review Import": "読み込み内容を確認",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "写真を右クリックして参照写真に設定し、「表示 → 参照表示」（⇧R）を選択してください",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "写真を右クリックして参照写真に設定し、「表示 → 参照表示」（⇧R）を選択してください",
   "Right-click a preset to delete it": "プリセットを右クリックして削除",
   "Rights Usage Terms": "利用条件",
   "Rotate": "回転",
@@ -2314,7 +2314,7 @@
   "Same folder as original photo": "元の写真と同じフォルダー",
   "Tidying the thumbnail cache": "サムネールキャッシュを整理中",
   "Reset all edits (Cmd+Shift+R)": "すべての編集をリセット（⌘⇧R）",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "スマートアルバムは自動で更新されます。ルールを変更するか、写真 → 写真を削除を使ってください",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "スマートアルバムは自動で更新されます。ルールを変更するか、写真 → 写真を削除を使ってください",
   "LightCraft stopped": "LightCraftが終了しました",
   "Focus Point": "フォーカスポイント",
   "Zoom to Focus Point": "フォーカスポイントにズーム",
diff --git a/crates/ui-egui/locales/ka-formats.json b/crates/ui-egui/locales/ka-formats.json
--- a/crates/ui-egui/locales/ka-formats.json
+++ b/crates/ui-egui/locales/ka-formats.json
@@ -35,7 +35,7 @@
   "Added “{keyword}” to {n} photo{}": "„{keyword}“ დაემატა ფოტოს: {n}{:.0}",
   "Adding photos… {} of {}": "ფოტოების დამატება… {} / {}",
   "All {n} selected photos move by the same amount.": "მონიშნული ფოტოები ({n}) ერთი და იმავე სიდიდით გადაინაცვლებს.",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "უკვე ბიბლიოთეკაშია, ახლახან წაშლილებში: მონიშნულია ფოტო: {}{:.0} · მარჯვენა დაწკაპუნება → აღდგენა, ან სამუდამოდ წაშლა თავიდან იმპორტისთვის",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "უკვე ბიბლიოთეკაშია, ახლახან წაშლილებში: მონიშნულია ფოტო: {}{:.0} · მარჯვენა დაწკაპუნება → აღდგენა, ან სამუდამოდ წაშლა თავიდან იმპორტისთვის",
   "Already in the library: {} photo{} selected in All Photos": "უკვე ბიბლიოთეკაშია: „ყველა ფოტოში“ მონიშნულია ფოტო: {}{:.0}",
   "Auto ({})": "ავტომატური ({})",
   "Auto Import: added {} photo{}": "ავტოიმპორტი: დაემატა ფოტო: {}{:.0}",
@@ -80,7 +80,7 @@
   "Imported {n} point curve preset{}": "იმპორტირებულია წერტილოვანი მრუდის პრესეტი: {n}{:.0}",
   "Imported {n} preset{}": "იმპორტირებულია პრესეტი: {n}{:.0}",
   "Imported {n} preset{}, {f} file{} not readable": "იმპორტირებულია პრესეტი: {n}{:.0}, ვერ იკითხება ფაილი: {f}{:.0}",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "იმპორტირებულია პროფილი: {profiles}{:.0} (პროფილების ბრაუზერი → მათი ჯგუფები)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "იმპორტირებულია პროფილი: {profiles}{:.0} (პროფილების ბრაუზერი → მათი ჯგუფები)",
   "Imported {} photo{}": "იმპორტირებულია ფოტო: {}{:.0}",
   "In line for AI Denoise ({ahead} ahead)…": "AI ხმაურის მოცილების რიგში (წინ არის: {ahead})…",
   "Insert at the cursor (also written {})": "კურსორის ადგილას ჩასმა (ასევე იწერება როგორც {})",
@@ -205,7 +205,7 @@
   "{n} photo{}": "ფოტო: {n}{:.0}",
   "{n} photo{} match · updates automatically as photos change": "შესაბამისი ფოტო: {n}{:.0} · ავტომატურად განახლდება ფოტოების ცვლილებისას",
   "{n} photo{} now · updates automatically as photos change": "ახლა ფოტო: {n}{:.0} · ავტომატურად განახლდება ფოტოების ცვლილებისას",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "მსგავსი ფოტო: {n}{:.0} · ყველას სანახავად: ხედი → ფილტრების გასუფთავება",
+  "{n} similar photo{} · View → Clear Filters to see all": "მსგავსი ფოტო: {n}{:.0} · ყველას სანახავად: ხედი → ფილტრების გასუფთავება",
   "{n} tasks are still running: {names}. Quitting stops them.": "ჯერ კიდევ მიმდინარეობს ამოცანები ({n}): {names}. გასვლა მათ შეწყვეტს.",
   "{n} {noun}": "{noun}: {n}",
   "{pct} %": "{pct} %",
@@ -262,12 +262,12 @@
   "Effect: {name}": "Effect: {name}",
   "Exported to {path}": "Exported to {path}",
   "Exported {} effect preset{}": "Exported {} effect preset{}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "Imported {e} mask effect{} (Masking → Effect)",
+  "Imported {e} mask effect{} (Masking → Effect)": "Imported {e} mask effect{} (Masking → Effect)",
   "Imported {name}": "Imported {name}",
   "Keywords of {n} selected photos": "Keywords of {n} selected photos",
   "LightCraft stopped unexpectedly: {e}": "LightCraft stopped unexpectedly: {e}",
   "LightCraft stopped unexpectedly: {e}\n\nThe log file has the details: {path}": "LightCraft stopped unexpectedly: {e}\n\nThe log file has the details: {path}",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "Removed “{name}” from {n} photos. Edit → Undo puts it back.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "Removed “{name}” from {n} photos. Edit → Undo puts it back.",
   "Inside: {names}": "შიგნით: {names}",
   "Renames {n} face{} across the library. The files are not changed.": "Renames {n} face{} across the library. The files are not changed.",
   "Saved template {name}": "Saved template {name}",
diff --git a/crates/ui-egui/locales/ka.json b/crates/ui-egui/locales/ka.json
--- a/crates/ui-egui/locales/ka.json
+++ b/crates/ui-egui/locales/ka.json
@@ -1484,7 +1484,7 @@
   "Photo Merge": "ფოტოების გაერთიანება",
   "Photo Merge of Canon CR2 (and other raws whose colour LightCraft fits from the camera's own JPEG, such as NEF and": "Canon CR2-ის (და სხვა RAW ფაილების, რომელთა ფერსაც LightCraft კამერის საკუთარი JPEG-იდან არგებს, როგორიცაა NEF და",
   "Photo menu. The filmstrip has the photo context menu too. Adding a file again that is in Recently Deleted no": "„ფოტო“ მენიუში. კადრების ზოლსაც აქვს ფოტოს კონტექსტური მენიუ. ახლახან წაშლილში მყოფი ფაილის ხელახლა დამატება აღარ",
-  "Photo ▸ Edit In ▸ Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "ფოტო → რედაქტირება სხვა პროგრამაში → რედაქტირება PhotoCraft-ში (⇧⌘E, ან მარჯვენა დაწკაპუნება ფოტოზე) არენდერებს ასლს თქვენი ცვლილებებით, ორიგინალთან ერთ დასტაში აერთიანებს და PhotoCraft-ში ხსნის. იქ შენახვისას ასლი აქაც განახლდება. PSD-ის შემთხვევაში PhotoCraft პირდაპირ მასში ინახავს. მდებარეობა ცარიელი დატოვეთ, თუ პორტატულ ასლს არ იყენებთ.",
+  "Photo → Edit In → Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "ფოტო → რედაქტირება სხვა პროგრამაში → რედაქტირება PhotoCraft-ში (⇧⌘E, ან მარჯვენა დაწკაპუნება ფოტოზე) არენდერებს ასლს თქვენი ცვლილებებით, ორიგინალთან ერთ დასტაში აერთიანებს და PhotoCraft-ში ხსნის. იქ შენახვისას ასლი აქაც განახლდება. PSD-ის შემთხვევაში PhotoCraft პირდაპირ მასში ინახავს. მდებარეობა ცარიელი დატოვეთ, თუ პორტატულ ასლს არ იყენებთ.",
   "Photo ▸ Edit in External Editor (⇧⌘E) renders a 16-bit TIFF copy, stacks it with the original and opens it here (an app name on macOS, a program path elsewhere).": "ფოტო → რედაქტირება გარე რედაქტორში (⇧⌘E) ქმნის 16-ბიტიან TIFF ასლს, ორიგინალთან ერთად დასტაში აერთიანებს და აქ ხსნის (macOS-ზე აპის სახელი, სხვაგან — პროგრამის მისამართი).",
   "Photos": "ფოტოები",
   "Photos are shown through your monitor's ICC profile, so colours are right on wide-gamut and calibrated displays, and the Detail view uses the display's whole gamut. The histogram, the preview caches and exports are not affected.": "ფოტოები თქვენი მონიტორის ICC პროფილით ნაჩვენებია, ამიტომ ფერები სწორია ფართო ფერთა გამისა და დაკალიბრებულ ეკრანებზე, ხოლო დეტალური ხედი ეკრანის მთელ ფერთა გამას იყენებს. ჰისტოგრამაზე, წინასწარი ხედების ქეშებსა და ექსპორტზე ეს გავლენას არ ახდენს.",
@@ -1689,7 +1689,7 @@
   "Remove location info": "მდებარეობის ინფორმაციის წაშლა",
   "Remove missing photos from the library": "დაკარგული ფოტოების ბიბლიოთეკიდან ამოშლა",
   "Remove shortcut": "მალსახმობის წაშლა",
-  "Remove this face box (undo with Edit ▸ Undo)": "ამ სახის ჩარჩოს წაშლა (დაბრუნება: რედაქტირება → დაბრუნება)",
+  "Remove this face box (undo with Edit → Undo)": "ამ სახის ჩარჩოს წაშლა (დაბრუნება: რედაქტირება → დაბრუნება)",
   "Remove this filter": "ამ ფილტრის მოშორება",
   "Remove this group": "ამ ჯგუფის წაშლა",
   "Remove this rule": "ამ წესის წაშლა",
@@ -1754,7 +1754,7 @@
   "Right-click a folder (or a disk other than the startup disk) in the sidebar's Folders section ▸ Synchronize": "მარჯვენა დაწკაპუნება საქაღალდეზე (ან სასტარტოს გარდა სხვა დისკზე) გვერდითი ზოლის საქაღალდეების სექციაში → საქაღალდის",
   "Right-click a folder ▸ New ▸ Create Album… / Create Smart Album… / Create Smart Album from Filter… / Create Folder…": "მარჯვენა დაწკაპუნება საქაღალდეზე → ახალი → ალბომის შექმნა… / სმარტ-ალბომის შექმნა… / სმარტ-ალბომის შექმნა ფილტრიდან… / საქაღალდის შექმნა…",
   "Right-click a photo ▸ Edit In ▸ Edit in PhotoCraft (or Photo ▸ Edit In, ⇧⌘E) renders a copy with your edits, stacks it": "მარჯვენა დაწკაპუნება ფოტოზე → რედაქტირება სხვა პროგრამაში → რედაქტირება PhotoCraft-ში (ან ფოტო → რედაქტირება სხვა პროგრამაში, ⇧⌘E) არენდერებს ასლს თქვენი ცვლილებებით, ათავსებს მას",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "დაწკაპეთ ფოტოზე მარჯვენა ღილაკით → ეტალონურ ფოტოდ დაყენება, შემდეგ ხედი → ეტალონური ხედი (Shift+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "დაწკაპეთ ფოტოზე მარჯვენა ღილაკით → ეტალონურ ფოტოდ დაყენება, შემდეგ ხედი → ეტალონური ხედი (Shift+R)",
   "Right-click a preset to delete it": "პრესეტის წასაშლელად დაწკაპეთ მასზე მარჯვენა ღილაკით",
   "Right-click ▸ Put New Keywords Inside This Keyword: new keywords, made with + or typed in the box above, go inside": "მარჯვენა დაწკაპუნება → ახალი საკვანძო სიტყვების ამ საკვანძო სიტყვაში ჩასმა: + ღილაკით ან ზემოთ ველში აკრეფილი ახალი საკვანძო სიტყვები",
   "Rights Usage Terms": "უფლებების გამოყენების პირობები",
diff --git a/crates/ui-egui/locales/pl-formats.json b/crates/ui-egui/locales/pl-formats.json
--- a/crates/ui-egui/locales/pl-formats.json
+++ b/crates/ui-egui/locales/pl-formats.json
@@ -25,7 +25,7 @@
   "Added {what} to {name}": "Dodano {what} do {name}",
   "Adding photos… {} of {}": "Dodawanie zdjęć… {} z {}",
   "All {n} selected photos move by the same amount.": "Wszystkie {n} zaznaczone zdjęcia zostaną przesunięte o tę samą wartość.",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Już w bibliotece, w Ostatnio usuniętych — zaznaczone zdjęcia: {}{:.0} · kliknij prawym przyciskiem → Przywróć albo Usuń trwale, aby zaimportować ponownie",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "Już w bibliotece, w Ostatnio usuniętych — zaznaczone zdjęcia: {}{:.0} · kliknij prawym przyciskiem → Przywróć albo Usuń trwale, aby zaimportować ponownie",
   "Already in the library: {} photo{} selected in All Photos": "Już w bibliotece — zaznaczone zdjęcia we Wszystkich zdjęciach: {}{:.0}",
   "Auto ({})": "Automatycznie ({})",
   "Auto Import: added {} photo{}": "Automatyczny import — dodane zdjęcia: {}{:.0}",
@@ -63,7 +63,7 @@
   "Imported {n} point curve preset{}": "Zaimportowane ustawienia krzywej punktowej: {n}{:.0}",
   "Imported {n} preset{}": "Zaimportowane ustawienia wstępne: {n}{:.0}",
   "Imported {n} preset{}, {f} file{} not readable": "Zaimportowane ustawienia wstępne: {n}{:.0}, nieczytelne pliki: {f}{:.0}",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "Zaimportowane profile: {profiles}{:.0} (Przeglądarka profili → odpowiednie grupy)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "Zaimportowane profile: {profiles}{:.0} (Przeglądarka profili → odpowiednie grupy)",
   "Imported {} photo{}": "Zaimportowane zdjęcia: {}{:.0}",
   "Insert at the cursor (also written {})": "Wstaw w miejscu kursora (zostanie też zapisane {})",
   "Label Set: {name}": "Zestaw etykiet: {name}",
@@ -183,7 +183,7 @@
   "… and {more} more": "… i jeszcze {more}",
   "LightCraft can't decode this raw variant yet ({}). You're editing the camera's embedded JPEG preview, which already includes the camera's picture style (e.g. Monochrome) and white balance.": "LightCraft nie obsługuje jeszcze tego wariantu RAW ({}). Edytujesz podgląd JPEG osadzony przez aparat, zawierający już styl obrazu aparatu (np. monochromatyczny) i balans bieli.",
   "Filtered: {label}": "Filtrowanie: {label}",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "Podobne zdjęcia: {n}{:.0} · Widok → Wyczyść filtry, aby zobaczyć wszystkie",
+  "{n} similar photo{} · View → Clear Filters to see all": "Podobne zdjęcia: {n}{:.0} · Widok → Wyczyść filtry, aby zobaczyć wszystkie",
   "Moved; {} photo(s) relinked": "Przeniesiono; ponownie powiązane zdjęcia: {}",
   "{} label (click more labels to show any of them)": "{} etykieta (kliknij kolejne etykiety, aby wyświetlić dowolną z nich)",
   ", {} star{}": ", gwiazdki: {}{:.0}",
@@ -257,7 +257,7 @@
   "Delete {n} effect preset{}? This can't be undone.": "Usunąć ustawienia efektów ({n})?{:.0} Tej operacji nie można cofnąć.",
   "Effect: {name}": "Efekt: {name}",
   "Exported {} effect preset{}": "Wyeksportowane ustawienia efektów: {}{:.0}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "Zaimportowane efekty masek: {e}{:.0} (Maskowanie → Efekt)",
+  "Imported {e} mask effect{} (Masking → Effect)": "Zaimportowane efekty masek: {e}{:.0} (Maskowanie → Efekt)",
   "Update “{name}” with Current Settings": "Zaktualizuj „{name}” bieżącymi ustawieniami",
   "{n} preset{}": "Ustawienia: {n}{:.0}",
   "{count} more warnings.": "Pozostałe ostrzeżenia: {count}.",
@@ -284,7 +284,7 @@
   "Create the keyword “{name}”": "Utwórz słowo kluczowe „{name}”",
   "new, inside {parent}": "nowe, w {parent}",
   "Keywords of {n} selected photos": "Słowa kluczowe zaznaczonych zdjęć ({n})",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "Usunięto „{name}” ze zdjęć ({n}). Edytuj → Cofnij przywraca.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "Usunięto „{name}” ze zdjęć ({n}). Edytuj → Cofnij przywraca.",
   "LightCraft stopped unexpectedly: {e}\n\nThe log file has the details: {path}": "LightCraft nieoczekiwanie zakończył działanie: {e}\n\nSzczegóły są w pliku dziennika: {path}",
   "LightCraft stopped unexpectedly: {e}": "LightCraft nieoczekiwanie zakończył działanie: {e}",
   "Renames {n} face{} across the library. The files are not changed.": "Zmienia nazwę twarzy w całej bibliotece (twarze: {n}).{:.0} Pliki nie są zmieniane.",
diff --git a/crates/ui-egui/locales/pl.json b/crates/ui-egui/locales/pl.json
--- a/crates/ui-egui/locales/pl.json
+++ b/crates/ui-egui/locales/pl.json
@@ -1152,7 +1152,7 @@
   "Remove from survey": "Usuń z widoku przeglądu",
   "Remove location info": "Usuń informacje o lokalizacji",
   "Remove shortcut": "Usuń skrót klawiszowy",
-  "Remove this face box (undo with Edit ▸ Undo)": "Usuń tę ramkę twarzy (cofnij przez Edycja → Cofnij)",
+  "Remove this face box (undo with Edit → Undo)": "Usuń tę ramkę twarzy (cofnij przez Edycja → Cofnij)",
   "Remove this filter": "Usuń ten filtr",
   "Remove this group": "Usuń tę grupę",
   "Remove this rule": "Usuń tę regułę",
@@ -1205,7 +1205,7 @@
   "Revert": "Cofnij zmiany",
   "Review": "Przegląd",
   "Review Import": "Sprawdź import",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Kliknij zdjęcie prawym przyciskiem → Ustaw jako zdjęcie referencyjne, a potem Widok → Widok referencyjny (Shift+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Kliknij zdjęcie prawym przyciskiem → Ustaw jako zdjęcie referencyjne, a potem Widok → Widok referencyjny (Shift+R)",
   "Right-click a preset to delete it": "Kliknij ustawienie wstępne prawym przyciskiem, aby je usunąć",
   "Rights Usage Terms": "Warunki korzystania z praw",
   "Rotate": "Obróć",
diff --git a/crates/ui-egui/locales/pt-br-formats.json b/crates/ui-egui/locales/pt-br-formats.json
--- a/crates/ui-egui/locales/pt-br-formats.json
+++ b/crates/ui-egui/locales/pt-br-formats.json
@@ -15,7 +15,7 @@
   "Added {n} photo{} to “{}”": "{n} foto{0} adicionada{0} a “{1}”",
   "Adding photos… {} of {}": "Adicionando fotos… {} de {}",
   "All {n} selected photos move by the same amount.": "Todas as {n} fotos selecionadas se movem pela mesma quantidade.",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Já na biblioteca, em Excluídos recentemente: {0} foto{1} selecionada{1} · clique com o botão direito → Restaurar, ou Excluir permanentemente para importar de novo",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "Já na biblioteca, em Excluídos recentemente: {0} foto{1} selecionada{1} · clique com o botão direito → Restaurar, ou Excluir permanentemente para importar de novo",
   "Already in the library: {} photo{} selected in All Photos": "Já na biblioteca: {0} foto{1} selecionada{1} em Todas as fotos",
   "Already in “{}”": "Já está em “{}”",
   "Auto ({})": "Automático ({})",
@@ -169,7 +169,7 @@
   "Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "A geometria do Lightroom não foi preservada para {path}; a transformação salva do Upright guiado não é compatível.",
   "Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "A geometria do Lightroom não foi preservada para {path}; a transformação salva do Upright não é compatível.",
   "XMP adjustments were not imported for {path}: {fields}.": "Os ajustes XMP não foram importados para {path}: {fields}.",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "{profiles} perfil(is) importado(s){:.0} (Navegador de perfis → seus grupos)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "{profiles} perfil(is) importado(s){:.0} (Navegador de perfis → seus grupos)",
   "Insert at the cursor (also written {})": "Inserir no cursor (também escrito {})",
   "MIT OR Apache-2.0. Fonts: {} (OFL). Icons: original.": "MIT OR Apache-2.0. Fontes: {} (OFL). Ícones: originais.",
   "Painting “{k}”: click photos to add or remove it · Esc stops": "Aplicando “{k}”: clique nas fotos para adicionar ou remover · Esc para",
@@ -199,7 +199,7 @@
   "{} — show their photos": "{} — mostrar as fotos desta pessoa",
   "LightCraft can't decode this raw variant yet ({}). You're editing the camera's embedded JPEG preview, which already includes the camera's picture style (e.g. Monochrome) and white balance.": "O LightCraft ainda não consegue decodificar esta variante RAW ({}). Você está editando a prévia JPEG incorporada da câmera, que já inclui o estilo de imagem da câmera (p. ex. Monocromático) e o balanço de branco.",
   "Filtered: {label}": "Filtrado: {label}",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "{n} foto{0} semelhante{0} · Exibir → Limpar filtros para ver todas",
+  "{n} similar photo{} · View → Clear Filters to see all": "{n} foto{0} semelhante{0} · Exibir → Limpar filtros para ver todas",
   "Moved; {} photo(s) relinked": "Movida; {} foto(s) vinculada(s) novamente",
   "{} label (click more labels to show any of them)": "Rótulo {} (clique em mais rótulos para mostrar qualquer um deles)",
   ", {} star{}": ", {} estrela{}",
@@ -246,7 +246,7 @@
   "Create the keyword “{name}”": "Criar a palavra-chave “{name}”",
   "new, inside {parent}": "nova, dentro de {parent}",
   "Keywords of {n} selected photos": "Palavras-chave de {n} fotos selecionadas",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "“{name}” removida de {n} fotos. Editar → Desfazer a traz de volta.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "“{name}” removida de {n} fotos. Editar → Desfazer a traz de volta.",
   "1 task is still running: {names}. Quitting stops it.": "1 tarefa ainda está em andamento: {names}. Sair a interrompe.",
   "{n} tasks are still running: {names}. Quitting stops them.": "{n} tarefas ainda estão em andamento: {names}. Sair as interrompe.",
   "{what} merge cancelled": "{what} mesclagem cancelada",
@@ -266,7 +266,7 @@
   "Delete {n} effect preset{}? This can't be undone.": "Excluir {n} predefinição(ões) de efeito?{:.0} Não é possível desfazer.",
   "Effect: {name}": "Efeito: {name}",
   "Exported {} effect preset{}": "Predefinições de efeito exportadas: {}{:.0}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "{e} efeito(s) de máscara importado(s){:.0} (Máscaras → Efeito)",
+  "Imported {e} mask effect{} (Masking → Effect)": "{e} efeito(s) de máscara importado(s){:.0} (Máscaras → Efeito)",
   "Update “{name}” with Current Settings": "Atualizar “{name}” com as configurações atuais",
   "{n} preset{}": "{n} predefinição(ões){:.0}",
   "{name} opened in the other editor": "{name} aberto no outro editor",
diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json
--- a/crates/ui-egui/locales/pt-br.json
+++ b/crates/ui-egui/locales/pt-br.json
@@ -1200,7 +1200,7 @@
   "Remove from survey": "Remover da inspeção",
   "Remove location info": "Remover informações de local",
   "Remove shortcut": "Remover atalho",
-  "Remove this face box (undo with Edit ▸ Undo)": "Remover esta caixa de rosto (desfaça com Editar → Desfazer)",
+  "Remove this face box (undo with Edit → Undo)": "Remover esta caixa de rosto (desfaça com Editar → Desfazer)",
   "Remove this filter": "Remover este filtro",
   "Remove this group": "Remover este grupo",
   "Remove this rule": "Remover esta regra",
@@ -1253,7 +1253,7 @@
   "Revert": "Reverter",
   "Review": "Revisão",
   "Review Import": "Revisar importação",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Clique com o botão direito em uma foto → Definir como foto de referência, depois Exibir → Exibição de referência (Shift+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Clique com o botão direito em uma foto → Definir como foto de referência, depois Exibir → Exibição de referência (Shift+R)",
   "Right-click a preset to delete it": "Clique com o botão direito em uma predefinição para excluí-la",
   "Rights Usage Terms": "Termos de uso dos direitos",
   "Rotate": "Girar",
@@ -2154,7 +2154,7 @@
   "Same folder as original photo": "Mesma pasta da foto original",
   "Tidying the thumbnail cache": "Organizando o cache de miniaturas",
   "Reset all edits (Cmd+Shift+R)": "Redefinir todas as edições (Cmd+Shift+R)",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "Álbuns inteligentes se atualizam sozinhos: altere as regras ou use Foto → Excluir foto",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "Álbuns inteligentes se atualizam sozinhos: altere as regras ou use Foto → Excluir foto",
   "LightCraft stopped": "O LightCraft parou",
   "Focus Point": "Ponto de foco",
   "Zoom to Focus Point": "Zoom no ponto de foco",
diff --git a/crates/ui-egui/locales/ru-formats.json b/crates/ui-egui/locales/ru-formats.json
--- a/crates/ui-egui/locales/ru-formats.json
+++ b/crates/ui-egui/locales/ru-formats.json
@@ -27,7 +27,7 @@
   "Added {what} to {name}": "{what}: добавлено в {name}",
   "Adding photos… {} of {}": "Добавление фото… {} из {}",
   "All {n} selected photos move by the same amount.": "Все выбранные фото ({n}) сдвигаются на одно и то же значение.",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Уже в библиотеке, в «Недавно удалённых»: выбрано фото: {}{:.0} · щёлкните правой кнопкой → Восстановить или Удалить навсегда, чтобы импортировать заново",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "Уже в библиотеке, в «Недавно удалённых»: выбрано фото: {}{:.0} · щёлкните правой кнопкой → Восстановить или Удалить навсегда, чтобы импортировать заново",
   "Already in the library: {} photo{} selected in All Photos": "Уже в библиотеке: выбрано фото в разделе «Все фото»: {}{:.0}",
   "Auto ({})": "Авто ({})",
   "Auto Import: added {} photo{}": "Автоимпорт: добавлено фото: {}{:.0}",
@@ -75,7 +75,7 @@
   "Imported {n} point curve preset{}": "Импортировано пресетов точечной кривой: {n}{:.0}",
   "Imported {n} preset{}": "Импортировано пресетов: {n}{:.0}",
   "Imported {n} preset{}, {f} file{} not readable": "Импортировано пресетов: {n}{:.0}, не читается файлов: {f}{:.0}",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "Импортировано профилей: {profiles}{:.0} (Обозреватель профилей → их группы)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "Импортировано профилей: {profiles}{:.0} (Обозреватель профилей → их группы)",
   "Imported {} photo{}": "Импортировано фото: {}{:.0}",
   "Insert at the cursor (also written {})": "Вставить в позицию курсора (также пишется как {})",
   "Label Set: {name}": "Набор меток: {name}",
@@ -169,7 +169,7 @@
   "{n} photo{} match · updates automatically as photos change": "Подходящих фото: {n}{:.0} · обновляется автоматически при изменении фото",
   "{n} photo{} now · updates automatically as photos change": "Сейчас фото: {n}{:.0} · обновляется автоматически при изменении фото",
   "{n} selected photos · {pages} pages": "Выбрано фото: {n} · страниц: {pages}",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "Похожих фото: {n}{:.0} · Вид → Сбросить фильтры, чтобы увидеть все",
+  "{n} similar photo{} · View → Clear Filters to see all": "Похожих фото: {n}{:.0} · Вид → Сбросить фильтры, чтобы увидеть все",
   "{pid} (missing)": "{pid} (отсутствует)",
   "{sel_n} selected · {counted}": "Выбрано: {sel_n} · {counted}",
   "{stage}… {:.0}%": "{stage}… {:.0}%",
@@ -242,7 +242,7 @@
   "Create the keyword “{name}”": "Создать ключевое слово «{name}»",
   "new, inside {parent}": "новое, внутри {parent}",
   "Keywords of {n} selected photos": "Ключевые слова выбранных фото: {n}",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "«{name}» убрано с фото: {n}. Правка → Отменить вернёт его.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "«{name}» убрано с фото: {n}. Правка → Отменить вернёт его.",
   "1 task is still running: {names}. Quitting stops it.": "Ещё выполняется задача: {names}. Выход прервёт её.",
   "{n} tasks are still running: {names}. Quitting stops them.": "Ещё выполняется задач: {n} — {names}. Выход прервёт их.",
   "{what} merge cancelled": "{what}: слияние отменено",
@@ -262,7 +262,7 @@
   "Delete {n} effect preset{}? This can't be undone.": "Удалить пресеты эффектов ({n})?{:.0} Это нельзя отменить.",
   "Effect: {name}": "Эффект: {name}",
   "Exported {} effect preset{}": "Экспортировано пресетов эффектов: {}{:.0}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "Импортировано эффектов маски: {e}{:.0} (Маски → Эффект)",
+  "Imported {e} mask effect{} (Masking → Effect)": "Импортировано эффектов маски: {e}{:.0} (Маски → Эффект)",
   "Update “{name}” with Current Settings": "Обновить «{name}» текущими настройками",
   "{n} preset{}": "Пресетов: {n}{:.0}",
   "{name} opened in the other editor": "{name} открыт в другом редакторе",
diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json
--- a/crates/ui-egui/locales/ru.json
+++ b/crates/ui-egui/locales/ru.json
@@ -1040,7 +1040,7 @@
   "Retouch": "Ретушь",
   "Revert": "Вернуть",
   "Review Import": "Проверить импорт",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Щёлкните фото правой кнопкой → Назначить эталонным фото, затем Вид → Режим эталона (Shift+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Щёлкните фото правой кнопкой → Назначить эталонным фото, затем Вид → Режим эталона (Shift+R)",
   "Right-click a preset to delete it": "Щёлкните пресет правой кнопкой, чтобы удалить его",
   "Rights Usage Terms": "Условия использования прав",
   "Rotate": "Повернуть",
@@ -1975,7 +1975,7 @@
   "Remove a filter above, or choose Clear all": "Уберите фильтр выше или выберите «Очистить всё»",
   "Remove from Library…": "Убрать из библиотеки…",
   "Remove from Target Album": "Убрать из целевого альбома",
-  "Remove this face box (undo with Edit ▸ Undo)": "Удалить эту рамку лица (отмена: Правка → Отменить)",
+  "Remove this face box (undo with Edit → Undo)": "Удалить эту рамку лица (отмена: Правка → Отменить)",
   "Rename Component": "Переименовать компонент",
   "Rename Folder… and Move Folder To… (Local) can be undone and redone (issue #97): the folder moves back on disk with": "Переименование папки… и перемещение папки в… («Локальное») можно отменить и повторить (проблема #97): папка возвращается на диске вместе с",
   "Rename Photos never overwrites another photo when only the letter case changes (issue #95): on case-sensitive": "Переименование фото никогда не перезаписывает другое фото, когда меняется только регистр букв (проблема #95): на томах с учётом регистра",
@@ -2307,7 +2307,7 @@
   "Same folder as original photo": "Та же папка, что и у оригинала",
   "Tidying the thumbnail cache": "Упорядочивание кэша миниатюр",
   "Reset all edits (Cmd+Shift+R)": "Сбросить все правки (Cmd+Shift+R)",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "Смарт-альбомы обновляются сами: измените их правила или выберите Фото → Удалить фото",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "Смарт-альбомы обновляются сами: измените их правила или выберите Фото → Удалить фото",
   "LightCraft stopped": "LightCraft остановился",
   "Focus Point": "Точка фокусировки",
   "Zoom to Focus Point": "Увеличить по точке фокусировки",
diff --git a/crates/ui-egui/locales/sv-formats.json b/crates/ui-egui/locales/sv-formats.json
--- a/crates/ui-egui/locales/sv-formats.json
+++ b/crates/ui-egui/locales/sv-formats.json
@@ -39,7 +39,7 @@
   "Added “{keyword}” to {n} photo{}": "Lade till ”{keyword}” på {n} foto{}",
   "Adding photos… {} of {}": "Lägger till foton… {} av {}",
   "All {n} selected photos move by the same amount.": "Alla {n} markerade foton flyttas lika mycket.",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Finns redan i biblioteket, i Nyligen raderade: {} foto{} markerade · högerklicka → Återställ, eller Radera permanent för att importera på nytt",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "Finns redan i biblioteket, i Nyligen raderade: {} foto{} markerade · högerklicka → Återställ, eller Radera permanent för att importera på nytt",
   "Already in the library: {} photo{} selected in All Photos": "Finns redan i biblioteket: {} foto{} markerade i Alla foton",
   "Applied {name}": "Tillämpade {name}",
   "Apply to “{mask}” on Selected Photos": "Använd på ”{mask}” i markerade foton",
@@ -91,12 +91,12 @@
   "Import cancelled · {} photo{} added": "Importen avbröts · {} foto{} tillagda",
   "Import failed: {e}": "Importen misslyckades: {e}",
   "Import warnings ({n}):\n{warnings}": "Importvarningar ({n}):\n{warnings}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "Importerade {e} maskeffekt{} (Maskering → Effekt)",
+  "Imported {e} mask effect{} (Masking → Effect)": "Importerade {e} maskeffekt{} (Maskering → Effekt)",
   "Imported {name}": "Importerade {name}",
   "Imported {n} point curve preset{}": "Importerade {n} punktkurvförinställning{}",
   "Imported {n} preset{}": "Importerade {n} förinställning{}",
   "Imported {n} preset{}, {f} file{} not readable": "Importerade {n} förinställning{}, {f} fil{} kunde inte läsas",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "Importerade {profiles} profil{} (Profilbläddrare → deras grupper)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "Importerade {profiles} profil{} (Profilbläddrare → deras grupper)",
   "Imported {} photo{}": "Importerade {} foto{}",
   "In line for AI Denoise ({ahead} ahead)…": "I kö för AI-brusreducering ({ahead} före)…",
   "Insert at the cursor (also written {})": "Infoga vid textmarkören (skrivs även {})",
@@ -153,7 +153,7 @@
   "Remove “{}” from Favorites": "Ta bort ”{}” från Favoriter",
   "Removed from {}": "Borttaget från {}",
   "Removed {what} from {name}": "Tog bort {what} från {name}",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "Tog bort ”{name}” från {n} foton. Redigera → Ångra lägger tillbaka det.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "Tog bort ”{name}” från {n} foton. Redigera → Ångra lägger tillbaka det.",
   "Rename {n} Photos…": "Byt namn på {n} foton…",
   "Rename “{name}”": "Byt namn på ”{name}”",
   "Renames {n} face{} across the library. The files are not changed.": "Byter namn på {n} ansikte{} i hela biblioteket. Filerna ändras inte.",
@@ -241,7 +241,7 @@
   "{n} photo{} now · updates automatically as photos change": "{n} foto{} just nu · uppdateras automatiskt när fotona ändras",
   "{n} preset{}": "{n} förinställning{}",
   "{n} raws can now be developed from their raw data": "{n} raw-filer kan nu framkallas från sina rådata",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "{n} likartade foto{} · Visa → Rensa filter för att se alla",
+  "{n} similar photo{} · View → Clear Filters to see all": "{n} likartade foto{} · Visa → Rensa filter för att se alla",
   "{n} tasks are still running: {names}. Quitting stops them.": "{n} uppgifter körs fortfarande: {names}. Om du avslutar stoppas de.",
   "{n} {noun}": "{n} {noun}",
   "{pct} %": "{pct} %",
diff --git a/crates/ui-egui/locales/sv.json b/crates/ui-egui/locales/sv.json
--- a/crates/ui-egui/locales/sv.json
+++ b/crates/ui-egui/locales/sv.json
@@ -1546,7 +1546,7 @@
   "Photo Merge": "Fotosammanfogning",
   "photo waiting": "foto väntar",
   "Photo ▸ Edit in External Editor (⇧⌘E) renders a 16-bit TIFF copy, stacks it with the original and opens it here (an app name on macOS, a program path elsewhere).": "Foto → Redigera i externt program (⇧⌘E) renderar en 16-bitars TIFF-kopia, staplar den med originalet och öppnar den här (ett appnamn på macOS, en programsökväg på andra system).",
-  "Photo ▸ Edit In ▸ Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "Foto → Redigera i → Redigera i PhotoCraft (⇧⌘E, eller högerklicka på ett foto) renderar en kopia med dina ändringar, staplar den med originalet och öppnar den i PhotoCraft. Spara där så uppdateras kopian här. Som PSD sparar PhotoCraft direkt tillbaka till den. Lämna platsen tom om du inte använder en portabel kopia.",
+  "Photo → Edit In → Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "Foto → Redigera i → Redigera i PhotoCraft (⇧⌘E, eller högerklicka på ett foto) renderar en kopia med dina ändringar, staplar den med originalet och öppnar den i PhotoCraft. Spara där så uppdateras kopian här. Som PSD sparar PhotoCraft direkt tillbaka till den. Lämna platsen tom om du inte använder en portabel kopia.",
   "PhotoCraft": "PhotoCraft",
   "Photos": "Foton",
   "Photos are shown through your monitor's ICC profile, so colours are right on wide-gamut and calibrated displays, and the Detail view uses the display's whole gamut. The histogram, the preview caches and exports are not affected.": "Foton visas genom bildskärmens ICC-profil, så att färgerna blir rätt på kalibrerade skärmar och skärmar med brett färgomfång, och vyn Detalj använder skärmens hela färgomfång. Histogrammet, förhandsgranskningscacharna och exporter påverkas inte.",
@@ -1757,7 +1757,7 @@
   "Remove missing photos from the library": "Ta bort saknade foton från biblioteket",
   "Remove region": "Ta bort region",
   "Remove shortcut": "Ta bort kortkommando",
-  "Remove this face box (undo with Edit ▸ Undo)": "Ta bort den här ansiktsrutan (ångra med Redigera → Ångra)",
+  "Remove this face box (undo with Edit → Undo)": "Ta bort den här ansiktsrutan (ångra med Redigera → Ångra)",
   "Remove this filter": "Ta bort det här filtret",
   "Remove this group": "Ta bort den här gruppen",
   "Remove this rule": "Ta bort den här regeln",
@@ -1826,7 +1826,7 @@
   "Review": "Granskning",
   "Review Import": "Granska import",
   "Right": "Höger",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Högerklicka på ett foto → Ange som referensfoto, sedan Visa → Referensvy (Shift+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Högerklicka på ett foto → Ange som referensfoto, sedan Visa → Referensvy (Shift+R)",
   "Right-click a preset to delete it": "Högerklicka på en förinställning för att radera den",
   "Rights Usage Terms": "Villkor för användningsrätt",
   "Rotate": "Rotera",
@@ -2044,7 +2044,7 @@
   "Smart Album Rule Fields": "Regelfält för smarta album",
   "smart album value|No": "Nej",
   "smart album value|Yes": "Ja",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "Smarta album uppdateras automatiskt: ändra deras regler eller använd Foto → Radera foto",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "Smarta album uppdateras automatiskt: ändra deras regler eller använd Foto → Radera foto",
   "Smart albums with a rule editor: match all / any / none, nested groups, 26 fields.": "Smarta album med regelredigerare: matcha alla/någon/ingen, nästlade grupper, 26 fält.",
   "Smart preview available": "Smart förhandsgranskning tillgänglig",
   "Smart Preview Status": "Status för smart förhandsgranskning",
diff --git a/crates/ui-egui/locales/uk-formats.json b/crates/ui-egui/locales/uk-formats.json
--- a/crates/ui-egui/locales/uk-formats.json
+++ b/crates/ui-egui/locales/uk-formats.json
@@ -25,7 +25,7 @@
   "Added {what} to {name}": "{what}: додано до {name}",
   "Adding photos… {} of {}": "Додавання фото… {} з {}",
   "All {n} selected photos move by the same amount.": "Усі вибрані фото ({n}) зміщуються на однакове значення.",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Уже в бібліотеці, у нещодавно видалених: вибрано фото: {}{:.0} · права кнопка → Відновити або Видалити назавжди, щоб імпортувати заново",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "Уже в бібліотеці, у нещодавно видалених: вибрано фото: {}{:.0} · права кнопка → Відновити або Видалити назавжди, щоб імпортувати заново",
   "Already in the library: {} photo{} selected in All Photos": "Уже в бібліотеці: вибрано фото в усіх фото: {}{:.0}",
   "Auto ({})": "Автоматично ({})",
   "Auto Import: added {} photo{}": "Автоімпорт: додано фото: {}{:.0}",
@@ -65,7 +65,7 @@
   "Imported {n} point curve preset{}": "Імпортовано наборів налаштувань точкової кривої: {n}{:.0}",
   "Imported {n} preset{}": "Імпортовано наборів налаштувань: {n}{:.0}",
   "Imported {n} preset{}, {f} file{} not readable": "Імпортовано наборів налаштувань: {n}{:.0}, файлів не читається: {f}{:.0}",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "Імпортовано профілів: {profiles}{:.0} (Оглядач профілів → їхні групи)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "Імпортовано профілів: {profiles}{:.0} (Оглядач профілів → їхні групи)",
   "The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.": "Не вдалося записати XMP-файли {n} фото (папка недоступна?). LightCraft продовжує спроби.",
   "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "Профіль XMP «{profile}» не застосовано до {path}; кольори можуть відрізнятися від Lightroom.",
   "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "Геометрію Lightroom для {path} не збережено; режим {mode} буде перераховано.",
@@ -195,7 +195,7 @@
   "… and {more} more": "… і ще {more}",
   "LightCraft can't decode this raw variant yet ({}). You're editing the camera's embedded JPEG preview, which already includes the camera's picture style (e.g. Monochrome) and white balance.": "LightCraft ще не може декодувати цей варіант RAW ({}). Ви редагуєте вбудований JPEG-перегляд камери, який уже містить її стиль зображення (наприклад, монохромний) і баланс білого.",
   "Filtered: {label}": "Відфільтровано: {label}",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "Подібних фото: {n}{:.0} · Вигляд → Скинути фільтри, щоб побачити всі",
+  "{n} similar photo{} · View → Clear Filters to see all": "Подібних фото: {n}{:.0} · Вигляд → Скинути фільтри, щоб побачити всі",
   "Moved; {} photo(s) relinked": "Переміщено; повторно пов’язано фото: {}",
   "{} label (click more labels to show any of them)": "Мітка «{}» (клацніть інші мітки, щоб показати фото з будь-якою з них)",
   ", {} star{}": ", зірок: {}{:.0}",
@@ -242,7 +242,7 @@
   "Create the keyword “{name}”": "Створити ключове слово «{name}»",
   "new, inside {parent}": "нове, всередині {parent}",
   "Keywords of {n} selected photos": "Ключові слова вибраних фото: {n}",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "«{name}» вилучено з фото: {n}. Редагування → Скасувати поверне його.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "«{name}» вилучено з фото: {n}. Редагування → Скасувати поверне його.",
   "1 task is still running: {names}. Quitting stops it.": "1 завдання ще виконується: {names}. Вихід зупинить його.",
   "{n} tasks are still running: {names}. Quitting stops them.": "Ще виконуються завдання ({n}): {names}. Вихід зупинить їх.",
   "{what} merge cancelled": "Об’єднання {what} скасовано",
@@ -262,7 +262,7 @@
   "Delete {n} effect preset{}? This can't be undone.": "Видалити пресети ефектів ({n})?{:.0} Це не можна скасувати.",
   "Effect: {name}": "Ефект: {name}",
   "Exported {} effect preset{}": "Експортовано пресетів ефектів: {}{:.0}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "Імпортовано ефектів маски: {e}{:.0} (Маски → Ефект)",
+  "Imported {e} mask effect{} (Masking → Effect)": "Імпортовано ефектів маски: {e}{:.0} (Маски → Ефект)",
   "Update “{name}” with Current Settings": "Оновити «{name}» поточними налаштуваннями",
   "{n} preset{}": "Пресетів: {n}{:.0}",
   "{name} opened in the other editor": "{name} відкрито в іншому редакторі",
diff --git a/crates/ui-egui/locales/uk.json b/crates/ui-egui/locales/uk.json
--- a/crates/ui-egui/locales/uk.json
+++ b/crates/ui-egui/locales/uk.json
@@ -930,7 +930,7 @@
   "Remove a filter above, or choose Clear all": "Видаліть фільтр вище або виберіть «Очистити все»",
   "Remove from Target Album": "Вилучити з цільового альбому",
   "Remove shortcut": "Видалити скорочення",
-  "Remove this face box (undo with Edit ▸ Undo)": "Видалити цю рамку обличчя (скасувати через Редагування → Скасувати)",
+  "Remove this face box (undo with Edit → Undo)": "Видалити цю рамку обличчя (скасувати через Редагування → Скасувати)",
   "Rename Component": "Перейменувати компонент",
   "Rename Folder": "Перейменувати теку",
   "Rename Mask": "Перейменувати маску",
@@ -1273,7 +1273,7 @@
   "Restore them": "Відновити їх",
   "Retouch": "Ретушування",
   "Review Import": "Перевірити імпорт",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Клацніть фото правою кнопкою → Задати як еталонне фото, потім Вигляд → Еталонне подання (Shift+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Клацніть фото правою кнопкою → Задати як еталонне фото, потім Вигляд → Еталонне подання (Shift+R)",
   "Right-click a preset to delete it": "Клацніть набір налаштувань правою кнопкою, щоб видалити його",
   "Rights Usage Terms": "Умови використання прав",
   "Rotate Crop": "Обертати кадрування",
@@ -2312,7 +2312,7 @@
   "Same folder as original photo": "Та сама папка, що й оригінал",
   "Tidying the thumbnail cache": "Упорядкування кешу мініатюр",
   "Reset all edits (Cmd+Shift+R)": "Скинути всі зміни (Cmd+Shift+R)",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "Розумні альбоми оновлюються самі: змініть їхні правила або виберіть Фото → Видалити фото",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "Розумні альбоми оновлюються самі: змініть їхні правила або виберіть Фото → Видалити фото",
   "LightCraft stopped": "LightCraft зупинився",
   "Focus Point": "Точка фокусування",
   "Zoom to Focus Point": "Збільшити до точки фокусування",
diff --git a/crates/ui-egui/locales/vi-formats.json b/crates/ui-egui/locales/vi-formats.json
--- a/crates/ui-egui/locales/vi-formats.json
+++ b/crates/ui-egui/locales/vi-formats.json
@@ -39,7 +39,7 @@
   "Added “{keyword}” to {n} photo{}": "Đã thêm «{keyword}» vào {n} ảnh{:.0}",
   "Adding photos… {} of {}": "Đang thêm ảnh… {} / {}",
   "All {n} selected photos move by the same amount.": "Tất cả {n} ảnh đã chọn dịch chuyển cùng một lượng.",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Đã có trong thư viện, trong Đã xóa gần đây: đã chọn {} ảnh{:.0} · nhấp chuột phải → Khôi phục, hoặc Xóa vĩnh viễn để nhập lại",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "Đã có trong thư viện, trong Đã xóa gần đây: đã chọn {} ảnh{:.0} · nhấp chuột phải → Khôi phục, hoặc Xóa vĩnh viễn để nhập lại",
   "Already in the library: {} photo{} selected in All Photos": "Đã có trong thư viện: đã chọn {} ảnh{:.0} trong Tất cả ảnh",
   "Already in “{}”": "Already in “{}”",
   "Applied {name}": "Applied {name}",
@@ -93,12 +93,12 @@
   "Import cancelled · {} photo{} added": "Đã hủy nhập · đã thêm {} ảnh{:.0}",
   "Import failed: {e}": "Nhập thất bại: {e}",
   "Import warnings ({n}):\n{warnings}": "Cảnh báo khi nhập ({n}):\n{warnings}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "Imported {e} mask effect{} (Masking → Effect)",
+  "Imported {e} mask effect{} (Masking → Effect)": "Imported {e} mask effect{} (Masking → Effect)",
   "Imported {name}": "Imported {name}",
   "Imported {n} point curve preset{}": "Đã nhập {n} thiết lập sẵn đường cong điểm{:.0}",
   "Imported {n} preset{}": "Đã nhập {n} thiết lập sẵn{:.0}",
   "Imported {n} preset{}, {f} file{} not readable": "Đã nhập {n} thiết lập sẵn{:.0}, {f} tệp{:.0} không đọc được",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "Đã nhập {profiles} hồ sơ{:.0} (Trình duyệt hồ sơ → nhóm của chúng)",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "Đã nhập {profiles} hồ sơ{:.0} (Trình duyệt hồ sơ → nhóm của chúng)",
   "Imported {} photo{}": "Đã nhập {} ảnh{:.0}",
   "In line for AI Denoise ({ahead} ahead)…": "Đang xếp hàng Khử nhiễu AI (trước bạn: {ahead})…",
   "Insert at the cursor (also written {})": "Chèn tại con trỏ (cũng ghi là {})",
@@ -157,7 +157,7 @@
   "Remove “{}” from Favorites": "Gỡ «{}» khỏi Yêu thích",
   "Removed from {}": "Đã gỡ khỏi {}",
   "Removed {what} from {name}": "Đã gỡ {what} khỏi {name}",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "Removed “{name}” from {n} photos. Edit → Undo puts it back.",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "Removed “{name}” from {n} photos. Edit → Undo puts it back.",
   "Rename {n} Photos…": "Đổi tên {n} ảnh…",
   "Rename “{name}”": "Đổi tên «{name}»",
   "Renames {n} face{} across the library. The files are not changed.": "Renames {n} face{} across the library. The files are not changed.",
@@ -249,7 +249,7 @@
   "{n} preset{}": "{n} preset{}",
   "{n} raws can now be developed from their raw data": "{n} raws can now be developed from their raw data",
   "{n} selected photos · {pages} pages": "{n} selected photos · {pages} pages",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "{n} ảnh tương tự{:.0} · Xem → Xóa bộ lọc để thấy tất cả",
+  "{n} similar photo{} · View → Clear Filters to see all": "{n} ảnh tương tự{:.0} · Xem → Xóa bộ lọc để thấy tất cả",
   "{n} tasks are still running: {names}. Quitting stops them.": "Còn {n} tác vụ đang chạy: {names}. Thoát sẽ dừng chúng.",
   "{n} {noun}": "{noun}: {n}",
   "{pct} %": "{pct} %",
diff --git a/crates/ui-egui/locales/vi.json b/crates/ui-egui/locales/vi.json
--- a/crates/ui-egui/locales/vi.json
+++ b/crates/ui-egui/locales/vi.json
@@ -1291,7 +1291,7 @@
   "Photo Grid (G)": "Lưới ảnh (G)",
   "Photo Merge": "Ghép ảnh",
   "Photo menu. The filmstrip has the photo context menu too. Adding a file again that is in Recently Deleted no": "Menu Ảnh. Dải phim cũng có menu ngữ cảnh ảnh. Thêm lại tệp đang ở Đã xóa gần đây không",
-  "Photo ▸ Edit In ▸ Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "Ảnh → Chỉnh trong → Chỉnh trong PhotoCraft (⇧⌘E, hoặc nhấp phải ảnh) kết xuất bản sao kèm chỉnh sửa, xếp chồng với bản gốc và mở trong PhotoCraft. Lưu ở đó thì bản sao cập nhật tại đây. Dạng PSD, PhotoCraft lưu thẳng lại. Để trống vị trí trừ khi dùng bản portable.",
+  "Photo → Edit In → Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.": "Ảnh → Chỉnh trong → Chỉnh trong PhotoCraft (⇧⌘E, hoặc nhấp phải ảnh) kết xuất bản sao kèm chỉnh sửa, xếp chồng với bản gốc và mở trong PhotoCraft. Lưu ở đó thì bản sao cập nhật tại đây. Dạng PSD, PhotoCraft lưu thẳng lại. Để trống vị trí trừ khi dùng bản portable.",
   "Photo ▸ Edit in External Editor (⇧⌘E) renders a 16-bit TIFF copy, stacks it with the original and opens it here (an app name on macOS, a program path elsewhere).": "Ảnh → Chỉnh trong trình soạn ngoài (⇧⌘E) kết xuất bản sao TIFF 16 bit, xếp chồng với bản gốc và mở tại đây (tên app trên macOS, đường dẫn chương trình ở nơi khác).",
   "PhotoCraft": "PhotoCraft",
   "Photos": "Ảnh",
@@ -1484,7 +1484,7 @@
   "Remove location info": "Xóa thông tin vị trí",
   "Remove missing photos from the library": "Gỡ ảnh mất khỏi thư viện",
   "Remove shortcut": "Gỡ lối tắt",
-  "Remove this face box (undo with Edit ▸ Undo)": "Gỡ khung khuôn mặt này (hoàn tác bằng Chỉnh sửa → Hoàn tác)",
+  "Remove this face box (undo with Edit → Undo)": "Gỡ khung khuôn mặt này (hoàn tác bằng Chỉnh sửa → Hoàn tác)",
   "Remove this filter": "Gỡ bộ lọc này",
   "Remove this group": "Gỡ nhóm này",
   "Remove this rule": "Gỡ quy tắc này",
@@ -1543,7 +1543,7 @@
   "Revert": "Hoàn tác",
   "Review": "Xem lại",
   "Review Import": "Xem lại nhập",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Nhấp phải ảnh → Đặt làm ảnh tham chiếu, rồi Xem → Chế độ tham chiếu (Shift+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Nhấp phải ảnh → Đặt làm ảnh tham chiếu, rồi Xem → Chế độ tham chiếu (Shift+R)",
   "Right-click a preset to delete it": "Nhấp phải thiết lập sẵn để xóa",
   "Rights Usage Terms": "Điều khoản sử dụng quyền",
   "Rotate": "Xoay",
diff --git a/crates/ui-egui/locales/zh-hans-formats.json b/crates/ui-egui/locales/zh-hans-formats.json
--- a/crates/ui-egui/locales/zh-hans-formats.json
+++ b/crates/ui-egui/locales/zh-hans-formats.json
@@ -120,7 +120,7 @@
   "Adding photos… {} of {}": "正在添加照片… 第 {0} 张，共 {1} 张",
   " ({} in Recently Deleted)": "（其中 {0} 张在“最近删除”中）",
   "Already in the library: {} photo{} selected in All Photos": "已在图库中：已在“所有照片”中选择 {0} 张照片{1:.0}",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "已在图库的“最近删除”中：已选择 {0} 张照片{1:.0} · 右键单击 → 恢复，或永久删除后重新导入",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "已在图库的“最近删除”中：已选择 {0} 张照片{1:.0} · 右键单击 → 恢复，或永久删除后重新导入",
   "Auto ({})": "自动（{}）",
   "Build 1:1 Previews ({scope})": "生成 1:1 预览（{scope}）",
   "Build Smart Previews ({scope})": "生成智能预览（{scope}）",
@@ -169,7 +169,7 @@
   "Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "未保留 {path} 的 Lightroom 几何信息；不支持保存的引导 Upright 变换。",
   "Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "未保留 {path} 的 Lightroom 几何信息；不支持保存的 Upright 变换。",
   "XMP adjustments were not imported for {path}: {fields}.": "未导入 {path} 的 XMP 调整：{fields}。",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "已导入 {profiles} 个配置文件{0:.0}（配置文件浏览器 → 各自的分组）",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "已导入 {profiles} 个配置文件{0:.0}（配置文件浏览器 → 各自的分组）",
   "Insert at the cursor (also written {})": "在光标处插入（也可写作 {0}）",
   "MIT OR Apache-2.0. Fonts: {} (OFL). Icons: original.": "MIT OR Apache-2.0。字体：{0}（OFL）。图标：原创。",
   "Painting “{k}”: click photos to add or remove it · Esc stops": "正在应用“{k}”：单击照片以添加或移除 · 按 Esc 停止",
@@ -199,7 +199,7 @@
   "{} — show their photos": "{0} — 显示其照片",
   "LightCraft can't decode this raw variant yet ({}). You're editing the camera's embedded JPEG preview, which already includes the camera's picture style (e.g. Monochrome) and white balance.": "LightCraft 暂时无法解码这种 RAW 变体（{0}）。你正在编辑相机内嵌的 JPEG 预览，其中已包含相机的照片风格（如单色）和白平衡。",
   "Filtered: {label}": "已筛选：{label}",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "{n} 张相似照片{0:.0} · 选择“视图 → 清除过滤器”可查看全部",
+  "{n} similar photo{} · View → Clear Filters to see all": "{n} 张相似照片{0:.0} · 选择“视图 → 清除过滤器”可查看全部",
   "Moved; {} photo(s) relinked": "已移动；已重新链接 {0} 张照片",
   "{} label (click more labels to show any of them)": "{0}标签（单击更多标签可显示符合其中任一标签的照片）",
   ", {} star{}": "，{0} 星{1:.0}",
@@ -246,7 +246,7 @@
   "Create the keyword “{name}”": "创建关键字“{name}”",
   "new, inside {parent}": "新关键字，位于 {parent} 内",
   "Keywords of {n} selected photos": "所选 {n} 张照片的关键字",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "已从 {n} 张照片中移除“{name}”。编辑 → 撤销可恢复。",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "已从 {n} 张照片中移除“{name}”。编辑 → 撤销可恢复。",
   "1 task is still running: {names}. Quitting stops it.": "仍有 1 个任务在运行：{names}。退出将停止它。",
   "{n} tasks are still running: {names}. Quitting stops them.": "仍有 {n} 个任务在运行：{names}。退出将停止它们。",
   "{what} merge cancelled": "已取消 {what} 合并",
@@ -266,7 +266,7 @@
   "Delete {n} effect preset{}? This can't be undone.": "删除 {n} 个效果预设？{:.0}此操作无法撤销。",
   "Effect: {name}": "效果：{name}",
   "Exported {} effect preset{}": "已导出 {} 个效果预设{:.0}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "已导入 {e} 个蒙版效果{:.0}（蒙版 → 效果）",
+  "Imported {e} mask effect{} (Masking → Effect)": "已导入 {e} 个蒙版效果{:.0}（蒙版 → 效果）",
   "Update “{name}” with Current Settings": "用当前设置更新“{name}”",
   "{n} preset{}": "{n} 个预设{:.0}",
   "{name} opened in the other editor": "已在其他编辑器中打开 {name}",
diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json
--- a/crates/ui-egui/locales/zh-hans.json
+++ b/crates/ui-egui/locales/zh-hans.json
@@ -662,7 +662,7 @@
   "Remove a filter above, or choose Clear all": "请移除上方的某个筛选条件，或选择“全部清除”",
   "Remove from Target Album": "从目标相册中移除",
   "Remove shortcut": "移除快捷键",
-  "Remove this face box (undo with Edit ▸ Undo)": "移除此人脸框（可通过“编辑 → 撤销”恢复）",
+  "Remove this face box (undo with Edit → Undo)": "移除此人脸框（可通过“编辑 → 撤销”恢复）",
   "Rename Component": "重命名组件",
   "Reorder Masks": "重新排序蒙版",
   "Reset All": "全部重置",
@@ -1237,7 +1237,7 @@
   "Revert": "恢复",
   "Review": "审阅",
   "Review Import": "查看导入内容",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "右键单击照片 » 设为参考照片，然后选择“视图 → 参考视图”(Shift+R)",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "右键单击照片 » 设为参考照片，然后选择“视图 → 参考视图”(Shift+R)",
   "Right-click a preset to delete it": "右键单击预设即可将其删除",
   "Rights Usage Terms": "使用条款",
   "Rotate": "旋转",
@@ -2147,7 +2147,7 @@
   "Same folder as original photo": "与原始照片相同的文件夹",
   "Tidying the thumbnail cache": "正在整理缩略图缓存",
   "Reset all edits (Cmd+Shift+R)": "重置所有编辑 (Cmd+Shift+R)",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "智能相册会自动更新：请更改其规则，或使用“照片 → 删除照片”",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "智能相册会自动更新：请更改其规则，或使用“照片 → 删除照片”",
   "LightCraft stopped": "LightCraft 已停止",
   "Zoom to Focus Point": "缩放至对焦点",
   "Focus": "对焦",
diff --git a/crates/ui-egui/locales/zh-hant-formats.json b/crates/ui-egui/locales/zh-hant-formats.json
--- a/crates/ui-egui/locales/zh-hant-formats.json
+++ b/crates/ui-egui/locales/zh-hant-formats.json
@@ -141,7 +141,7 @@
   "… and {more} more": "…以及另外 {more} 個",
   " ({} in Recently Deleted)": "（其中 {} 張在「最近刪除」中）",
   "Already in the library: {} photo{} selected in All Photos": "已在照片庫中：已在「所有照片」中選取 {} 張照片{:.0}",
-  "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "已在照片庫的「最近刪除」中：已選取 {} 張照片{:.0} · 按右鍵 → 還原，或永久刪除後重新匯入",
+  "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh": "已在照片庫的「最近刪除」中：已選取 {} 張照片{:.0} · 按右鍵 → 還原，或永久刪除後重新匯入",
   "Remove “{name}” and its {count} photo{} from the library?": "要將「{name}」及其 {count} 張照片從照片庫移除嗎？{:.0}",
   " ({unsure} with several look-alike files: use Locate)": "（其中 {unsure} 張有多個相似檔案：請使用「尋找」）",
   " · {:.0}% clipped": " · {:.0}% 剪裁",
@@ -169,7 +169,7 @@
   "Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "未保留 {path} 的 Lightroom 幾何資料；不支援儲存的引導 Upright 轉換。",
   "Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "未保留 {path} 的 Lightroom 幾何資料；不支援儲存的 Upright 轉換。",
   "XMP adjustments were not imported for {path}: {fields}.": "未匯入 {path} 的 XMP 調整：{fields}。",
-  "Imported {profiles} profile{} (Profile browser ▸ their groups)": "已匯入 {profiles} 個描述檔{:.0}（描述檔瀏覽器 → 各自的群組）",
+  "Imported {profiles} profile{} (Profile browser → their groups)": "已匯入 {profiles} 個描述檔{:.0}（描述檔瀏覽器 → 各自的群組）",
   "Insert at the cursor (also written {})": "在游標處插入（也可寫成 {}）",
   "MIT OR Apache-2.0. Fonts: {} (OFL). Icons: original.": "MIT OR Apache-2.0。字型：{}（OFL）。圖示：原創。",
   "Painting “{k}”: click photos to add or remove it · Esc stops": "正在套用「{k}」：點擊照片即可加入或移除 · 按 Esc 停止",
@@ -199,7 +199,7 @@
   "{} — show their photos": "{} — 顯示其照片",
   "LightCraft can't decode this raw variant yet ({}). You're editing the camera's embedded JPEG preview, which already includes the camera's picture style (e.g. Monochrome) and white balance.": "LightCraft 目前還無法解碼這種 RAW 變體（{}）。你正在編輯相機內嵌的 JPEG 預覽，其中已包含相機的相片風格（例如單色）與白平衡。",
   "Filtered: {label}": "已篩選：{label}",
-  "{n} similar photo{} · View ▸ Clear Filters to see all": "{n} 張相似照片{:.0} · 選擇「檢視 → 清除篩選條件」可查看全部",
+  "{n} similar photo{} · View → Clear Filters to see all": "{n} 張相似照片{:.0} · 選擇「檢視 → 清除篩選條件」可查看全部",
   "Moved; {} photo(s) relinked": "已移動；已重新連結 {} 張照片",
   "{} label (click more labels to show any of them)": "{}標籤（點擊更多標籤可顯示符合其中任一標籤的照片）",
   ", {} star{}": "，{} 顆星{:.0}",
@@ -246,7 +246,7 @@
   "Create the keyword “{name}”": "建立關鍵字「{name}」",
   "new, inside {parent}": "新關鍵字，位於 {parent} 內",
   "Keywords of {n} selected photos": "選取的 {n} 張照片的關鍵字",
-  "Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "已從 {n} 張照片移除「{name}」。編輯 → 復原可恢復。",
+  "Removed “{name}” from {n} photos. Edit → Undo puts it back.": "已從 {n} 張照片移除「{name}」。編輯 → 復原可恢復。",
   "1 task is still running: {names}. Quitting stops it.": "仍有 1 個工作在執行：{names}。結束將停止它。",
   "{n} tasks are still running: {names}. Quitting stops them.": "仍有 {n} 個工作在執行：{names}。結束將停止它們。",
   "{what} merge cancelled": "已取消{what}合併",
@@ -266,7 +266,7 @@
   "Delete {n} effect preset{}? This can't be undone.": "刪除 {n} 個效果預設集？{:.0}此動作無法復原。",
   "Effect: {name}": "效果：{name}",
   "Exported {} effect preset{}": "已匯出 {} 個效果預設集{:.0}",
-  "Imported {e} mask effect{} (Masking ▸ Effect)": "已匯入 {e} 個遮色片效果{:.0}（遮色片 → 效果）",
+  "Imported {e} mask effect{} (Masking → Effect)": "已匯入 {e} 個遮色片效果{:.0}（遮色片 → 效果）",
   "Update “{name}” with Current Settings": "以目前設定更新「{name}」",
   "{n} preset{}": "{n} 個預設集{:.0}",
   "{name} opened in the other editor": "已在其他編輯器中開啟 {name}",
diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json
--- a/crates/ui-egui/locales/zh-hant.json
+++ b/crates/ui-egui/locales/zh-hant.json
@@ -663,7 +663,7 @@
   "Remove a filter above, or choose Clear all": "請移除上方的某個篩選條件，或選擇「全部清除」",
   "Remove from Target Album": "從目標相簿移除",
   "Remove shortcut": "移除快速鍵",
-  "Remove this face box (undo with Edit ▸ Undo)": "移除此臉部框（可用「編輯 → 復原」還原）",
+  "Remove this face box (undo with Edit → Undo)": "移除此臉部框（可用「編輯 → 復原」還原）",
   "Rename Component": "重新命名元件",
   "Reorder Masks": "重新排序遮色片",
   "Reset All": "全部重設",
@@ -1238,7 +1238,7 @@
   "Revert": "回復至上次儲存",
   "Review": "審閱",
   "Review Import": "檢視匯入",
-  "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "在照片上按右鍵 → 設為參考照片，再選擇「檢視 → 參考檢視」（Shift+R）",
+  "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "在照片上按右鍵 → 設為參考照片，再選擇「檢視 → 參考檢視」（Shift+R）",
   "Right-click a preset to delete it": "在預設集上按右鍵即可刪除",
   "Rights Usage Terms": "使用權利條款",
   "Rotate": "旋轉",
@@ -2115,7 +2115,7 @@
   "Same folder as original photo": "與原始照片相同的資料夾",
   "Tidying the thumbnail cache": "正在整理縮圖快取",
   "Reset all edits (Cmd+Shift+R)": "重設所有編輯（Cmd+Shift+R）",
-  "Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo": "智慧型相簿會自動更新：請變更其規則，或使用「照片 → 刪除照片」",
+  "Smart albums update automatically: change their rules, or use Photo → Delete Photo": "智慧型相簿會自動更新：請變更其規則，或使用「照片 → 刪除照片」",
   "LightCraft stopped": "LightCraft 已停止",
   "Focus Point": "對焦點",
   "Zoom to Focus Point": "縮放至對焦點",
diff --git a/crates/ui-egui/src/dialogs/import/task.rs b/crates/ui-egui/src/dialogs/import/task.rs
--- a/crates/ui-egui/src/dialogs/import/task.rs
+++ b/crates/ui-egui/src/dialogs/import/task.rs
@@ -534,7 +534,7 @@ fn show_existing(app: &mut LightcraftApp, ctx: &egui::Context, existing: &[u64])
         crate::i18n::tr_format!("Already in the library: {} photo{} selected in All Photos", n, plural)
     } else {
         crate::i18n::tr_format!(
-            "Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh",
+            "Already in the library, in Recently Deleted: {} photo{} selected · right-click → Restore, or Delete Permanently to import afresh",
             n,
             plural
         )
diff --git a/crates/ui-egui/src/dialogs/rules_editor/rules_move.rs b/crates/ui-egui/src/dialogs/rules_editor/rules_move.rs
--- a/crates/ui-egui/src/dialogs/rules_editor/rules_move.rs
+++ b/crates/ui-egui/src/dialogs/rules_editor/rules_move.rs
@@ -154,7 +154,7 @@ pub fn move_node(rs: &mut RuleSet, from: &[usize], dest: &[usize]) -> Option<Vec
     }
     let mut chain = from.get(..from.len().checked_sub(1)?)?.to_vec();
     // (a group the row is going into stays, however empty it is for now)
-    while !chain.is_empty() && !(dest.len() > chain.len() && dest.starts_with(&chain)) && list_at(&next, &chain).is_some_and(|l| l.rules.is_empty()) {
+    while !(chain.is_empty() || dest.len() > chain.len() && dest.starts_with(&chain)) && list_at(&next, &chain).is_some_and(|l| l.rules.is_empty()) {
         remove_at(&mut next, &chain)?;
         if !shift_after_removal(&mut dest, &chain) {
             return None;
diff --git a/crates/ui-egui/src/dialogs/settings/general.rs b/crates/ui-egui/src/dialogs/settings/general.rs
--- a/crates/ui-egui/src/dialogs/settings/general.rs
+++ b/crates/ui-egui/src/dialogs/settings/general.rs
@@ -3,6 +3,7 @@ use super::*;
 
 // ------------------------------------------------------------------------------------- General
 
+/// Draw the General settings tab, including language and library-location controls.
 pub(super) fn general_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
     row(ui, t, crate::i18n::tr("Language"), |ui| {
         let languages: Vec<_> = crate::i18n::Locale::for_picker().into_iter().map(|language| (language, language.name())).collect();
@@ -87,7 +88,7 @@ pub(super) fn general_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens
         ui,
         t,
         crate::i18n::tr(
-            "Photo ▸ Edit In ▸ Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.",
+            "Photo → Edit In → Edit in PhotoCraft (⇧⌘E, or right-click a photo) renders a copy with your edits, stacks it with the original and opens it in PhotoCraft. Save there and the copy updates here. As PSD, PhotoCraft saves straight back to it. Leave the location empty unless you use a portable copy.",
         ),
     );
     row(ui, t, crate::i18n::tr("Other editor"), |ui| {
diff --git a/crates/ui-egui/src/dialogs/settings/libraries.rs b/crates/ui-egui/src/dialogs/settings/libraries.rs
--- a/crates/ui-egui/src/dialogs/settings/libraries.rs
+++ b/crates/ui-egui/src/dialogs/settings/libraries.rs
@@ -160,7 +160,7 @@ pub fn new_library(app: &mut LightcraftApp, p: &Value) -> Result<Value, String>
         None => location,
     };
     if lightcraft_engine::library::is_library(&dir) {
-        return Err(format!("{}: there is a library here already; open it with File ▸ Open Library…", dir.display()));
+        return Err(format!("{}: there is a library here already; open it with File → Open Library…", dir.display()));
     }
     if dir.is_file() {
         return Err(format!("{}: is a file; a library is a folder", dir.display()));
diff --git a/crates/ui-egui/src/menus/files.rs b/crates/ui-egui/src/menus/files.rs
--- a/crates/ui-egui/src/menus/files.rs
+++ b/crates/ui-egui/src/menus/files.rs
@@ -8,7 +8,7 @@ pub fn delete_in_album(app: &mut LightcraftApp, ctx: &egui::Context) -> bool {
     let lightcraft_engine::LibrarySource::Album(aid) = app.session.source else { return false };
     let Some(album) = app.session.catalog.album(aid) else { return false };
     if album.is_smart() {
-        app.toast(ctx, crate::i18n::tr("Smart albums update automatically: change their rules, or use Photo ▸ Delete Photo"));
+        app.toast(ctx, crate::i18n::tr("Smart albums update automatically: change their rules, or use Photo → Delete Photo"));
         return true;
     }
     if app.session.selected().is_empty() {
diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs
--- a/crates/ui-egui/src/menus/run.rs
+++ b/crates/ui-egui/src/menus/run.rs
@@ -1678,7 +1678,7 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
                         let profiles = v["imported"].as_array().map_or(0, Vec::len);
                         if profiles > 0 {
                             parts.push(crate::i18n::tr_format!(
-                                "Imported {profiles} profile{} (Profile browser ▸ their groups)",
+                                "Imported {profiles} profile{} (Profile browser → their groups)",
                                 if profiles == 1 { "" } else { "s" },
                                 profiles = profiles
                             ));
diff --git a/crates/ui-egui/src/panels/compare.rs b/crates/ui-egui/src/panels/compare.rs
--- a/crates/ui-egui/src/panels/compare.rs
+++ b/crates/ui-egui/src/panels/compare.rs
@@ -601,7 +601,7 @@ pub fn show_reference(app: &mut LightcraftApp, ui: &mut egui::Ui) {
     let canvas = area_and_filmstrip(app, ui);
     let reference = app.ui.reference.map(PhotoId).filter(|r| live(app, *r));
     let (Some(r), Some(active)) = (reference, app.session.active()) else {
-        super::empty_message(ui, canvas, "No reference photo", "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)");
+        super::empty_message(ui, canvas, "No reference photo", "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)");
         return;
     };
     let area = canvas.shrink(18.0);
diff --git a/crates/ui-egui/src/panels/detail/masks.rs b/crates/ui-egui/src/panels/detail/masks.rs
--- a/crates/ui-egui/src/panels/detail/masks.rs
+++ b/crates/ui-egui/src/panels/detail/masks.rs
@@ -113,7 +113,7 @@ pub(super) fn region_overlay(
             let cross = Stroke::new(1.4, if resp.hovered() { Color32::WHITE } else { Color32::from_gray(210) });
             p.line_segment([c - vec2(m, m), c + vec2(m, m)], cross);
             p.line_segment([c - vec2(m, -m), c + vec2(m, -m)], cross);
-            if resp.on_hover_text(crate::i18n::tr("Remove this face box (undo with Edit ▸ Undo)")).clicked() {
+            if resp.on_hover_text(crate::i18n::tr("Remove this face box (undo with Edit → Undo)")).clicked() {
                 edit = Some(RegionEdit::Remove(index));
             }
         }
diff --git a/crates/ui-egui/src/panels/grid.rs b/crates/ui-egui/src/panels/grid.rs
--- a/crates/ui-egui/src/panels/grid.rs
+++ b/crates/ui-egui/src/panels/grid.rs
@@ -1214,6 +1214,7 @@ pub fn toggle_stack(app: &mut LightcraftApp, ctx: &egui::Context, id: PhotoId) {
     }
 }
 
+/// Draw the photo's grid context menu and dispatch the selected action.
 pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
     if !app.session.selection.contains(id) {
         let _ = app.run("library.select", json!({"ids": [id.0]}));
@@ -1250,7 +1251,7 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
                 app.ui.view = crate::state::ViewMode::PhotoGrid;
                 app.toast(
                     ui.ctx(),
-                    crate::i18n::tr_format!("{n} similar photo{} · View ▸ Clear Filters to see all", if n == 1 { "" } else { "s" }, n = n),
+                    crate::i18n::tr_format!("{n} similar photo{} · View → Clear Filters to see all", if n == 1 { "" } else { "s" }, n = n),
                 );
             }
             Err(e) => app.toast(ui.ctx(), e),
diff --git a/crates/ui-egui/src/panels/keywording.rs b/crates/ui-egui/src/panels/keywording.rs
--- a/crates/ui-egui/src/panels/keywording.rs
+++ b/crates/ui-egui/src/panels/keywording.rs
@@ -134,7 +134,7 @@ fn remove(app: &mut LightcraftApp, ctx: &egui::Context, chip: &Chip) {
             let name = chip.path.replace('|', " › ");
             app.toast_for(
                 ctx,
-                crate::i18n::tr_format!("Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.", name = name, n = chip.have),
+                crate::i18n::tr_format!("Removed “{name}” from {n} photos. Edit → Undo puts it back.", name = name, n = chip.have),
                 6.0,
             );
         }
diff --git a/crates/ui-egui/src/panels/mask_effects.rs b/crates/ui-egui/src/panels/mask_effects.rs
--- a/crates/ui-egui/src/panels/mask_effects.rs
+++ b/crates/ui-egui/src/panels/mask_effects.rs
@@ -688,7 +688,7 @@ pub fn manage_body(
 pub fn import_toast(v: &Value) -> String {
     let rows: Vec<&Value> = v["effects"].as_array().or_else(|| v["imported"].as_array()).into_iter().flatten().collect();
     let n = rows.len();
-    let mut msg = crate::i18n::tr_format!("Imported {e} mask effect{} (Masking ▸ Effect)", if n == 1 { "" } else { "s" }, e = n);
+    let mut msg = crate::i18n::tr_format!("Imported {e} mask effect{} (Masking → Effect)", if n == 1 { "" } else { "s" }, e = n);
     let mut groups: Vec<&str> = rows.iter().filter_map(|r| r["group"].as_str()).filter(|g| !g.is_empty()).collect();
     groups.sort_unstable();
     groups.dedup();
diff --git a/docs/camera-preview-colour.md b/docs/camera-preview-colour.md
--- a/docs/camera-preview-colour.md
+++ b/docs/camera-preview-colour.md
@@ -98,9 +98,18 @@ These are camera-JPEG look estimates, **not measured spectral calibration or Lig
 
 ### Canon CR3
 
-CR3 uses the same colour gates and relative WB. Its sensor and JPEG proxies are first oriented identically. Canon can crop its JPEG differently from the sensor; no lens distortion correction is applied to the sensor proxy, so a strongly distortion-corrected JPEG can fail the gates. A fixed 96-pixel proxy measures this remaining framing from luminance-gradient directions: scale is limited to 0.9–1.1 and translation to two proxy pixels. At least 192 edges are required; two thirds train the framing and the remaining third must independently reach direction agreement ≥0.90 with an improvement ≥0.04. At least 80% of each edge set must remain usable. Weak, flat, unrelated or already aligned references keep the sensor framing. This alignment affects training correspondences only; the RAW render still uses the actual sensor pixels.
-
-On the public CC0 EOS R100 / RF-S 18–45 mm sample, the measured framing scale was 0.968. Held-out edge agreement rose from 0.7291 to 0.9194, and the unchanged colour gates accepted the fit (held-out RMS 0.10625 → 0.07704). The corpus test asserts acceptance after decoding the sensor RAW, and synthetic tests cover nonlinear camera tone, known crop/translation and unrelated references. The matrix, tone and hue/saturation table are estimated from the file; no third-party camera matrix or decoder calibration source is copied.
+For CR3 with a fallback camera matrix and no in-file geometric warp, a resolved lens
+profile can also propose the correspondence geometry. It must pass the existing
+training/held-out direction gates and must not reduce agreement against the established
+framing fallback on either split. Sensor, clipping mask and JPEG share orientation;
+white balance is applied once. Rejected or missing profiles retain the framing path.
+This changes calibration pairs, not the optics baked into the rendered RAW, and does
+not enable profile-based correspondences for other RAW formats.
+See [CR3 profile correspondence geometry](cr3-profile-alignment.md) for guards and scope.
+
+CR3 uses the same colour gates and relative WB. Its sensor and JPEG proxies are first oriented identically. Canon can crop its JPEG differently from the sensor; without an accepted lens-profile proposal, a strongly distortion-corrected JPEG can still fail the gates. A fixed 96-pixel proxy measures this remaining framing from luminance-gradient directions: scale is limited to 0.9–1.1 and translation to two proxy pixels. At least 192 edges are required; two thirds train the framing and the remaining third must independently reach direction agreement ≥0.90 with an improvement ≥0.04. At least 80% of each edge set must remain usable. Weak, flat, unrelated or already aligned references keep the sensor framing. This alignment affects training correspondences only; the RAW render still uses the actual sensor pixels.
+
+Before the profile-based proposal, the public CC0 EOS R100 / RF-S 18–45 mm sample had a measured framing scale of 0.968. Held-out edge agreement rose from 0.7291 to 0.9194, and the unchanged colour gates accepted the fit (held-out RMS 0.10625 → 0.07704). The corpus test asserts acceptance after decoding the sensor RAW, and synthetic tests cover nonlinear camera tone, known crop/translation and unrelated references. The matrix, tone and hue/saturation table are estimated from the file; no third-party camera matrix or decoder calibration source is copied.
 
 This is a per-file camera-look estimate, **not measured spectral calibration or Lightroom parity**. Camera local tone, picture styles, noise reduction and lens warps cannot all be reproduced by a global matrix/curve; same-aspect crops may evade the aspect check. Skin, mixed-light scenes and saturated highlights can still differ from a supplied JPEG. Broader Sony, Nikon and Canon model/lighting coverage, chart-based calibration, absolute WB and other manufacturers remain unverified. Rejected fits and other RAW formats retain their existing behaviour.
 
diff --git a/docs/cr3-profile-alignment.md b/docs/cr3-profile-alignment.md
new file mode 100644
--- /dev/null
+++ b/docs/cr3-profile-alignment.md
@@ -0,0 +1,65 @@
+# CR3 profile correspondence geometry
+
+CR3 colour calibration can propose a resolved lens profile’s geometry before collecting
+sensor/JPEG training pairs. It reuses the guarded correspondence algorithm proposed in
+#2675, with an additional comparison against CR3’s existing framing fallback.
+It does not change CR3 decoding or the lens warp applied when developing the photograph.
+
+A resolved external lens profile may propose geometry only for file-local colour calibration,
+with a fallback camera matrix and no in-file lens warp. The sensor, sensor clip mask, and embedded JPEG are
+oriented together before evaluating the already oriented lens profile. White balance and
+baseline exposure are applied exactly once before geometry assessment.
+
+The existing fixed-proxy edge test, bounded framing search (scale 0.9–1.1 and translation up to
+two proxy pixels), minimum edge coverage, 0.9 agreement, and independent held-out improvement
+of at least 0.04 are retained. Geometry uses luminance edge directions, without fitting RGB
+values. Accepted geometry moves the sensor and clip mask together; invalid borders remain NaN.
+CR3 proposals must also preserve or improve agreement against the existing framed fallback
+on both training and held-out edges, compared on the exact intersection of valid edges with
+the same reference weights. The published minimum 0.04 improvement over the unframed sensor
+is retained separately; the fallback comparison adds no extra improvement threshold. Rejected or absent geometry takes the existing CR3 framing path. All subsequent colour,
+tone, hue/saturation, clipping, and held-out acceptance gates stay unchanged.
+
+The proposal reaches file-local colour and tone, pooled camera-profile pairs, and colour tags
+for merge/DNG conversion. Cache versions advance to look 14 and render 71. No camera allowlist,
+new profile matching rules, or weaker acceptance gate is introduced. Other RAW formats
+retain their existing correspondence behaviour. The non-CR3 and fixed-mount changes
+proposed by #2675 are separate work.
+
+Synthetic tests cover lens-only and lens-plus-framing acceptance, unrelated/flat/already aligned
+rejection, crop-relative profile orientation at multiple resolutions, clip-mask contributions,
+and the complete CR3 correspondence route for EXIF orientations 1–8 with non-unity white balance.
+The opt-in `corpus_cr3_profile_geometry_diagnostics` test exports `geometry.tsv` and plain,
+selected, and JPEG proxy PNGs when `LIGHTCRAFT_CR3_DIAGNOSTICS` names an output directory.
+It reads CR3 files from `$LIGHTCRAFT_CORPUS/raw` and the normal configured lens database.
+The TSV appends `status` and `reason` columns. `measured` means both proxy calls returned
+images; acceptance or rejection is recorded separately in the existing measurement columns.
+If either call returns no proxy, the row records `missing_plain_proxy`,
+`missing_chosen_proxy`, or `missing_both_proxies`, leaves all measurement fields empty,
+and exports no PNGs for that file. The diagnostic continues with the next CR3. The reason
+does not attribute a cause: the proxy API returns only `None`, which can cover unavailable
+previews or sensor proxies. An unavailable row is not a passed geometry or colour check.
+The PNGs show linear proxy RGB converted to sRGB, without a camera tone curve; they are geometry
+inspection aids, not developed image fidelity comparisons. Real-sample assessment and unchanged
+fallback controls are required before any publication. This experiment does not establish
+camera-wide colour parity or validate arbitrary Canon lenses, picture styles, or CRX modes.
+
+## Public-sample assessment
+
+On 22 SHA-256-verified, individually CC0-listed raw.pixls.us CR3 files using lensfun
+XML data at `43f9e001`, the R100 RAW/C-RAW pair with RF-S18–45 at 18 mm accepts the
+new geometry. The other 20 files retain pixel-identical 1600-pixel renders with optics
+on/off, including eight R6 Mark II full-frame/crop, RAW/C-RAW and Dual-Pixel variants
+of one scene. All 22 retain identical renders without a lens database.
+
+With the same independent baseline-derived radial registration applied to both
+outputs, mean DE76 at 768×512 versus the embedded JPEG improves 4.54 to 3.79 (R100 RAW)
+and 4.15 to 3.79 (C-RAW) with optics on. Unregistered whole-frame error slightly worsens
+because optical framing still differs. This is a modest calibration benefit and does
+not remove the photograph's visible lens distortion. The camera JPEG is not a
+colorimetric reference. Four 24 MP files (R100 RAW/C-RAW and R6 II full-frame RAW/C-RAW)
+render on CPU/Metal within 1/255 per channel, with optics on/off.
+
+The sample set does not establish all-camera correctness. R6 II’s eight variants
+share one scene, not eight independent scenes. Pooled-profile and DNG-tag forwarding
+are tested synthetically; this is not a real multi-camera calibration or DNG roundtrip.
diff --git a/docs/gaps.md b/docs/gaps.md
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -1,6 +1,6 @@
 # Gaps: where LightCraft falls short of Lightroom
 
-> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-24 · **Change:** minor (gap 30: the Info panel shows the selected photos' shared metadata and marks mixed fields; gap 28: ⌘⌥⇧C / ⌘⌥⇧V copy and paste metadata; gap 13: before a Lightroom catalog import the app asks merge or new library, #2672; gap 28: F8 shows / hides the right panel; import review album hierarchy, #2362; gap 20: Minolta MRW decodes; gap 10: 551 models verified; gap 15: Print Page Options — page numbers, page info, crop marks; gap 28: `\` shows / hides the Filter Bar in the grids; gap 21: Indonesian interface shipped, every message but What's New; gap 27: the Windows x64 installer verified on Windows 11; the Settings → Apps uninstall confirmation remains; gap 28: ⌘L Enable Filters switches the filter off and on; gap 15: Japanese photo and postcard paper sizes in the Print page setup and PDF)
+> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-27 · **Change:** minor (gap 1: bounded CR3 profile correspondences, broader calibration still open; earlier: gap 30: the Info panel shows the selected photos' shared metadata and marks mixed fields; gap 28: ⌘⌥⇧C / ⌘⌥⇧V copy and paste metadata; gap 13: before a Lightroom catalog import the app asks merge or new library, #2672; gap 28: F8 shows / hides the right panel; import review album hierarchy, #2362; gap 20: Minolta MRW decodes; gap 10: 551 models verified; gap 15: Print Page Options — page numbers, page info, crop marks; gap 28: `\` shows / hides the Filter Bar in the grids; gap 21: Indonesian interface shipped, every message but What's New; gap 27: the Windows x64 installer verified on Windows 11; the Settings → Apps uninstall confirmation remains; gap 28: ⌘L Enable Filters switches the filter off and on; gap 15: Japanese photo and postcard paper sizes in the Print page setup and PDF)
 
 Every known shortfall, one entry each, ranked by user impact (how many Lightroom users hit it, and
 how hard it blocks them), then by effort. **Agents pick work from the top** (the first entry nobody is
@@ -15,7 +15,7 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
 
 | # | Gap | Evidence | User impact | Hours | Doc |
 |---:|---|---|---|---:|---|
-| 1 B | **Camera colour is estimated, not measured** (LR-PROF-CAMERACOLOR). Raws start from a per-file fit to the camera JPEG, 5 bundled profiles or 52 spectral matrices (plus same-sensor bodies, e.g. the X-T30 through the X-T3); others use a neutral matrix (a black-and-white camera JPEG gives its tone curve only, #577; dark JPEG tones also guide the toe, #1307) | ΔE vs camera JPEG 2.9–10 after fitting (raw-parity.md); 0 chart-calibrated models vs 1,500 Adobe Standard profiles | every raw shooter sees colour that is close but not Lightroom's; some get neutral-matrix colour | 50–90 | [raw-parity](raw-parity.md#colour) |
+| 1 B | **Camera colour is estimated, not measured** (LR-PROF-CAMERACOLOR). Raws start from a per-file fit to the camera JPEG, 5 bundled profiles or 52 spectral matrices (plus same-sensor bodies, e.g. the X-T30 through the X-T3); others use a neutral matrix (a black-and-white camera JPEG gives its tone curve only, #577; dark JPEG tones also guide the toe, #1307) | ΔE vs camera JPEG 2.9–10 after fitting (raw-parity.md); 0 chart-calibrated models vs 1,500 Adobe Standard profiles | every raw shooter sees colour that is close but not Lightroom's; some get neutral-matrix colour | 50–90 | [raw-parity](raw-parity.md#colour) CR3 profile correspondence alignment is guarded against its framing fallback; limited public-sample coverage does not close this gap ([scope](cr3-profile-alignment.md)). |
 | 2 B | **CR3 variants no public file uses** (LR-IMP-FORMATS): `crx.rs` refuses vertical-tile C-RAW, partial-subband and other plane coding, multi-tile 0x200 and adaptive low-frequency gains, and those files fall back to the preview. All 110 CC0 CR3s on raw.pixls.us (31 folders) decode from sensor data, so none of these variants has a public sample; 26 CR3 bodies are verified (M50 / R100 / R8 exact, R5 Mark II pinned, 22 more pinned on one CC0 file each and checked against the camera JPEG, [cr3.md](cr3.md#further-bodies-one-cc0-file-each)). HEIF stills are `.HIF` files, not CR3; no CR3 sRAW / mRAW sample exists | measured: Canon's share whose camera fully works 73% → 94% (raw-parity.md); 18 of 22 popular Canon bodies write CR3, all 18 now verified | +0.3 points of photographers; the variants surface only as user files that open as a preview | 10–25 | [raw-parity](raw-parity.md#remaining-work-ranked-by-photographers-unlocked-per-hour) |
 | 3 B | **Render fidelity unmeasured** (LR-BEHAV-RENDER-FIDELITY): tone, highlights, texture / clarity / dehaze, NR and sharpening are tuned by eye | no side-by-side suite; a retune needs a new process version (only V1 exists) | edits imported from Lightroom look different; the look "isn't Lightroom's" | 30–60 | [raw-parity](raw-parity.md), [process-versions](process-versions.md) |
 | 4 | ~~Original-pixel detail at 100%~~ (LR-VIEW-ZOOM): closed 2026-10-12, see Closed recently | checklist row ✅ | — | 0 | [ui-parity](ui-parity.md) |
@@ -84,6 +84,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-27 | minor | Gap 1: CR3 profile correspondence alignment guarded against the existing framing fallback; limited real-file evidence, camera-colour gap remains open |
 | 2026-10-24 | minor | Gap 28: ⌘⌥⇧C / ⌘⌥⇧V (Ctrl+Alt+Shift+C / V on Windows and Linux) copy and paste metadata; no key leaves the entry; hours unchanged |
 | 2026-10-24 | minor | Gap 30 shrinks: the Info panel's Show Metadata For (Target Photo / Selected Photos, `view.infoSelectedPhotos`) shows the selected photos' shared values and marks fields that differ as Mixed; the All Metadata dialog remains; 1–3 h (was 4–8) |
 | 2026-10-23 | minor | Gap 13: before a Lightroom catalog import the app asks whether it merges into the open library or goes into a new one (opt-out in Settings, #2672); hours unchanged |
diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md
--- a/docs/parity-checklist.md
+++ b/docs/parity-checklist.md
@@ -1,6 +1,6 @@
 # Lightroom parity checklist
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (LR-PROF-CAMERACOLOR: CR3 guarded profile correspondences, partial status unchanged; earlier: LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
 
 The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we
 have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good
@@ -236,7 +236,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
 | LR-PROF-ADOBE | Standard raw looks (own equivalents) | P0 | ✅ | `crates/engine/src/presets.rs` (`PROFILES`), `crates/pipeline/src/profiles.rs` | six own looks: Color, Neutral, Vivid, Landscape, Portrait, Monochrome |
 | LR-PROF-ADAPTIVE | Adaptive profiles | P2 | ⬜ | | |
 | LR-PROF-CAMERA | Camera-matching looks | P2 | ⬜ | | |
-| LR-PROF-CAMERACOLOR | Camera colour calibration (own) | P0 | 🟡 | `crates/raw/src/color.rs`, `crates/raw/src/profile.rs`, `crates/raw/src/gaintable.rs`, `crates/engine/src/camera_preview.rs` | DNG files use the colour matrices and the camera-profile look they carry (`ProfileHueSatMap`, `ProfileLookTable`, `ProfileToneCurve`, read from the file at run time per DNG spec ch. 6 — Lightroom-converted DNGs rendered flat and muted without them, issue #138). A DNG whose profile defines no tone curve (absent or identity `ProfileToneCurve`, with or without a `LookTable`, as Adobe Standard in Lightroom-written DNGs) gets its tone fitted to its embedded preview when that is at least 10% of the raw's long edge and 384 px (Lightroom's 1024 px previews qualify): two Lightroom Classic 15.1 lossy ILCE-7CM2 DNGs went from about 0.8 EV darker and flat to within 3% of Lightroom's mean linear RGB. A DNG's `ProfileGainTableMap` / `ProfileGainTableMap2` (Apple ProRAW's local tone map; both versions, DNG 1.7 precedence, the file's byte order with a big-endian fallback) is read and written back by DNG export and not rendered by default, as Lightroom Classic renders ProRAW without it (measured on one CC0 iPhone 12 Pro ProRAW); the Profile option "Camera local tone mapping" (`profile.camera_local_tone`, per photo, off by default, `crates/pipeline/src/local_tone.rs`) renders it as the camera does, per output pixel in the scene-linear stage (CPU, also under the GPU path); Sony ARW, Nikon NEF, Panasonic RW2, Fujifilm RAF, Canon CR3 and CR2, Pentax PEF, Samsung SRW and Olympus ORF get a guarded file-local fit to their own embedded JPEG — chromaticity matrix + hue/saturation/value table + tone and chroma curves, relative WB — or, with a bundled or local per-model profile pooled from many photos (`lightcraft-cli calibrate`, ARW, NEF and RAF so far, `crates/engine/src/camera_profiles.rs`), the profile's colour and only their own tone/chroma curves (on 29 held-out ILCE-7M4 photos mean ΔE vs the camera JPEG 3.54 → 3.10; bundled ILCE-7CR from 125 photos: 26 held-out 4.07 → 3.29, and every one of the 131 photos accepted, 8 of which fell back to neutral without it; bundled ILCA-99M2 from 144 photos: 30 held-out 5.11 → 4.87, 25 of them closer); the tone fit also uses dark JPEG samples excluded from colour training, so deep shadows follow the camera toe (#1307); near-black frames with too few colour pairs can take a guarded tone-only fallback on their existing source colour (#1587); Sony ARWs shot with DRO on (maker note `0xb025`, on by default) keep that colour but their tone curve is lowered to Sony's curve without DRO, whose lifted shadows are in the camera JPEG only (issue #244: 8 ILCE-7CR photos +6.4 → +0.8 L* from Lightroom's default, median ΔE 7.5 → 3.3); 52 models without a profile whose JPEG fit fails (or that have no usable JPEG) start from DNG-style matrices fitted to their measured spectral sensitivities instead of the neutral fallback (`crates/raw/src/spectral.rs`, rawtoaces-data v1.1.0, Apache-2.0, aliases included; not yet measured against the camera JPEG); ARW: on 10 public raw.pixls.us samples from 8 bodies 7 fits were accepted, mean ΔE vs the camera JPEG 17–26 → 3–10; NEF: 13 of 13 decodable samples (D750, D780, D850, D7500, Z 50 and local D7500 shots) accepted, ΔE 13–46 → 3–8 (issue #150); RW2 / RWL: 140 of 174 public samples accepted, from 93 of the 114 bodies with a preview, median ΔE 5.0 with matching lightness (13 of the 14 corpus files with a preview: ΔE 9.6–27 → 2.8–8.9; most rejections are compacts and kit zooms whose camera JPEG is distortion-corrected); a fit that fails the gates on all pixels is retried away from edges, where a lens-corrected camera JPEG still lines up (issue #232: the six distortion-corrected public ILCE-7RM2 samples ΔE 16.6–16.9 → 3.7–3.8, the corpus GX80 19.1 → 7.9); RAF: bundled X-H2S/X-T4 profiles from 201/545 photos; on 150 withheld files mean ΔE improves slightly over per-file fitting (X-H2S 1.70 → 1.62, X-T4 2.43 → 2.34); 23 of 24 references from 13 bodies accepted, including the dark X-H2S samples (8.74 → 2.25); ORF: the 3 decodable corpus files (E-1, E-400, XZ-2) accepted (`docs/camera-preview-colour.md`); a raw whose camera JPEG is black and white (a monochrome picture style) takes only the JPEG's tone curve, on the camera profile's, spectral or neutral colour (issue #577); this is not measured camera calibration; other raws and rejected fits use a neutral fallback (camera RGB ≈ linear sRGB, `matrix_is_fallback`) with as-shot white balance, so colours are muted and not accurate. Needs our own per-camera calibration: matrices the files carry themselves (e.g. Olympus `ColorMatrix`), fitting each camera to its own embedded JPEG, then chart shots. Adobe matrices / DCPs are never used. Biggest image-quality gap today; ILCE-7RM6: per-file fit now runs on previews above 64 MP, a pooled profile (46 photos) was tried and not bundled (withheld ΔE 5.71 → 6.40, 11 of 15 regressed), kept with smart previews (see docs/camera-preview-colour.md); the fit pairs the sensor moved into the file's lens-warp geometry when that lines up better with a lens-corrected camera JPEG (29 CC0 raws: alignment +0.13, mean ΔE 3.73 → 3.61); bundled NIKON Z 6_2 profile (296 photos; 59 held-back photos mean ΔE 2.73 → 2.54) NIKON Z 8 profile (261 photos; 55 held back 2.55 → 2.33) and NIKON D5300 profile (264 photos; 53 held back 2.86 → 2.66) |
+| LR-PROF-CAMERACOLOR | Camera colour calibration (own) | P0 | 🟡 | `crates/raw/src/color.rs`, `crates/raw/src/profile.rs`, `crates/raw/src/gaintable.rs`, `crates/engine/src/camera_preview.rs` | DNG files use the colour matrices and the camera-profile look they carry (`ProfileHueSatMap`, `ProfileLookTable`, `ProfileToneCurve`, read from the file at run time per DNG spec ch. 6 — Lightroom-converted DNGs rendered flat and muted without them, issue #138). A DNG whose profile defines no tone curve (absent or identity `ProfileToneCurve`, with or without a `LookTable`, as Adobe Standard in Lightroom-written DNGs) gets its tone fitted to its embedded preview when that is at least 10% of the raw's long edge and 384 px (Lightroom's 1024 px previews qualify): two Lightroom Classic 15.1 lossy ILCE-7CM2 DNGs went from about 0.8 EV darker and flat to within 3% of Lightroom's mean linear RGB. A DNG's `ProfileGainTableMap` / `ProfileGainTableMap2` (Apple ProRAW's local tone map; both versions, DNG 1.7 precedence, the file's byte order with a big-endian fallback) is read and written back by DNG export and not rendered by default, as Lightroom Classic renders ProRAW without it (measured on one CC0 iPhone 12 Pro ProRAW); the Profile option "Camera local tone mapping" (`profile.camera_local_tone`, per photo, off by default, `crates/pipeline/src/local_tone.rs`) renders it as the camera does, per output pixel in the scene-linear stage (CPU, also under the GPU path); Sony ARW, Nikon NEF, Panasonic RW2, Fujifilm RAF, Canon CR3 and CR2, Pentax PEF, Samsung SRW and Olympus ORF get a guarded file-local fit to their own embedded JPEG — chromaticity matrix + hue/saturation/value table + tone and chroma curves, relative WB — or, with a bundled or local per-model profile pooled from many photos (`lightcraft-cli calibrate`, ARW, NEF and RAF so far, `crates/engine/src/camera_profiles.rs`), the profile's colour and only their own tone/chroma curves (on 29 held-out ILCE-7M4 photos mean ΔE vs the camera JPEG 3.54 → 3.10; bundled ILCE-7CR from 125 photos: 26 held-out 4.07 → 3.29, and every one of the 131 photos accepted, 8 of which fell back to neutral without it; bundled ILCA-99M2 from 144 photos: 30 held-out 5.11 → 4.87, 25 of them closer); the tone fit also uses dark JPEG samples excluded from colour training, so deep shadows follow the camera toe (#1307); near-black frames with too few colour pairs can take a guarded tone-only fallback on their existing source colour (#1587); Sony ARWs shot with DRO on (maker note `0xb025`, on by default) keep that colour but their tone curve is lowered to Sony's curve without DRO, whose lifted shadows are in the camera JPEG only (issue #244: 8 ILCE-7CR photos +6.4 → +0.8 L* from Lightroom's default, median ΔE 7.5 → 3.3); 52 models without a profile whose JPEG fit fails (or that have no usable JPEG) start from DNG-style matrices fitted to their measured spectral sensitivities instead of the neutral fallback (`crates/raw/src/spectral.rs`, rawtoaces-data v1.1.0, Apache-2.0, aliases included; not yet measured against the camera JPEG); ARW: on 10 public raw.pixls.us samples from 8 bodies 7 fits were accepted, mean ΔE vs the camera JPEG 17–26 → 3–10; NEF: 13 of 13 decodable samples (D750, D780, D850, D7500, Z 50 and local D7500 shots) accepted, ΔE 13–46 → 3–8 (issue #150); RW2 / RWL: 140 of 174 public samples accepted, from 93 of the 114 bodies with a preview, median ΔE 5.0 with matching lightness (13 of the 14 corpus files with a preview: ΔE 9.6–27 → 2.8–8.9; most rejections are compacts and kit zooms whose camera JPEG is distortion-corrected); a fit that fails the gates on all pixels is retried away from edges, where a lens-corrected camera JPEG still lines up (issue #232: the six distortion-corrected public ILCE-7RM2 samples ΔE 16.6–16.9 → 3.7–3.8, the corpus GX80 19.1 → 7.9); RAF: bundled X-H2S/X-T4 profiles from 201/545 photos; on 150 withheld files mean ΔE improves slightly over per-file fitting (X-H2S 1.70 → 1.62, X-T4 2.43 → 2.34); 23 of 24 references from 13 bodies accepted, including the dark X-H2S samples (8.74 → 2.25); ORF: the 3 decodable corpus files (E-1, E-400, XZ-2) accepted (`docs/camera-preview-colour.md`); a raw whose camera JPEG is black and white (a monochrome picture style) takes only the JPEG's tone curve, on the camera profile's, spectral or neutral colour (issue #577); this is not measured camera calibration; other raws and rejected fits use a neutral fallback (camera RGB ≈ linear sRGB, `matrix_is_fallback`) with as-shot white balance, so colours are muted and not accurate. Needs our own per-camera calibration: matrices the files carry themselves (e.g. Olympus `ColorMatrix`), fitting each camera to its own embedded JPEG, then chart shots. Adobe matrices / DCPs are never used. Biggest image-quality gap today; ILCE-7RM6: per-file fit now runs on previews above 64 MP, a pooled profile (46 photos) was tried and not bundled (withheld ΔE 5.71 → 6.40, 11 of 15 regressed), kept with smart previews (see docs/camera-preview-colour.md); the fit pairs the sensor moved into the file's lens-warp geometry when that lines up better with a lens-corrected camera JPEG (29 CC0 raws: alignment +0.13, mean ΔE 3.73 → 3.61); bundled NIKON Z 6_2 profile (296 photos; 59 held-back photos mean ΔE 2.73 → 2.54) NIKON Z 8 profile (261 photos; 55 held back 2.55 → 2.33) and NIKON D5300 profile (264 photos; 53 held back 2.86 → 2.66) CR3 calibration pairs may use guarded resolved-profile geometry with non-regression against its framing fallback ([scope](cr3-profile-alignment.md)); other formats and partial status stay unchanged. |
 | LR-PROF-CREATIVE | Creative profiles (own) | P2 | ✅ | `cmd:develop.profile`, `crates/pipeline/src/profiles.rs` | 16 own looks in Film / Cinematic / Muted / B&W (tone + point-curve fades, colour grading, mixer / B&W mix); scale with `ctl:profile.amount`; sliders untouched |
 | LR-PROF-LEGACY | Legacy profiles | P2 | ⬜ | | |
 | LR-PROF-NONRAW | Profiles for non-raw files | P0 | ✅ | `cmd:develop.profile` | same looks apply to JPEG/TIFF |
@@ -799,6 +799,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-27 | minor | LR-PROF-CAMERACOLOR: CR3 profile geometry with independent original-edge gates and non-regression against its framing fallback; other formats and partial status unchanged |
 | 2026-10-25 | patch | LR-BEHAV-TITLEBAR: the top bar's title-bar gestures come from craft-window's `title_bar`: a double-click follows the system setting, and a press on a button, the search field or a panel floating over the bar never moves the window; status counts unchanged |
 | 2026-10-24 | minor | KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 (`keyword.nextSet`, `keyword.previousSet`) go to the next / previous keyword set; status counts unchanged (KEYC-METADATA stays 🟡) |
 | 2026-10-24 | minor | KEYC-METADATA: ⌘K / Ctrl+K (`panel.addKeywords`, Add Keywords) shows the Keywords panel, opens Keywording and puts the cursor in the Add keyword box; status counts unchanged (KEYC-METADATA stays 🟡) |
