diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md
--- a/ATTRIBUTION.md
+++ b/ATTRIBUTION.md
@@ -15,6 +15,7 @@ next to them.
 | `assets/social/linkedin.svg` | LinkedIn logo, linking to ArtCraft's company page | Fonticons, Inc. ([Font Awesome Free](https://fontawesome.com) 6.7.2) | Font Awesome Free `svgs/brands/linkedin.svg`; white fill and a square view box, see [`assets/social/README.md`](assets/social/README.md) | CC BY 4.0. The logo is a trademark of LinkedIn Corporation. |
 | `assets/social/x.svg`, `youtube.svg`, `tiktok.svg`, `instagram.svg`, `discord.svg` | X, YouTube, TikTok, Instagram and Discord logos, linking to ArtCraft's accounts | [Simple Icons](https://simpleicons.org/) contributors | Simple Icons, `fill` set to white for tinting; see [`assets/social/README.md`](assets/social/README.md) | CC0 1.0. The logos are trademarks of X Corp., Google LLC, TikTok / ByteDance, Meta Platforms, Inc. and Discord Inc. |
 | `assets/social/github.svg`, `assets/social/globe.svg` | GitHub logo (linking to ArtCraft's GitHub) and a globe (linking to ArtCraft's website) | [Simple Icons](https://simpleicons.org/) contributors; [Lucide](https://lucide.dev) contributors | Simple Icons `github.svg` and Lucide `globe.svg` (lucide-static 0.468.0), white for tinting; see [`assets/social/README.md`](assets/social/README.md) | CC0 1.0 (GitHub logo; a trademark of GitHub, Inc.); ISC (globe) |
+| `assets/icons/*.svg` (nine files) | The icons at the start of each row in an app's `...` menu: release notes, download, open, show in folder, reinstall, uninstall, all releases, hide and unhide | [Lucide](https://lucide.dev) contributors, with portions held by Cole Bemis 2013-2022 as part of [Feather](https://feathericons.com) (MIT) | Lucide 0.468.0, taken from `lucide-icons/lucide` at tag `0.468.0`; `stroke` and, in `tag.svg`, the dot's `fill` set to white for tinting, see [`assets/icons/README.md`](assets/icons/README.md) | ISC; the notice is `assets/icons/LICENSE-Lucide.txt` and the `NOTICE` file |
 | `docs/screenshots/artcraft-launcher-grid.png`, `artcraft-launcher-table.png` | Screenshots of ArtCraft Launcher (macOS: grid view in dark theme, table view in light theme), used in the README | ArtCraft Launcher contributors | Captured from the app | MIT OR Apache-2.0 for the screenshot itself; the ArtCraft logo in it stays an ArtCraft trademark ([`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt)) and the app icons in it keep their own licenses (above) |
 | (not bundled) the pictures on each app's details page | Screenshots of the Craft Apps, fetched when a page is opened and kept in the launcher's cache folder | The apps' contributors | [storytold/craft-launcher-assets](https://github.com/storytold/craft-launcher-assets), pinned to a tag (`SCREENSHOTS_REV` in `crates/core/src/details.rs`); each picture's source and license are listed in that repository's `ATTRIBUTION.md` | Each app's own license for the interface (MIT OR Apache-2.0); the work shown in them is public domain or made in the app, as that file says |
 | `docs/brand/artcraft-logo.svg`, `artcraft-logo-white.svg` | ArtCraft wordmark, shown in the window header | ArtCraft Team | Supplied brand files, unmodified (same as PhotoCraft's `docs/brand/`) | **Not open source; not MIT or Apache-2.0.** ArtCraft trademark, [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt) |
diff --git a/NOTICE b/NOTICE
--- a/NOTICE
+++ b/NOTICE
@@ -18,6 +18,7 @@ list (author, source, license) is in ATTRIBUTION.md.
 Social service icons (assets/social/*.svg) — Simple Icons, CC0 1.0 (X, YouTube, TikTok, Instagram),
 and Font Awesome Free 6.7.2 by Fonticons, Inc., CC BY 4.0 (LinkedIn; https://fontawesome.com/license/free).
 The logos are trademarks of their respective owners and are used only to link to ArtCraft's accounts.
+Menu icons (assets/icons/*.svg) - Lucide 0.468.0, ISC. Copyright (c) for portions of Lucide is held by Cole Bemis 2013-2022 as part of Feather (MIT); all other copyright for Lucide is held by Lucide Contributors 2022. assets/icons/LICENSE-Lucide.txt carries the Lucide ISC notice and, for the icons Lucide derives from Feather (such as trash-2.svg), Feather's full MIT notice as well. assets/social/globe.svg comes from the same set under the same licence, so that one file covers it too.
 Inter font (assets/fonts/Inter-Variable.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-Inter.txt.
 Archivo font (assets/fonts/Archivo-Variable.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-Archivo.txt.
 Geist Mono font (assets/fonts/GeistMono-Variable.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-GeistMono.txt.
diff --git a/assets/icons/LICENSE-Lucide.txt b/assets/icons/LICENSE-Lucide.txt
new file mode 100644
--- /dev/null
+++ b/assets/icons/LICENSE-Lucide.txt
@@ -0,0 +1,42 @@
+ISC License
+
+Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+---
+
+Portions of Lucide, including `trash-2.svg`, are derived from Feather, which is
+released under the MIT License below.
+
+The MIT License (MIT)
+
+Copyright (c) 2013-2023 Cole Bemis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/assets/icons/README.md b/assets/icons/README.md
new file mode 100644
--- /dev/null
+++ b/assets/icons/README.md
@@ -0,0 +1,29 @@
+# Menu icons
+
+The small icon at the start of every row in an app's `...` menu. All from
+[Lucide](https://lucide.dev) 0.468.0, ISC licensed, on its own 24 px grid.
+
+| File | Lucide name | Row it marks |
+| --- | --- | --- |
+| `file-text.svg` | `file-text` | Release notes |
+| `download.svg` | `download` | Download again, and the per-platform download list |
+| `play.svg` | `play` | Open |
+| `folder-open.svg` | `folder-open` | Show in Explorer / Reveal in Finder / Open containing folder |
+| `rotate-ccw.svg` | `rotate-ccw` | Reinstall |
+| `trash-2.svg` | `trash-2` | Uninstall |
+| `tag.svg` | `tag` | All releases |
+| `eye-off.svg` | `eye-off` | Hide App |
+| `eye.svg` | `eye` | Unhide App |
+
+`globe.svg` (Website) and `github.svg` (GitHub repository) live in `assets/social/`.
+
+The `stroke="currentColor"` Lucide ships was changed to `stroke="#FFFFFF"`, the same trick
+`assets/social` uses, so the app can tint each icon to its own colour: muted grey on most rows,
+red on Uninstall. `tag.svg` carries one more of the same: the dot's `fill` is white too, or the
+tint would leave it black.
+
+Lucide is ISC. Its notice, which the licence asks to travel with every copy, is
+`LICENSE-Lucide.txt` beside these files and is also listed in the repository's `NOTICE`.
+
+Nothing here draws a character: the icons are images, so no font has to carry a symbol. The menu
+row paints the icon and then the label, and a screen reader is told only the label.
diff --git a/assets/icons/download.svg b/assets/icons/download.svg
new file mode 100644
--- /dev/null
+++ b/assets/icons/download.svg
@@ -0,0 +1,15 @@
+<svg
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="#FFFFFF"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
+  <polyline points="7 10 12 15 17 10" />
+  <line x1="12" x2="12" y1="15" y2="3" />
+</svg>
diff --git a/assets/icons/eye-off.svg b/assets/icons/eye-off.svg
new file mode 100644
--- /dev/null
+++ b/assets/icons/eye-off.svg
@@ -0,0 +1,16 @@
+<svg
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="#FFFFFF"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" />
+  <path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
+  <path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" />
+  <path d="m2 2 20 20" />
+</svg>
diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg
new file mode 100644
--- /dev/null
+++ b/assets/icons/eye.svg
@@ -0,0 +1,14 @@
+<svg
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="#FFFFFF"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
+  <circle cx="12" cy="12" r="3" />
+</svg>
diff --git a/assets/icons/file-text.svg b/assets/icons/file-text.svg
new file mode 100644
--- /dev/null
+++ b/assets/icons/file-text.svg
@@ -0,0 +1,17 @@
+<svg
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="#FFFFFF"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
+  <path d="M14 2v4a2 2 0 0 0 2 2h4" />
+  <path d="M10 9H8" />
+  <path d="M16 13H8" />
+  <path d="M16 17H8" />
+</svg>
diff --git a/assets/icons/folder-open.svg b/assets/icons/folder-open.svg
new file mode 100644
--- /dev/null
+++ b/assets/icons/folder-open.svg
@@ -0,0 +1,13 @@
+<svg
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="#FFFFFF"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2" />
+</svg>
diff --git a/assets/icons/play.svg b/assets/icons/play.svg
new file mode 100644
--- /dev/null
+++ b/assets/icons/play.svg
@@ -0,0 +1,13 @@
+<svg
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="#FFFFFF"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <polygon points="6 3 20 12 6 21 6 3" />
+</svg>
diff --git a/assets/icons/rotate-ccw.svg b/assets/icons/rotate-ccw.svg
new file mode 100644
--- /dev/null
+++ b/assets/icons/rotate-ccw.svg
@@ -0,0 +1,14 @@
+<svg
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="#FFFFFF"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
+  <path d="M3 3v5h5" />
+</svg>
diff --git a/assets/icons/tag.svg b/assets/icons/tag.svg
new file mode 100644
--- /dev/null
+++ b/assets/icons/tag.svg
@@ -0,0 +1,14 @@
+<svg
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="#FFFFFF"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" />
+  <circle cx="7.5" cy="7.5" r=".5" fill="#FFFFFF" />
+</svg>
diff --git a/assets/icons/trash-2.svg b/assets/icons/trash-2.svg
new file mode 100644
--- /dev/null
+++ b/assets/icons/trash-2.svg
@@ -0,0 +1,17 @@
+<svg
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="#FFFFFF"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M3 6h18" />
+  <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
+  <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
+  <line x1="10" x2="10" y1="11" y2="17" />
+  <line x1="14" x2="14" y1="11" y2="17" />
+</svg>
diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs
--- a/crates/ui-egui/src/icons.rs
+++ b/crates/ui-egui/src/icons.rs
@@ -86,6 +86,25 @@ pub fn social(service: &str) -> Option<ImageSource<'static>> {
     })
 }
 
+/// The small icon a menu row starts with, white so the row can tint it. Lucide, ISC, on its own
+/// 24 px grid; `assets/icons/README.md` and `ATTRIBUTION.md` say where each one came from.
+pub fn menu(name: &str) -> Option<ImageSource<'static>> {
+    Some(match name {
+        "notes" => egui::include_image!("../../../assets/icons/file-text.svg"),
+        "download" => egui::include_image!("../../../assets/icons/download.svg"),
+        "open" => egui::include_image!("../../../assets/icons/play.svg"),
+        "reveal" => egui::include_image!("../../../assets/icons/folder-open.svg"),
+        "reinstall" => egui::include_image!("../../../assets/icons/rotate-ccw.svg"),
+        "uninstall" => egui::include_image!("../../../assets/icons/trash-2.svg"),
+        "releases" => egui::include_image!("../../../assets/icons/tag.svg"),
+        "hide" => egui::include_image!("../../../assets/icons/eye-off.svg"),
+        "unhide" => egui::include_image!("../../../assets/icons/eye.svg"),
+        "website" => egui::include_image!("../../../assets/social/globe.svg"),
+        "github" => egui::include_image!("../../../assets/social/github.svg"),
+        _ => return None,
+    })
+}
+
 /// The ArtCraft "A" mark in blue, as supplied: the launcher's app icon and getartcraft.com's favicon.
 pub fn artcraft_mark() -> ImageSource<'static> {
     egui::include_image!("../../../docs/brand/artcraft-mark.svg")
diff --git a/crates/ui-egui/src/views.rs b/crates/ui-egui/src/views.rs
--- a/crates/ui-egui/src/views.rs
+++ b/crates/ui-egui/src/views.rs
@@ -864,33 +864,33 @@ impl LauncherUi {
         let manage = self.caps.manage_apps;
         let reveal = self.caps.reveal_label;
         widgets::more_menu(ui, |ui| {
-            if entry.release().is_some() && ui.button(tl!("Release notes")).clicked() {
+            if entry.release().is_some() && widgets::menu_item(ui, "notes", tl!("Release notes"), None).clicked() {
                 self.dialog = Some(Dialog::Notes(id.clone()));
                 ui.close();
             }
             // Only where Install would download: an up-to-date app would be reinstalled.
             let can_download_again =
                 manage && entry.ready && entry.paused.is_none() && matches!(status, AppStatus::Available | AppStatus::UpdateAvailable);
-            if can_download_again && ui.button(tl!("Download again")).clicked() {
+            if can_download_again && widgets::menu_item(ui, "download", tl!("Download again"), None).clicked() {
                 self.send(Command::Cancel { app: id.clone() });
                 self.install_app(&id);
                 ui.close();
             }
             if manage && status.is_installed() {
-                if ui.button(tl!("Open")).clicked() {
+                if widgets::menu_item(ui, "open", tl!("Open"), None).clicked() {
                     self.launch_app(&id);
                     ui.close();
                 }
-                if ui.button(t(reveal)).clicked() {
+                if widgets::menu_item(ui, "reveal", t(reveal), None).clicked() {
                     self.send(Command::Reveal { app: id.clone() });
                     ui.close();
                 }
-                if *status == AppStatus::UpToDate && ui.button(tl!("Reinstall")).clicked() {
+                if *status == AppStatus::UpToDate && widgets::menu_item(ui, "reinstall", tl!("Reinstall"), None).clicked() {
                     self.install_app(&id);
                     ui.close();
                 }
                 ui.separator();
-                if ui.button(RichText::new(tl!("Uninstall…")).color(Tokens::get(ui.ctx()).bad)).clicked() {
+                if widgets::menu_item(ui, "uninstall", tl!("Uninstall…"), Some(Tokens::get(ui.ctx()).bad)).clicked() {
                     self.dialog = Some(Dialog::ConfirmUninstall(id.clone()));
                     ui.close();
                 }
@@ -908,7 +908,7 @@ impl LauncherUi {
                 if !options.is_empty() {
                     ui.separator();
                     for (label, url) in options {
-                        if ui.button(label).clicked() {
+                        if widgets::menu_item(ui, "download", &label, None).clicked() {
                             ui.ctx().open_url(egui::OpenUrl::new_tab(url));
                             ui.close();
                         }
@@ -918,19 +918,21 @@ impl LauncherUi {
             // The drawer shows the links as buttons, so its menu leaves them out (but keeps Hide).
             if links {
                 ui.separator();
-                let link = |ui: &mut Ui, label: &str, url: String| {
-                    if ui.button(label).clicked() {
+                let link = |ui: &mut Ui, icon: &str, label: &str, url: String| {
+                    if widgets::menu_item(ui, icon, label, None).clicked() {
                         ui.ctx().open_url(egui::OpenUrl::new_tab(url));
                         ui.close();
                     }
                 };
-                link(ui, tl!("Website"), entry.app.website());
-                link(ui, tl!("GitHub repository"), entry.app.repo_url());
-                link(ui, tl!("All releases"), entry.app.releases_url());
+                link(ui, "website", tl!("Website"), entry.app.website());
+                link(ui, "github", tl!("GitHub repository"), entry.app.repo_url());
+                link(ui, "releases", tl!("All releases"), entry.app.releases_url());
             }
             ui.separator();
             let hidden = self.settings.hidden.contains(entry.app.id);
-            if ui.button(if hidden { tl!("Unhide App") } else { tl!("Hide App") }).clicked() {
+            if widgets::menu_item(ui, if hidden { "unhide" } else { "hide" }, if hidden { tl!("Unhide App") } else { tl!("Hide App") }, None)
+                .clicked()
+            {
                 if hidden {
                     self.settings.hidden.remove(entry.app.id);
                     if self.settings.hidden.is_empty() && self.settings.filter == Filter::Hidden {
diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs
--- a/crates/ui-egui/src/widgets.rs
+++ b/crates/ui-egui/src/widgets.rs
@@ -478,6 +478,38 @@ pub fn glyph_button(ui: &mut Ui, glyph: Glyph, tip: &str) -> Response {
     resp.on_hover_text(tip).on_hover_cursor(egui::CursorIcon::PointingHand)
 }
 
+/// A menu row: an icon, the label, and a full-width hover. A screen reader hears only `label`, so
+/// the icon is decoration and anything looking for the row still finds it by its words.
+pub fn menu_item(ui: &mut Ui, icon: &str, label: &str, colour: Option<Color32>) -> Response {
+    let t = Tokens::get(ui.ctx());
+    let height = 26.0;
+    let ink = colour.unwrap_or(t.ink);
+    let font = egui::TextStyle::Body.resolve(ui.style());
+    // The row must be wide enough for the longest label: a translated one can be much longer than
+    // the English it was written from, and a label that runs past the row cannot be clicked.
+    let label_width = ui.painter().layout_no_wrap(label.to_owned(), font.clone(), ink).size().x;
+    let needed = 28.0 + label_width.ceil() + 16.0;
+    // The row is exactly as wide as it needs to be, never as wide as the menu could be: asking for
+    // `available_width` here inflates the popup, because a menu is measured before it has a width.
+    // 200 is the floor `more_menu` already asks for, so short labels keep the old look.
+    let width = needed.max(200.0);
+    let (rect, response) = ui.allocate_exact_size(vec2(width, height), Sense::click());
+    response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), label));
+    if response.hovered() || response.has_focus() {
+        // The hover fills the row and runs on to the edge of the menu, so it does not stop short
+        // of the panel and look like a second, smaller button.
+        let span = egui::Rect::from_min_max(egui::pos2(ui.max_rect().left(), rect.top()), egui::pos2(ui.max_rect().right(), rect.bottom()));
+        ui.painter().rect_filled(span, CornerRadius::ZERO, t.sunken);
+    }
+    let middle = egui::pos2(rect.left() + 14.0, rect.center().y);
+    if let Some(source) = crate::icons::menu(icon) {
+        let tint = if colour.is_some() { ink } else { t.muted };
+        egui::Image::new(source).fit_to_exact_size(vec2(14.0, 14.0)).tint(tint).paint_at(ui, egui::Rect::from_center_size(middle, vec2(14.0, 14.0)));
+    }
+    ui.painter().text(egui::pos2(rect.left() + 28.0, rect.center().y), egui::Align2::LEFT_CENTER, label, font, ink);
+    response
+}
+
 /// The "…" button that opens an app's menu: an outlined square like [`glyph_button`].
 pub fn more_menu<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) {
     let t = Tokens::get(ui.ctx());
