diff --git a/docs/superpowers/plans/2026-04-23-outlook-relook.md b/docs/superpowers/plans/2026-04-23-outlook-relook.md
index 26cfa4c..48e36ab 100644
--- a/docs/superpowers/plans/2026-04-23-outlook-relook.md
+++ b/docs/superpowers/plans/2026-04-23-outlook-relook.md
@@ -23,6 +23,7 @@ outlook-relook/
│ ├── observer.js # MutationObserver: suppress elements based on active settings
│ ├── selectors.js # Selector registry: logical name → primary + fallback selectors
│ ├── behavior.js # JS behavior patches (auto-advance, hover suppress, etc.)
+│ ├── keyboard.js # Gmail-style keyboard navigation & multi-select
│ └── injector.js # DOM injection (mark-all-read button, folder jump dialog)
├── popup/
│ ├── popup.html # Settings panel markup
@@ -68,6 +69,7 @@ outlook-relook/
"content/selectors.js",
"content/observer.js",
"content/behavior.js",
+ "content/keyboard.js",
"content/injector.js",
"content/content.js"
],
@@ -241,6 +243,9 @@ window.OutlookRelook.DEFAULTS = {
autoResizeCompose: true,
throttleNotifications: false,
+ // Keyboard Navigation
+ keyboardMultiSelect: true,
+
// Quick Actions
markAllReadButton: true,
quickFolderJump: true,
@@ -2722,6 +2727,20 @@ body::-webkit-scrollbar-thumb {
+
+
+
+
+
+
+
+
+
+ j/k or arrows to navigate, x/Space to select, # delete, e archive, Shift+i read, Shift+u unread, v move, Esc deselect
+
+
+
+
@@ -2827,6 +2846,7 @@ Handles loading settings into the UI, saving changes on toggle, density preset b
stickyReplyBar: true,
autoResizeCompose: true,
throttleNotifications: false,
+ keyboardMultiSelect: true,
markAllReadButton: true,
quickFolderJump: true,
};
@@ -3214,6 +3234,7 @@ A Chrome extension that reskins Outlook Web App (outlook.office.com) with minima
- **~40 granular toggles** for density, element hiding, readability, and behavior
- **MutationObserver** suppresses dynamically injected clutter (Copilot, banners, suggested replies)
- **Behavior patches:** auto-collapse ribbon, suppress contact hover cards, auto-advance after delete, toast management
+- **Gmail-style keyboard navigation:** j/k to move, x/Space to multi-select, # delete, e archive, Shift+i/u read/unread, v move
- **Quick actions:** "Mark all as read" button, Ctrl+Shift+K folder jump
- **Settings sync** across Chrome instances via chrome.storage.sync
- **Export/Import/Reset** settings
@@ -3232,6 +3253,20 @@ Click the extension icon to open the settings panel. Changes apply immediately
### Keyboard Shortcuts
+**Message list (Gmail-style):**
+- `j` / `Down Arrow` — Next message
+- `k` / `Up Arrow` — Previous message
+- `x` / `Space` — Toggle select
+- `Shift+Down` / `Shift+Up` — Extend selection
+- `#` — Delete selected
+- `e` — Archive selected
+- `Shift+i` — Mark read
+- `Shift+u` — Mark unread
+- `v` — Move to folder
+- `Escape` — Deselect all
+- `Enter` / `o` — Open message
+
+**Global:**
- `Ctrl+Shift+K` (or `Cmd+Shift+K` on Mac) — Quick folder jump
## Development
@@ -3287,10 +3322,589 @@ git commit -m "docs: README with install, usage, and development guide"
---
+### Task 14: Gmail-Style Keyboard Navigation & Multi-Select
+
+**Files:**
+- Create: `content/keyboard.js`
+- Modify: `content/content.js` (wire up Keyboard module)
+- Modify: `themes/base.css` (add focus/selected styles)
+
+This is the most complex single feature. It adds a custom focus cursor and multi-select system to OWA's message list, with Gmail-style keyboard shortcuts for bulk actions.
+
+- [ ] **Step 1: Add keyboard focus/select CSS to base.css**
+
+Append the following to the end of `themes/base.css`:
+
+```css
+/* ============================================================
+ KEYBOARD NAVIGATION
+ ============================================================ */
+
+/* Focus cursor — the message the keyboard is currently pointing at */
+.or-kb-focused {
+ outline: 2px solid var(--or-accent, #0078d4) !important;
+ outline-offset: -2px;
+ position: relative;
+ z-index: 1;
+}
+
+/* Selected/checked messages */
+.or-kb-selected {
+ background-color: rgba(0, 120, 212, 0.08) !important;
+}
+
+html[data-outlook-relook-scheme="dark"] .or-kb-selected {
+ background-color: rgba(100, 181, 246, 0.12) !important;
+}
+
+/* Selection indicator — small left bar */
+.or-kb-selected::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 3px;
+ background: var(--or-accent, #0078d4);
+}
+
+/* Selection count badge (injected by keyboard.js) */
+.or-kb-selection-count {
+ position: fixed;
+ bottom: 16px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--or-bg-primary, #333);
+ color: var(--or-text-primary, #fff);
+ border: 1px solid var(--or-border, #555);
+ padding: 6px 16px;
+ border-radius: 20px;
+ font-size: 13px;
+ font-family: system-ui, sans-serif;
+ z-index: 999998;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
+ pointer-events: none;
+ transition: opacity 0.15s;
+}
+```
+
+- [ ] **Step 2: Create keyboard.js**
+
+```js
+// Outlook Relook — Gmail-Style Keyboard Navigation & Multi-Select
+// Adds keyboard focus cursor and multi-select to OWA's message list.
+// Gated by the keyboardMultiSelect setting.
+
+window.OutlookRelook = window.OutlookRelook || {};
+
+window.OutlookRelook.Keyboard = (function () {
+ 'use strict';
+
+ var OR = window.OutlookRelook;
+ var currentSettings = {};
+ var cleanupFns = [];
+
+ // State
+ var focusedIndex = -1; // Index of the focused message in the list
+ var selectedSet = new Set(); // Set of selected message DOM elements
+ var countBadge = null; // Selection count badge element
+
+ // --- Helpers ---
+
+ function getMessageItems() {
+ // Get all message items from the message list
+ var items = document.querySelectorAll(
+ '[role="listbox"] [role="option"], [role="list"] [role="listitem"]'
+ );
+ return Array.from(items);
+ }
+
+ function isComposeOrDialogActive() {
+ var active = document.activeElement;
+ if (!active) return false;
+
+ // Check if focus is in a compose area, search bar, or dialog
+ var tag = active.tagName.toLowerCase();
+ if (tag === 'input' || tag === 'textarea') return true;
+ if (active.getAttribute('contenteditable') === 'true') return true;
+ if (active.closest('[role="dialog"]')) return true;
+ if (active.closest('[role="search"]')) return true;
+ if (active.closest('[aria-label*="compose" i]')) return true;
+ if (active.closest('[aria-label*="New message" i]')) return true;
+
+ return false;
+ }
+
+ function setFocus(items, index) {
+ // Remove old focus
+ var oldFocused = document.querySelector('.or-kb-focused');
+ if (oldFocused) oldFocused.classList.remove('or-kb-focused');
+
+ if (index < 0 || index >= items.length) return;
+
+ focusedIndex = index;
+ var item = items[index];
+ item.classList.add('or-kb-focused');
+
+ // Scroll into view if needed
+ item.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
+ }
+
+ function toggleSelect(item) {
+ if (selectedSet.has(item)) {
+ selectedSet.delete(item);
+ item.classList.remove('or-kb-selected');
+ } else {
+ selectedSet.add(item);
+ item.classList.add('or-kb-selected');
+ }
+ updateCountBadge();
+ }
+
+ function clearSelection() {
+ selectedSet.forEach(function (item) {
+ item.classList.remove('or-kb-selected');
+ });
+ selectedSet.clear();
+ updateCountBadge();
+ }
+
+ function getActionTargets(items) {
+ // If messages are selected, return those. Otherwise return the focused message.
+ if (selectedSet.size > 0) {
+ return Array.from(selectedSet);
+ }
+ if (focusedIndex >= 0 && focusedIndex < items.length) {
+ return [items[focusedIndex]];
+ }
+ return [];
+ }
+
+ // --- Selection count badge ---
+
+ function createCountBadge() {
+ var badge = document.createElement('div');
+ badge.className = 'or-kb-selection-count';
+ badge.style.opacity = '0';
+ document.body.appendChild(badge);
+ return badge;
+ }
+
+ function updateCountBadge() {
+ if (!countBadge) countBadge = createCountBadge();
+ var count = selectedSet.size;
+ if (count === 0) {
+ countBadge.style.opacity = '0';
+ } else {
+ countBadge.textContent = count + ' selected';
+ countBadge.style.opacity = '1';
+ }
+ }
+
+ // --- Actions ---
+ // Each action simulates what a user would do in OWA to perform the operation.
+ // We click the message to select it in OWA, then trigger the toolbar action.
+
+ function performAction(targets, actionFn) {
+ if (targets.length === 0) return;
+
+ // Process targets one at a time with a small delay between each
+ var i = 0;
+ function processNext() {
+ if (i >= targets.length) {
+ // After all targets processed, clear selection
+ clearSelection();
+ return;
+ }
+ var target = targets[i];
+ i++;
+
+ // Click the message to make it OWA-selected
+ target.click();
+
+ // Small delay to let OWA register the selection, then perform action
+ setTimeout(function () {
+ actionFn(target);
+ // Delay before next target
+ setTimeout(processNext, 150);
+ }, 100);
+ }
+ processNext();
+ }
+
+ function actionDelete(targets) {
+ performAction(targets, function () {
+ // Try keyboard shortcut first (Delete key)
+ var deleteEvent = new KeyboardEvent('keydown', {
+ key: 'Delete',
+ code: 'Delete',
+ bubbles: true,
+ cancelable: true
+ });
+ document.activeElement.dispatchEvent(deleteEvent);
+
+ // Fallback: find and click the delete button in the toolbar
+ var deleteBtn = document.querySelector(
+ '[aria-label*="Delete" i][role="button"], [aria-label*="delete" i][role="menuitem"]'
+ );
+ if (deleteBtn) deleteBtn.click();
+ });
+ }
+
+ function actionArchive(targets) {
+ performAction(targets, function () {
+ var archiveBtn = document.querySelector(
+ '[aria-label*="Archive" i][role="button"], [aria-label*="archive" i][role="menuitem"]'
+ );
+ if (archiveBtn) {
+ archiveBtn.click();
+ } else {
+ console.warn('[Outlook Relook] Archive button not found');
+ }
+ });
+ }
+
+ function actionMarkRead(targets) {
+ performAction(targets, function () {
+ var readBtn = document.querySelector(
+ '[aria-label*="Mark as read" i][role="button"], [aria-label*="Mark as read" i][role="menuitem"]'
+ );
+ if (readBtn) {
+ readBtn.click();
+ } else {
+ // Try context menu approach
+ triggerContextMenuAction(/mark as read/i);
+ }
+ });
+ }
+
+ function actionMarkUnread(targets) {
+ performAction(targets, function () {
+ var unreadBtn = document.querySelector(
+ '[aria-label*="Mark as unread" i][role="button"], [aria-label*="Mark as unread" i][role="menuitem"]'
+ );
+ if (unreadBtn) {
+ unreadBtn.click();
+ } else {
+ triggerContextMenuAction(/mark as unread/i);
+ }
+ });
+ }
+
+ function actionMove(targets) {
+ // For move, we just need to trigger OWA's move dialog on the current selection
+ if (targets.length > 0) {
+ // Click the first target to ensure something is selected
+ targets[0].click();
+
+ setTimeout(function () {
+ var moveBtn = document.querySelector(
+ '[aria-label*="Move to" i][role="button"], [aria-label*="Move" i][role="menuitem"]'
+ );
+ if (moveBtn) {
+ moveBtn.click();
+ } else {
+ triggerContextMenuAction(/move to/i);
+ }
+ }, 100);
+ }
+ }
+
+ function triggerContextMenuAction(pattern) {
+ // Open context menu on the focused/selected element
+ var focused = document.querySelector('.or-kb-focused');
+ if (!focused) return;
+
+ var rect = focused.getBoundingClientRect();
+ var contextEvent = new MouseEvent('contextmenu', {
+ bubbles: true,
+ cancelable: true,
+ clientX: rect.x + 10,
+ clientY: rect.y + 10,
+ });
+ focused.dispatchEvent(contextEvent);
+
+ setTimeout(function () {
+ var menuItems = document.querySelectorAll('[role="menuitem"]');
+ for (var j = 0; j < menuItems.length; j++) {
+ if (pattern.test(menuItems[j].textContent)) {
+ menuItems[j].click();
+ return;
+ }
+ }
+ // Close context menu if not found
+ document.body.click();
+ }, 300);
+ }
+
+ // --- Key handler ---
+
+ function handleKeydown(e) {
+ // Skip if keyboard mode is off or compose/dialog is active
+ if (!currentSettings.keyboardMultiSelect) return;
+ if (isComposeOrDialogActive()) return;
+
+ var items = getMessageItems();
+ if (items.length === 0) return;
+
+ var key = e.key;
+ var shift = e.shiftKey;
+
+ // Initialize focus if not set
+ if (focusedIndex < 0 || focusedIndex >= items.length) {
+ // Find the currently OWA-selected item, or start at 0
+ for (var f = 0; f < items.length; f++) {
+ if (items[f].getAttribute('aria-selected') === 'true') {
+ focusedIndex = f;
+ break;
+ }
+ }
+ if (focusedIndex < 0) focusedIndex = 0;
+ }
+
+ var handled = true;
+ var targets;
+
+ switch (key) {
+ case 'j':
+ case 'ArrowDown':
+ if (shift) {
+ // Select current and move down
+ if (focusedIndex >= 0 && focusedIndex < items.length) {
+ if (!selectedSet.has(items[focusedIndex])) {
+ toggleSelect(items[focusedIndex]);
+ }
+ }
+ if (focusedIndex < items.length - 1) {
+ setFocus(items, focusedIndex + 1);
+ toggleSelect(items[focusedIndex]);
+ }
+ } else {
+ if (focusedIndex < items.length - 1) {
+ setFocus(items, focusedIndex + 1);
+ }
+ }
+ break;
+
+ case 'k':
+ case 'ArrowUp':
+ if (shift) {
+ // Select current and move up
+ if (focusedIndex >= 0 && focusedIndex < items.length) {
+ if (!selectedSet.has(items[focusedIndex])) {
+ toggleSelect(items[focusedIndex]);
+ }
+ }
+ if (focusedIndex > 0) {
+ setFocus(items, focusedIndex - 1);
+ toggleSelect(items[focusedIndex]);
+ }
+ } else {
+ if (focusedIndex > 0) {
+ setFocus(items, focusedIndex - 1);
+ }
+ }
+ break;
+
+ case 'x':
+ case ' ':
+ // Toggle select on focused message
+ e.preventDefault(); // Prevent page scroll on Space
+ if (focusedIndex >= 0 && focusedIndex < items.length) {
+ toggleSelect(items[focusedIndex]);
+ }
+ break;
+
+ case '#':
+ // Delete selected/focused messages
+ targets = getActionTargets(items);
+ actionDelete(targets);
+ break;
+
+ case 'e':
+ // Archive selected/focused messages
+ targets = getActionTargets(items);
+ actionArchive(targets);
+ break;
+
+ case 'I':
+ // Shift+i — Mark as read
+ if (shift) {
+ targets = getActionTargets(items);
+ actionMarkRead(targets);
+ } else {
+ handled = false;
+ }
+ break;
+
+ case 'U':
+ // Shift+u — Mark as unread
+ if (shift) {
+ targets = getActionTargets(items);
+ actionMarkUnread(targets);
+ } else {
+ handled = false;
+ }
+ break;
+
+ case 'v':
+ // Move selected messages
+ targets = getActionTargets(items);
+ actionMove(targets);
+ break;
+
+ case 'Escape':
+ // Deselect all
+ clearSelection();
+ break;
+
+ case 'Enter':
+ case 'o':
+ // Open focused message in reading pane
+ if (focusedIndex >= 0 && focusedIndex < items.length) {
+ items[focusedIndex].click();
+ }
+ break;
+
+ default:
+ handled = false;
+ }
+
+ if (handled) {
+ e.stopPropagation();
+ // Only preventDefault for keys we handle (except Enter which OWA should also process)
+ if (key !== 'Enter' && key !== 'o') {
+ e.preventDefault();
+ }
+ }
+ }
+
+ // --- Cleanup stale selections when message list re-renders ---
+
+ function setupListObserver() {
+ var listObserver = new MutationObserver(function () {
+ // Remove stale entries from selectedSet (elements no longer in DOM)
+ selectedSet.forEach(function (item) {
+ if (!document.contains(item)) {
+ selectedSet.delete(item);
+ }
+ });
+ updateCountBadge();
+
+ // Reset focusedIndex if the focused item is gone
+ var focusedEl = document.querySelector('.or-kb-focused');
+ if (!focusedEl || !document.contains(focusedEl)) {
+ focusedIndex = -1;
+ }
+ });
+
+ // Watch the message list container for child changes
+ var checkInterval = setInterval(function () {
+ var list = document.querySelector('[role="listbox"], [role="list"]');
+ if (list) {
+ clearInterval(checkInterval);
+ listObserver.observe(list, { childList: true, subtree: true });
+ }
+ }, 1000);
+
+ cleanupFns.push(function () {
+ clearInterval(checkInterval);
+ listObserver.disconnect();
+ });
+ }
+
+ // --- Public API ---
+
+ function start(settings) {
+ currentSettings = settings;
+ if (!settings.keyboardMultiSelect) return;
+
+ document.addEventListener('keydown', handleKeydown, true);
+ setupListObserver();
+
+ console.log('[Outlook Relook] Keyboard navigation started');
+ cleanupFns.push(function () {
+ document.removeEventListener('keydown', handleKeydown, true);
+ });
+ }
+
+ function updateSettings(settings) {
+ stop();
+ currentSettings = settings;
+ start(settings);
+ }
+
+ function stop() {
+ for (var i = 0; i < cleanupFns.length; i++) {
+ try { cleanupFns[i](); } catch (e) { /* ignore */ }
+ }
+ cleanupFns = [];
+
+ // Clean up DOM state
+ clearSelection();
+ var focused = document.querySelector('.or-kb-focused');
+ if (focused) focused.classList.remove('or-kb-focused');
+ focusedIndex = -1;
+
+ if (countBadge) {
+ countBadge.remove();
+ countBadge = null;
+ }
+ }
+
+ return { start: start, updateSettings: updateSettings, stop: stop };
+})();
+```
+
+- [ ] **Step 3: Wire keyboard into content.js**
+
+In `content/content.js`, inside `init()`, after `OR.Behavior.start(settings)` and before `OR.Injector.start(settings)`, add:
+
+```js
+ // Start keyboard navigation
+ OR.Keyboard.start(settings);
+```
+
+Inside the `chrome.storage.onChanged` listener, after `OR.Behavior.updateSettings(updated)`, add:
+
+```js
+ // Update keyboard navigation
+ OR.Keyboard.updateSettings(updated);
+```
+
+- [ ] **Step 4: Verify keyboard navigation**
+
+1. Reload extension
+2. Open `outlook.office.com`
+3. Verify in console: `[Outlook Relook] Keyboard navigation started`
+4. Click on the message list area, then:
+ - Press `j` — focus cursor (blue outline) should move to the next message
+ - Press `k` — focus cursor should move to the previous message
+ - Press `x` — focused message should get a selected highlight (light blue background + left accent bar)
+ - Press `j`, `x`, `j`, `x` — three messages should now be selected, badge shows "3 selected"
+ - Press `#` — all selected messages should be deleted
+ - Select two messages with `x`, press `e` — should archive them
+ - Press `Shift+i` on a message — should mark as read
+ - Press `Shift+u` on a message — should mark as unread
+ - Press `v` — OWA's "Move to" dialog should open
+ - Press `Escape` — all selections should clear
+5. Start typing in the search bar — keyboard shortcuts should NOT fire
+6. Open a compose window — keyboard shortcuts should NOT fire
+7. Toggle the setting off in the popup — keyboard navigation should stop
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add content/keyboard.js content/content.js themes/base.css
+git commit -m "feat: Gmail-style keyboard navigation and multi-select for message list"
+```
+
+---
+
## Self-Review Results
**Spec coverage:** All spec sections are covered:
-- Architecture (Tasks 1-2), Theme System (Tasks 4-6), Selector Strategy (Task 3), Settings Panel (Tasks 10-11), all toggle categories (distributed across Tasks 4, 7, 8, 9), File Structure (Task 1), Development & Testing (Task 12-13). Verified each spec requirement maps to a task.
+- Architecture (Tasks 1-2), Theme System (Tasks 4-6), Selector Strategy (Task 3), Settings Panel (Tasks 10-11), all toggle categories (distributed across Tasks 4, 7, 8, 9), Keyboard Navigation (Task 14), File Structure (Task 1), Development & Testing (Task 12-13). Verified each spec requirement maps to a task.
**Placeholder scan:** No TBDs or TODOs. All code blocks are complete. OWA-specific selectors are noted with a clear strategy (inspect live DOM) rather than left as placeholders.
@@ -3298,6 +3912,7 @@ git commit -m "docs: README with install, usage, and development guide"
- `window.OutlookRelook` namespace used consistently
- `OR.Observer.start/updateSettings/stop` API matches between `observer.js` (Task 7) and `content.js` (Task 7 wiring)
- `OR.Behavior.start/updateSettings/stop` API matches between `behavior.js` (Task 8) and `content.js` (Task 8 wiring)
+- `OR.Keyboard.start/updateSettings/stop` API matches between `keyboard.js` (Task 14) and `content.js` (Task 14 wiring)
- `OR.Injector.start/updateSettings/stop` API matches between `injector.js` (Task 9) and `content.js` (Task 9 wiring)
- `OR.loadSettings`, `OR.saveSettings`, `OR.DEFAULTS`, `OR.DENSITY_PRESETS` match between `settings-defaults.js` (Task 2) and `popup.js` (Task 11, duplicated for popup context)
- `OR.resolveSelector`, `OR.resolveSelectorString` match between `selectors.js` (Task 3) and consumers in `observer.js`, `behavior.js`, `injector.js`
diff --git a/docs/superpowers/specs/2026-04-23-outlook-relook-design.md b/docs/superpowers/specs/2026-04-23-outlook-relook-design.md
index 58fdfab..7c01d2b 100644
--- a/docs/superpowers/specs/2026-04-23-outlook-relook-design.md
+++ b/docs/superpowers/specs/2026-04-23-outlook-relook-design.md
@@ -175,6 +175,40 @@ Changing the density preset sets all individual density toggles to the preset's
| Auto-resize compose window | Boolean | On |
| Throttle desktop notifications | Boolean | Off |
+### Keyboard Navigation
+
+Gmail-style keyboard navigation and multi-select for the message list. OWA lacks the ability to select multiple messages and act on them purely from the keyboard — this is the single biggest functionality gap vs Gmail.
+
+| Toggle | Type | Default |
+|--------|------|---------|
+| Keyboard multi-select mode | Boolean | On |
+
+**Key bindings (active when message list is focused, not during compose):**
+
+| Key | Action |
+|-----|--------|
+| `j` / `Down Arrow` | Move focus to next message |
+| `k` / `Up Arrow` | Move focus to previous message |
+| `x` or `Space` | Toggle select on focused message (multi-select) |
+| `Shift+j` / `Shift+Down` | Select and move to next (extend selection) |
+| `Shift+k` / `Shift+Up` | Select and move to previous (extend selection) |
+| `#` | Delete selected message(s) |
+| `e` | Archive selected message(s) |
+| `Shift+i` | Mark selected as read |
+| `Shift+u` | Mark selected as unread |
+| `v` | Move selected (open OWA's move-to-folder dialog) |
+| `Escape` | Deselect all |
+| `Enter` / `o` | Open focused message in reading pane |
+
+**Implementation approach:**
+
+- The extension manages its own "focus cursor" on the message list, visually distinguished from OWA's native selection (e.g., a left-border highlight or subtle background tint)
+- A Set tracks "checked" (multi-selected) message elements, visually marked with a checkbox indicator or distinct background
+- Action keys (`#`, `e`, `Shift+i`, etc.) iterate over checked messages and dispatch OWA's native actions for each (via toolbar button clicks, context menu triggers, or keyboard shortcut forwarding)
+- If no messages are checked, actions apply to the currently focused message (single-select behavior, matching Gmail)
+- All keyboard handling is suppressed when a compose window, search bar, or dialog has focus — detected via `activeElement` checks
+- A new file `content/keyboard.js` encapsulates all keyboard navigation logic, separate from `behavior.js`
+
### Quick Actions (injected UI)
| Toggle | Type | Default |
@@ -202,6 +236,7 @@ outlook-relook/
│ ├── observer.js # MutationObserver logic, element suppression
│ ├── selectors.js # Selector registry (logical name -> strategies)
│ ├── behavior.js # JS behavior tweaks
+│ ├── keyboard.js # Gmail-style keyboard navigation & multi-select
│ └── injector.js # DOM injection (mark-all-read, folder jump)
├── themes/
│ ├── base.css # Shared density/spacing/hiding overrides