diff --git a/frontend/taskdeck-web/src/components/paper/PaperCommandPalette.vue b/frontend/taskdeck-web/src/components/paper/PaperCommandPalette.vue
index 75e5c983a..e5beb4cf5 100644
--- a/frontend/taskdeck-web/src/components/paper/PaperCommandPalette.vue
+++ b/frontend/taskdeck-web/src/components/paper/PaperCommandPalette.vue
@@ -231,6 +231,7 @@ watch(orderedItems, (items) => {
role="dialog"
aria-modal="true"
aria-label="Command palette"
+ data-shell-surface="command-palette"
@click.self="handleClose"
@keydown.escape.prevent="handleClose"
>
diff --git a/frontend/taskdeck-web/src/components/paper/PaperShortcutsOverlay.vue b/frontend/taskdeck-web/src/components/paper/PaperShortcutsOverlay.vue
index a35a25458..222935ab4 100644
--- a/frontend/taskdeck-web/src/components/paper/PaperShortcutsOverlay.vue
+++ b/frontend/taskdeck-web/src/components/paper/PaperShortcutsOverlay.vue
@@ -56,6 +56,7 @@ function onBackdropClick() {
role="dialog"
aria-modal="true"
aria-labelledby="paper-shortcuts-title"
+ data-shell-surface="keyboard-help"
@click.self="onBackdropClick"
>
diff --git a/frontend/taskdeck-web/src/components/shell/AppShell.vue b/frontend/taskdeck-web/src/components/shell/AppShell.vue
index ea64b183c..bdd04d8cb 100644
--- a/frontend/taskdeck-web/src/components/shell/AppShell.vue
+++ b/frontend/taskdeck-web/src/components/shell/AppShell.vue
@@ -11,8 +11,9 @@ import { useViewportMode } from '../../composables/useViewportMode'
import { provideShellKeyboardHelp } from '../../composables/useShellKeyboardHelp'
import {
APP_SHELL_SHORTCUT_BINDINGS,
+ strokeMatches,
+ type AppShellShortcutAction,
type AppShellShortcutBinding,
- type ShortcutStroke,
} from '../../utils/keyboardShortcuts'
import CaptureModal from '../common/CaptureModal.vue'
import OfflineBanner from './OfflineBanner.vue'
@@ -148,18 +149,31 @@ function isTextEntryTarget(target: EventTarget | null): boolean {
return target.matches(selector) || target.closest(selector) !== null
}
+/**
+ * Only a surface that declares itself MODAL owns the keyboard (#1968).
+ *
+ * A bare `[role="dialog"]` is not enough, and matching it was a live defect:
+ * `CardModal` keeps `role="dialog"` in both presentations but sets
+ * `aria-modal` only outside the inspector, so the Paper desktop card inspector
+ * -- a sticky side panel that traps nothing and leaves the board usable --
+ * counted as a keyboard-owning surface. That made `?`, `mod+k` and
+ * `mod+shift+c` dead for as long as a card was open for reading, and stopped
+ * every non-Escape key pressed outside the panel.
+ *
+ * `dialog[open]` and `[role="alertdialog"]` stay: a native open `