diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs
--- a/crates/ui-egui/src/commands.rs
+++ b/crates/ui-egui/src/commands.rs
@@ -112,6 +112,7 @@ impl PdfCraftApp {
         }
     }
 
+    /// Run `id` against the current state (called behind [`Self::execute`]'s `catch_unwind` guard).
     fn execute_unguarded(&mut self, id: &str) -> bool {
         let Some(spec) = commands::command(id) else { return false };
         if !self.command_enabled(spec) {
@@ -169,7 +170,7 @@ impl PdfCraftApp {
                 None => self.notify_tr("No recent files"),
             },
             "file.clear_recent" if self.recent.is_empty() => self.notify_tr("No recent files"),
-            "file.clear_recent" => self.recent.clear(),
+            "file.clear_recent" => self.recent.retain(|r| r.pinned),
             "file.pin_folder" => self.pin_folder_dialog(),
             "page.combine" => self.open_combine_tab(),
             "file.save" => {
diff --git a/crates/ui-egui/src/home.rs b/crates/ui-egui/src/home.rs
--- a/crates/ui-egui/src/home.rs
+++ b/crates/ui-egui/src/home.rs
@@ -154,6 +154,7 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
             }
             let mut open = None;
             let mut remove = None;
+            let mut pin = None;
             for r in &app.recent {
                 let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 46.0), Sense::click());
                 resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, &r.name));
@@ -166,6 +167,26 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
                 } else {
                     press.wash(ui, rect, 8, false);
                 }
+                // Pin or unpin this file so it stays at the top of Recent across Clear (#4772).
+                // The interaction is always present so keyboard users can focus and activate it;
+                // the icon is drawn when the row is hovered, focused, or already pinned.
+                let pin_rect = Rect::from_center_size(rect.right_center() - vec2(58.0, 0.0), vec2(28.0, 28.0));
+                let p = ui.interact(pin_rect, ui.scope_id().with(("pin-recent", &r.path)), Sense::click());
+                let (label, hover) = if r.pinned {
+                    (crate::i18n::fmt(tl!("Unpin {name} from Recent"), &[("name", &r.name)]), tl!("Unpin from Recent"))
+                } else {
+                    (crate::i18n::fmt(tl!("Pin {name} to Recent"), &[("name", &r.name)]), tl!("Pin to Recent"))
+                };
+                p.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, &label));
+                if hovered || r.pinned || p.has_focus() {
+                    if p.hovered() || p.has_focus() {
+                        ui.painter().rect_filled(pin_rect, CornerRadius::same(6), t.pressed);
+                    }
+                    icons::paint(ui, pin_rect.shrink(6.0), "pin", 16.0, if r.pinned { t.accent_text } else { t.icon });
+                }
+                if p.on_hover_text(hover).clicked() {
+                    pin = Some(r.path.clone());
+                }
                 if hovered {
                     // Remove just this file from the list (#430); the file itself is untouched.
                     let x_rect = Rect::from_center_size(rect.right_center() - vec2(26.0, 0.0), vec2(28.0, 28.0));
@@ -188,9 +209,9 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
                     22.0,
                     egui::Color32::from_rgb(0xE0, 0x3E, 0x3E),
                 );
-                // Room on the right for the remove button.
+                // Room on the right for the pin and remove buttons.
                 let detail = ui.painter().text(
-                    body.right_center() - vec2(48.0, 0.0),
+                    body.right_center() - vec2(80.0, 0.0),
                     Align2::RIGHT_CENTER,
                     format!("{}  ·  {}", crate::i18n::trn(crate::i18n::current(), r.pages as u64, "{n} page", "{n} pages"), human_size(r.size)),
                     theme::regular(12.0),
@@ -207,6 +228,12 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
             if let Some(p) = open {
                 app.open_recent(&p);
             }
+            if let Some(p) = pin {
+                if let Some(r) = app.recent.iter_mut().find(|r| r.path == p) {
+                    r.pinned = !r.pinned;
+                }
+                app.recent.sort_by_key(|r| !r.pinned);
+            }
             if let Some(p) = remove {
                 app.recent.retain(|r| r.path != p);
             }
@@ -227,6 +254,7 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
 mod tests {
     use super::*;
 
+    /// Verify that translated tool card descriptions wrap without overlapping the action label.
     #[test]
     fn translated_card_description_leaves_room_for_the_action() {
         let de = crate::i18n::Lang::from_code("de").unwrap();
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
@@ -612,17 +612,21 @@ pub struct UnlockPrompt {
 }
 
 impl UnlockPrompt {
+    /// Create a prompt asking for `doc`'s permissions password before running `then`.
     pub(crate) fn new(doc: &pdfcraft_engine::Document, then: Option<&'static str>) -> Self {
         Self { doc: doc.id, name: doc.name.clone(), input: String::new(), error: None, then }
     }
 }
 
+/// A file in the Recent list, with its path, page count, byte size and whether it is pinned.
 #[derive(serde::Serialize, serde::Deserialize)]
 pub struct RecentFile {
     pub name: String,
     pub path: String,
     pub pages: usize,
     pub size: usize,
+    #[serde(default)]
+    pub pinned: bool,
 }
 
 pub struct PdfCraftApp {
@@ -1529,6 +1533,7 @@ impl PdfCraftApp {
         }
     }
 
+    /// Prepare `bytes` with `password` (or open a password prompt if needed) and commit the tab.
     fn try_open(
         &mut self,
         name: &str,
@@ -1564,6 +1569,7 @@ impl PdfCraftApp {
         self.finish_open(name, path, bytes.len(), prepared, activate).map(|_| ())
     }
 
+    /// Commit a prepared document to the session, open its view tab and record it in Recent.
     fn finish_open(
         &mut self,
         name: &str,
@@ -1638,9 +1644,7 @@ impl PdfCraftApp {
         if let Some(p) = path {
             #[cfg(target_os = "macos")]
             macos_association::repair(std::path::Path::new(&p));
-            self.recent.retain(|r| r.path != p);
-            self.recent.insert(0, RecentFile { name: name.to_string(), path: p, pages, size });
-            self.recent.truncate(12);
+            self.add_recent(name, p, pages, size);
         }
         // What the form's scripts said while it opened (messages, errors) shows now, not with
         // the next edit.
@@ -1653,6 +1657,17 @@ impl PdfCraftApp {
         Ok(id)
     }
 
+    /// Record `path` at the head of the Recent list, preserving its pinned state and keeping
+    /// pinned files at the top and within the 12-file cap.
+    pub(crate) fn add_recent(&mut self, name: &str, path: String, pages: usize, size: usize) {
+        let pinned = self.recent.iter().find(|r| same_file(&r.path, &path)).is_some_and(|r| r.pinned);
+        self.recent.retain(|r| !same_file(&r.path, &path));
+        self.recent.insert(0, RecentFile { name: name.to_string(), path, pages, size, pinned });
+        self.recent.sort_by_key(|r| !r.pinned);
+        self.recent.truncate(12);
+    }
+
+    /// Open a file dropped onto the application window, by path when absolute or by its bytes.
     #[cfg(not(target_arch = "wasm32"))]
     fn open_dropped(&mut self, f: egui::DroppedFileHandle, _ctx: &egui::Context) {
         let p = f.path().to_string_lossy().into_owned();
@@ -2721,9 +2736,13 @@ impl PdfCraftApp {
             self.organize_zoom = zoom.clamp(*canvas::GRID_ZOOM_RANGE.start(), *canvas::GRID_ZOOM_RANGE.end());
         }
         if let Ok(r) = serde_json::from_value::<Vec<RecentFile>>(v["recent"].clone()) {
-            // Only keep entries whose files still exist.
+            // Keep pinned entries (whose volume or synced drive may be offline at startup) and
+            // unpinned entries whose files still exist, sorted with pinned files first.
             #[cfg(not(target_arch = "wasm32"))]
-            let r: Vec<RecentFile> = r.into_iter().filter(|f| std::path::Path::new(&f.path).exists()).collect();
+            let mut r: Vec<RecentFile> = r.into_iter().filter(|f| f.pinned || std::path::Path::new(&f.path).exists()).collect();
+            #[cfg(target_arch = "wasm32")]
+            let mut r = r;
+            r.sort_by_key(|f| !f.pinned);
             self.recent = r;
         }
         if let Some(on) = v["reopen_last_session"].as_bool() {
diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs
--- a/crates/ui-egui/src/menus.rs
+++ b/crates/ui-egui/src/menus.rs
@@ -706,6 +706,7 @@ mod tests {
         assert_eq!(kind(&app, "view.side_panel"), ItemKind::Check(true));
     }
 
+    /// Verify that an open modal dialog disables document menu commands while leaving Quit enabled.
     #[test]
     fn a_dialog_greys_document_commands_and_drops_their_clicks() {
         let (mut app, ctx) = app(&["a.pdf"]);
@@ -721,10 +722,13 @@ mod tests {
         assert!(app.menu_gate(&ctx).allows_command(&Builtin::Quit.command()));
     }
 
+    /// Verify that Open Recent and Window list each recent file and open document once in order.
     #[test]
     fn recent_files_and_open_documents_are_listed_once_each() {
         let (mut app, ctx) = app(&["a.pdf", "b.pdf"]);
-        app.recent = ["one.pdf", "two.pdf"].map(|n| crate::RecentFile { name: n.into(), path: format!("/nowhere/{n}"), pages: 1, size: 0 }).into();
+        app.recent = ["one.pdf", "two.pdf"]
+            .map(|n| crate::RecentFile { name: n.into(), path: format!("/nowhere/{n}"), pages: 1, size: 0, pinned: false })
+            .into();
         let bar = linux(&app, &ctx);
         for (i, name) in ["one.pdf", "two.pdf"].into_iter().enumerate() {
             assert_eq!(item(&bar, &CommandRef::with_params("file.open_recent", serde_json::json!({ "index": i }))).label, name);
diff --git a/crates/ui-egui/tests/it/commands.rs b/crates/ui-egui/tests/it/commands.rs
--- a/crates/ui-egui/tests/it/commands.rs
+++ b/crates/ui-egui/tests/it/commands.rs
@@ -321,7 +321,7 @@ fn the_open_recent_menu_lists_files_and_opens_one() {
             let mut app = PdfCraftApp::new();
             app.set_option("language", "en").unwrap();
             app.open_bytes("doc.pdf", None, fixture(3)).unwrap();
-            app.recent.push(pdfcraft_ui_egui::RecentFile { name: "recent.pdf".into(), path, pages: 2, size: 0 });
+            app.recent.push(pdfcraft_ui_egui::RecentFile { name: "recent.pdf".into(), path, pages: 2, size: 0, pinned: false });
             app
         }
     });
@@ -351,8 +351,9 @@ fn the_open_recent_menu_is_disabled_while_the_list_is_empty() {
     );
 }
 
+/// Build a synthetic unpinned `RecentFile` entry for menu and command tests.
 fn recent_file(name: &str) -> pdfcraft_ui_egui::RecentFile {
-    pdfcraft_ui_egui::RecentFile { name: name.into(), path: format!("/nowhere/{name}"), pages: 1, size: 0 }
+    pdfcraft_ui_egui::RecentFile { name: name.into(), path: format!("/nowhere/{name}"), pages: 1, size: 0, pinned: false }
 }
 
 /// Check that Clear Recent Files empties the list and disables Open Recent.
@@ -376,6 +377,7 @@ fn clear_recent_files_at_the_foot_of_open_recent_empties_the_list() {
     assert!(h.query_by(|n| n.label().as_deref() == Some("Open Recent") && n.is_disabled()).is_some(), "and Open Recent is disabled");
 }
 
+/// Verify that `file.clear_recent` clears unpinned entries, persists, and reports an empty list.
 #[test]
 fn clear_recent_files_runs_from_the_palette_and_is_saved() {
     let mut app = PdfCraftApp::new();
diff --git a/crates/ui-egui/tests/it/ui.rs b/crates/ui-egui/tests/it/ui.rs
--- a/crates/ui-egui/tests/it/ui.rs
+++ b/crates/ui-egui/tests/it/ui.rs
@@ -39,6 +39,7 @@ fn home_shows_welcome_and_tools() {
     h.get_by_label("Open file");
 }
 
+/// Verify that changing the tools panel side in Preferences moves the pane and persists.
 #[test]
 fn tools_panel_side_preference_moves_and_persists() {
     use pdfcraft_ui_egui::Dialog;
@@ -72,13 +73,14 @@ fn painted_text(h: &Harness<'static, PdfCraftApp>) -> Vec<(String, bool, egui::R
         .collect()
 }
 
+/// Verify that long recent file names and paths truncate before colliding with the page count.
 #[test]
 fn long_recent_names_and_paths_are_cut_before_the_page_count() {
     let name = format!("{}.pdf", "Quarterly report final version ".repeat(10));
     let path = format!("/{}/{name}", "A folder with a long name".repeat(8));
     let h = harness({
         let (name, path) = (name.clone(), path.clone());
-        move |app| app.recent.push(pdfcraft_ui_egui::RecentFile { name, path, pages: 7, size: 2048 })
+        move |app| app.recent.push(pdfcraft_ui_egui::RecentFile { name, path, pages: 7, size: 2048, pinned: false })
     });
     let text = painted_text(&h);
     let detail = text.iter().find(|(s, ..)| s.starts_with("7 pages")).expect("the row shows its page count").2;
@@ -89,6 +91,7 @@ fn long_recent_names_and_paths_are_cut_before_the_page_count() {
     }
 }
 
+/// Verify that removing a single recent file or clearing all unpinned recent files updates Home.
 #[test]
 fn home_removes_one_recent_file_or_clears_them_all() {
     // Tall enough that the Recent list is on screen without scrolling.
@@ -96,7 +99,7 @@ fn home_removes_one_recent_file_or_clears_them_all() {
         let mut app = PdfCraftApp::new();
         app.set_option("language", "en").unwrap();
         for name in ["first.pdf", "second.pdf", "third.pdf"] {
-            app.recent.push(pdfcraft_ui_egui::RecentFile { name: name.into(), path: format!("/nowhere/{name}"), pages: 1, size: 0 });
+            app.recent.push(pdfcraft_ui_egui::RecentFile { name: name.into(), path: format!("/nowhere/{name}"), pages: 1, size: 0, pinned: false });
         }
         app
     });
@@ -115,6 +118,76 @@ fn home_removes_one_recent_file_or_clears_them_all() {
     assert!(h.query_by_label("Clear Recent Files").is_none(), "and goes away with nothing left to clear");
 }
 
+/// Pinning a recent file moves it to the top of Recent, keeps it across Clear Recent Files and
+/// new opens, and persists its pinned state across a restart (#4772).
+#[test]
+fn home_pins_recent_file_to_top_keeps_it_across_clear_and_persists() {
+    let dir = std::env::temp_dir().join(format!("pdfcraft-pin-recent-{}", std::process::id()));
+    let _ = std::fs::remove_dir_all(&dir);
+    std::fs::create_dir_all(&dir).unwrap();
+    let files: Vec<(String, String)> = ["first.pdf", "second.pdf", "third.pdf"]
+        .into_iter()
+        .map(|name| {
+            let p = dir.join(name);
+            std::fs::write(&p, FIXTURE).unwrap();
+            (name.to_string(), p.to_string_lossy().into_owned())
+        })
+        .collect();
+    let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1600.0)).build_eframe({
+        let files = files.clone();
+        move |_cc| {
+            let mut app = PdfCraftApp::new();
+            app.set_option("language", "en").unwrap();
+            for (name, path) in files {
+                app.recent.push(pdfcraft_ui_egui::RecentFile { name, path, pages: 1, size: FIXTURE.len(), pinned: false });
+            }
+            app
+        }
+    });
+    h.run_steps(4);
+    h.get_by_label("second.pdf").hover();
+    h.run_steps(2);
+    h.get_by_label("Pin second.pdf to Recent").click();
+    h.run_steps(2);
+    let names: Vec<String> = h.state().recent.iter().map(|r| r.name.clone()).collect();
+    assert_eq!(names, ["second.pdf", "first.pdf", "third.pdf"], "pinning moves the file to the top of Recent");
+    assert!(h.state().recent[0].pinned);
+
+    // Opening more files (past the 12-file cap) and reopening the pinned file keeps it pinned at the top.
+    for i in 0..14 {
+        let extra = dir.join(format!("extra-{i}.pdf"));
+        h.state_mut().open_bytes(&format!("extra-{i}.pdf"), Some(extra.to_string_lossy().into_owned()), FIXTURE.to_vec()).unwrap();
+    }
+    assert_eq!(h.state().recent.len(), 12);
+    assert_eq!(h.state().recent[0].name, "second.pdf", "pinned file stays at the top past the 12-file cap");
+    assert!(h.state().recent[0].pinned);
+    h.state_mut().open_bytes("second.pdf", Some(files[1].1.clone()), FIXTURE.to_vec()).unwrap();
+    assert!(h.state().recent[0].pinned, "reopening a pinned file preserves its pinned state");
+    h.state_mut().active = None;
+    h.run_steps(2);
+
+    // Clear Recent Files removes unpinned files and keeps pinned ones.
+    h.get_by_label("Clear Recent Files").click();
+    h.run_steps(2);
+    let remaining: Vec<(&str, bool)> = h.state().recent.iter().map(|r| (r.name.as_str(), r.pinned)).collect();
+    assert_eq!(remaining, [("second.pdf", true)], "Clear Recent Files keeps pinned files");
+
+    // Pinned state survives persistence round-trip, even when the file is temporarily missing at startup.
+    let saved = h.state().persist();
+    let _ = std::fs::remove_dir_all(&dir);
+    let mut restarted = PdfCraftApp::new();
+    restarted.restore(&saved);
+    assert_eq!(restarted.recent.len(), 1);
+    assert_eq!(restarted.recent[0].name, "second.pdf");
+    assert!(restarted.recent[0].pinned, "pinned state survives persist and restore");
+
+    // The unpin button stays reachable on a pinned row and unpins it when clicked.
+    h.get_by_label("Unpin second.pdf from Recent").click();
+    h.run_steps(2);
+    assert!(!h.state().recent[0].pinned, "clicking Unpin clears the pinned state");
+}
+
+/// Verify that clicking View more in the sidebar lists every tool group in the catalog.
 #[test]
 fn every_catalog_tool_is_listed_after_view_more() {
     let mut h = harness(|_| {});
@@ -125,6 +198,7 @@ fn every_catalog_tool_is_listed_after_view_more() {
     }
 }
 
+/// Verify that expanding All tools shows every category and filters tools by search query.
 #[test]
 fn expanded_all_tools_lists_every_tool_by_category_and_searches() {
     let mut h = harness(|_| {});
diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml
--- a/parity/acrobat-features.toml
+++ b/parity/acrobat-features.toml
@@ -682,8 +682,8 @@ milestone = "M3"
 status = "shipped"
 commands = ["file.open_recent", "file.clear_recent"]
 tools = []
-evidence = ["crates/ui-egui/tests/it/ui.rs::persistence_round_trips_and_tolerates_garbage", "crates/ui-egui/tests/it/commands.rs::the_open_recent_menu_lists_files_and_opens_one", "crates/ui-egui/tests/it/commands.rs::the_open_recent_menu_is_disabled_while_the_list_is_empty", "crates/ui-egui/tests/it/commands.rs::clear_recent_files_at_the_foot_of_open_recent_empties_the_list", "crates/ui-egui/tests/it/commands.rs::clear_recent_files_runs_from_the_palette_and_is_saved", "crates/ui-egui/tests/it/ui.rs::home_removes_one_recent_file_or_clears_them_all"]
-notes = "never reads other apps' lists; File ▸ Open Recent lists them (#290), an already-open file focuses its tab instead of reopening it, from any open path (#1620); Clear Recent Files at the foot of Open Recent, on Home and in the palette, and a remove button on each Home row (#430); missing: the row's remove button shows on pointer hover only (keyboard users clear the whole list)"
+evidence = ["crates/ui-egui/tests/it/ui.rs::persistence_round_trips_and_tolerates_garbage", "crates/ui-egui/tests/it/commands.rs::the_open_recent_menu_lists_files_and_opens_one", "crates/ui-egui/tests/it/commands.rs::the_open_recent_menu_is_disabled_while_the_list_is_empty", "crates/ui-egui/tests/it/commands.rs::clear_recent_files_at_the_foot_of_open_recent_empties_the_list", "crates/ui-egui/tests/it/commands.rs::clear_recent_files_runs_from_the_palette_and_is_saved", "crates/ui-egui/tests/it/ui.rs::home_removes_one_recent_file_or_clears_them_all", "crates/ui-egui/tests/it/ui.rs::home_pins_recent_file_to_top_keeps_it_across_clear_and_persists"]
+notes = "never reads other apps' lists; File ▸ Open Recent lists them (#290), an already-open file focuses its tab instead of reopening it, from any open path (#1620); Clear Recent Files at the foot of Open Recent, on Home and in the palette, a remove button on each Home row (#430), and a pin button on each Home row that keeps pinned files at the top across new opens, Clear Recent Files and restarts (#4772); missing: the row's remove button shows on pointer hover only (keyboard users clear the unpinned list)"
 
 [[feature]]
 id = "core.reopen-last-session"
