diff --git a/apps/photocraft/src/main.rs b/apps/photocraft/src/main.rs
--- a/apps/photocraft/src/main.rs
+++ b/apps/photocraft/src/main.rs
@@ -75,8 +75,7 @@ mod window_frame;
 use photocraft_engine::Session;
 use photocraft_ui_egui::PhotocraftApp;
 
-/// Matches the `.desktop` file and hicolor icon name, so Wayland docks pick up the icon.
-const APP_ID: &str = "ai.storyteller.photocraft";
+use photocraft_ui_egui::os_window::APP_ID;
 
 /// Windows and Linux: no OS title bar; the app's top bar is the title bar, with its own caption
 /// buttons and edge resizing (`photocraft_ui_egui::titlebar`), as Photoshop does on Windows. macOS
diff --git a/crates/ui-egui/src/canvas/tabs.rs b/crates/ui-egui/src/canvas/tabs.rs
--- a/crates/ui-egui/src/canvas/tabs.rs
+++ b/crates/ui-egui/src/canvas/tabs.rs
@@ -704,7 +704,7 @@ pub fn extra_windows(app: &mut PhotocraftApp, ctx: &egui::Context) {
         let Some(st) = app.session.documents().get(w.document) else { continue };
         let title = format!("{} — window {}", st.doc.name, w.id);
         let vid = egui::ViewportId::from_hash_of(("docwin", w.id));
-        let mut builder = egui::ViewportBuilder::default().with_title(title).with_inner_size([800.0, 600.0]);
+        let mut builder = crate::os_window::builder().with_title(title).with_inner_size([800.0, 600.0]);
         if let Some([x, y]) = w.position {
             builder = builder.with_position([x, y]);
         }
diff --git a/crates/ui-egui/src/dialogs/camera_raw_ui/controls.rs b/crates/ui-egui/src/dialogs/camera_raw_ui/controls.rs
--- a/crates/ui-egui/src/dialogs/camera_raw_ui/controls.rs
+++ b/crates/ui-egui/src/dialogs/camera_raw_ui/controls.rs
@@ -197,7 +197,7 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
         ui.painter().rect_filled(r, 0.0, Tokens::get(ui.ctx()).scrim);
     });
     // A window of its own (title bar, moved and resized like any window), like Adobe Camera Raw.
-    let builder = egui::ViewportBuilder::default().with_title(window_title).with_inner_size([1360.0, 900.0]).with_min_inner_size([900.0, 600.0]);
+    let builder = crate::os_window::builder().with_title(window_title).with_inner_size([1360.0, 900.0]).with_min_inner_size([900.0, 600.0]);
     let mut close = false;
     ctx.show_viewport_immediate(egui::ViewportId::from_hash_of("camera-raw-window"), builder, |ui, class| {
         if ui.ctx().input(|i| i.viewport().close_requested()) {
diff --git a/crates/ui-egui/src/dialogs/screen_picker.rs b/crates/ui-egui/src/dialogs/screen_picker.rs
--- a/crates/ui-egui/src/dialogs/screen_picker.rs
+++ b/crates/ui-egui/src/dialogs/screen_picker.rs
@@ -261,7 +261,7 @@ pub fn cancel(ctx: &Context) {
 
 /// The picker window: hidden until its snapshot has been painted (#3210).
 fn viewport_builder(ctx: &Context, image: &ScreenImage) -> egui::ViewportBuilder {
-    let mut builder = egui::ViewportBuilder::default()
+    let mut builder = crate::os_window::builder()
         .with_title(tl!("PhotoCraft — Screen Color Picker"))
         .with_decorations(false)
         .with_resizable(false)
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -30,6 +30,7 @@ pub mod menu_catalog;
 pub mod menus;
 pub mod monitor_status;
 pub mod notices;
+pub mod os_window;
 pub mod panels;
 pub mod preset_files_ui;
 pub mod proxy;
diff --git a/crates/ui-egui/src/os_window.rs b/crates/ui-egui/src/os_window.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/os_window.rs
@@ -0,0 +1,19 @@
+//! Extra OS windows (document windows, Camera Raw, the screen color picker) share the main
+//! window's app id, so docks show the PhotoCraft icon on them and Wayland compositors' window
+//! rules can match them (Hyprland's `class:`, #6697). Without one their app id is empty.
+
+/// Matches the `.desktop` file and hicolor icon name, so Wayland docks pick up the icon.
+pub const APP_ID: &str = "ai.storyteller.photocraft";
+
+/// A builder for an extra OS window, carrying [`APP_ID`].
+pub fn builder() -> egui::ViewportBuilder {
+    egui::ViewportBuilder::default().with_app_id(APP_ID)
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn extra_windows_carry_the_app_id() {
+        assert_eq!(super::builder().app_id.as_deref(), Some("ai.storyteller.photocraft"));
+    }
+}
