---
phase: 28-dashboard-shell-context-detection
plan: 02
type: execute
wave: 2
depends_on:
  - 28-01  # Index utilities (_loadIndex, _findEntry, _filterByDomain) + mode shell CSS must exist
files_modified:
  - paperforge/plugin/main.js
  - paperforge/plugin/styles.css
autonomous: true
requirements:
  - DASH-01  # .base file -> collection mode
  - DASH-02  # .md with zotero_key -> per-paper mode
  - DASH-03  # other files / no file -> global mode
  - DASH-04  # file switch -> auto-refresh correct mode
  - REFR-01  # formal-library.json change -> refresh
  - REFR-02  # active file change -> refresh

must_haves:
  truths:
    - "Opening a .base file switches dashboard to collection mode with domain name in header"
    - "Opening a .md file with zotero_key in frontmatter switches dashboard to per-paper mode with paper title in header"
    - "Opening any other file or no file switches dashboard to global mode"
    - "Tab click or Ctrl+Tab triggers mode switch within observable time, previous mode data released"
    - "External modification of formal-library.json triggers dashboard refresh within 2 seconds, preserving current mode"
    - "Index cache invalidates on file modify and file switch events"
    - "Quick Actions bar remains visible in all three modes"
  artifacts:
    - path: "paperforge/plugin/main.js"
      provides: "_currentMode, _detectAndSwitch, _switchMode, event subscriptions, mode-aware header render, mode-specific render stubs, onClose cleanup"
      min_lines: 150
    - path: "paperforge/plugin/styles.css"
      provides: "Contextual action card variants (if any CSS additions needed)"
      min_lines: 0
  key_links:
    - from: "PaperForgeStatusView.onOpen()"
      to: "_detectAndSwitch()"
      via: "call at end of onOpen after structural build"
    - from: "app.workspace.on('active-leaf-change')"
      to: "_detectAndSwitch()"
      via: "debounced callback with 300ms delay"
    - from: "app.vault.on('modify', file)"
      to: "_refreshCurrentMode()"
      via: "filter: file.path includes formal-library.json, then invalidate cache + re-detect"
    - from: "_detectAndSwitch()"
      to: "_switchMode(mode)"
      via: "determines mode from active file, then calls _switchMode"
    - from: "_switchMode(mode)"
      to: "_renderGlobalMode / _renderPaperMode / _renderCollectionMode"
      via: "switch statement, each clears _contentEl.empty() then renders"
---

<objective>
Implement context detection, mode switching, auto-refresh, and mode-aware header rendering in PaperForgeStatusView. The dashboard automatically detects the active file type (`.base`, `.md` with `zotero_key`, or other/no file) and switches to the correct view mode without manual intervention. It also auto-refreshes when the canonical index changes or the user switches files.

Purpose: Deliver DASH-01 through DASH-04 and REFR-01 through REFR-02. Phase 29 and 30 will flesh out the per-paper and collection views with actual components; this plan provides the routing shell that makes those phases possible.

Output: `paperforge/plugin/main.js` with mode detection, switching, event subscriptions, and mode-aware rendering. `paperforge/plugin/styles.css` with contextual action variant CSS.
</objective>

<execution_context>
@C:/Users/Lin/.opencode/get-shit-done/workflows/execute-plan.md
@C:/Users/Lin/.opencode/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/28-dashboard-shell-context-detection/28-CONTEXT.md
@.planning/phases/28-dashboard-shell-context-detection/28-01-SUMMARY.md
@paperforge/plugin/main.js
@paperforge/plugin/styles.css
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add _currentMode state, _detectAndSwitch(), and _switchMode() infrastructure</name>
  <files>paperforge/plugin/main.js</files>

  <read_first>
  - paperforge/plugin/main.js — PaperForgeStatusView class (lines 210-815), especially:
    * `onOpen()` at lines 217-225 (must refactor to call _detectAndSwitch)
    * `_buildPanel()` at lines 232-280 (must refactor to separate structural header from content)
    * `_fetchStats()` at lines 285-375 (will become part of global mode render)
    * `_renderOcr()` at lines 432-498 (will become part of global mode render)
  - .planning/phases/28-dashboard-shell-context-detection/28-CONTEXT.md
    * D-01 through D-07 (context detection and mode switching)
    * D-10 (initial data load via _detectAndSwitch)
    * D-11 (index loading)
    * D-15 (base filename = domain)
    * D-17, D-18 (error handling)
    * "Specific Ideas" section (contextual actions per mode)
  - .planning/phases/28-dashboard-shell-context-detection/28-01-SUMMARY.md (index utilities that now exist)
  </read_first>

  <action>
  Refactor `PaperForgeStatusView` to support mode-aware rendering. This is the largest refactor in the phase.

  **=== Step 1: Refactor onOpen() — replace direct content build with mode-detected init ===**

  Replace the existing `onOpen()` method (lines 217-225) with:

  ```javascript
  async onOpen() {
      this._buildPanel();
      this._contentEl = this.containerEl.querySelector('.paperforge-content-area');
      this._modeSubscribers = [];     // reused by both workspace and vault events
      this._leafChangeTimer = null;   // debounce timer for active-leaf-change

      // Initial data load per D-10
      this._detectAndSwitch();
  }
  ```

  **=== Step 2: Refactor _buildPanel() — separate structural shell from content ===**

  The current `_buildPanel()` (lines 232-280) renders the entire dashboard inline: header, message, metrics, OCR, and quick actions. We need to:

  1. Keep the header (logo, title, badge, refresh button) in `_buildPanel()` — it stays across modes
  2. Move the metrics (`_metricsEl`) and OCR section (`_ocrSection`) into a NEW structural container that will be swapped per mode
  3. Add a `.paperforge-content-area` div between the header and the Quick Actions section
  4. Keep Quick Actions in `_buildPanel()` — they stay across modes

  Replace the existing `_buildPanel()` method (lines 232-280) with this refactored version:

  ```javascript
  _buildPanel() {
      const root = this.containerEl;
      root.empty();
      root.addClass('paperforge-status-panel');

      /* ── Header ── */
      const header = root.createEl('div', { cls: 'paperforge-header' });

      const headerLeft = header.createEl('div', { cls: 'paperforge-header-left' });
      headerLeft.createEl('div', { cls: 'paperforge-header-logo', text: 'P' });

      // Mode context area (populated by _renderModeHeader)
      this._modeContextEl = headerLeft.createEl('div', { cls: 'paperforge-mode-context' });

      this._headerTitle = headerLeft.createEl('h3', { cls: 'paperforge-header-title', text: 'PaperForge' });
      this._versionBadge = headerLeft.createEl('span', { cls: 'paperforge-header-badge', text: 'v\u2014' });

      const refreshBtn = header.createEl('button', { cls: 'paperforge-header-refresh', attr: { 'aria-label': 'Refresh' } });
      refreshBtn.innerHTML = '\u21BB';
      refreshBtn.addEventListener('click', () => {
          this._invalidateIndex();
          this._detectAndSwitch();
      });

      /* ── Status Message (command output) ── */
      this._messageEl = root.createEl('div', { cls: 'paperforge-message' });

      /* ── Mode-Switched Content Area (per D-06) ── */
      this._contentEl = root.createEl('div', { cls: 'paperforge-content-area' });

      /* ── Quick Actions (visible in all modes per D-07 / "Specific Ideas") ── */
      const actions = root.createEl('div', { cls: 'paperforge-actions-section' });
      actions.createEl('h4', { cls: 'paperforge-actions-title', text: 'Quick Actions' });
      this._actionsGrid = actions.createEl('div', { cls: 'paperforge-actions-grid' });
      this._renderActions();  // extracted so actions can be re-rendered per mode
  }
  ```

  Key changes from current:
  - Removed `_metricsEl`, `_ocrSection`, `_ocrBadge`, `_ocrTrack`, `_ocrCounts`, `_ocrEmpty` creation from `_buildPanel()` — these will be created dynamically in mode render methods
  - Added `_modeContextEl` — populated by `_renderModeHeader()` in Task 2
  - Added `_headerTitle` — reference to update header text per mode
  - Added `_contentEl` — the mode-switched content area
  - Added `_actionsGrid` — reference to action grid for re-rendering
  - Extracted action rendering into `_renderActions()` method
  - Refresh button now calls `_invalidateIndex()` + `_detectAndSwitch()` instead of `_fetchStats()` — since mode detection now owns data loading
  - Removed the old `_metricsEl`, `_ocrSection` instance variable references from `_buildPanel` — these will be created inside mode render methods instead

  **=== Step 3: Add _renderActions() — extracted from old _buildPanel() ===**

  ```javascript
  /* ── Render Quick Actions (extracted from _buildPanel for mode-aware reuse) ── */
  _renderActions() {
      this._actionsGrid.empty();
      // Base actions always visible (D-07)
      const baseActions = [
          { id: 'paperforge-sync', icon: '\u21BB', title: 'Sync Library', desc: 'Pull new references from Zotero and generate literature notes' },
          { id: 'paperforge-ocr', icon: '\u229E', title: 'Run OCR', desc: 'Extract full text and figures from PDFs via PaddleOCR' },
          { id: 'paperforge-doctor', icon: '\u2695', title: 'Run Doctor', desc: 'Verify PaperForge setup — check configs, Zotero, paths, and index health' },
          { id: 'paperforge-repair', icon: '\u21BA', title: 'Repair Issues', desc: 'Fix three-way state divergence, path errors, and rebuild index' },
      ];

      for (const a of baseActions) {
          const card = this._actionsGrid.createEl('div', { cls: 'paperforge-action-card' });
          card.createEl('div', { cls: 'paperforge-action-card-icon', text: a.icon });
          card.createEl('div', { cls: 'paperforge-action-card-title', text: a.title });
          card.createEl('div', { cls: 'paperforge-action-card-desc', text: a.desc });
          card.createEl('div', { cls: 'paperforge-action-card-hint', text: 'Click to run' });
          const actionDef = ACTIONS.find(x => x.id === a.id);
          if (actionDef) {
              card.addEventListener('click', () => this._runAction(actionDef, card));
          }
        }
    }
  ```

  Wait — looking at the existing code, `ACTIONS` is a module-level constant array (lines 157-208). The actions in `_buildPanel` iterate over `ACTIONS` directly. Let me simplify — just use `ACTIONS` directly instead of duplicating action definitions:

  ```javascript
  /* ── Render Quick Actions (extracted from _buildPanel for mode-aware reuse) ── */
  _renderActions() {
      this._actionsGrid.empty();
      for (const a of ACTIONS) {
          const card = this._actionsGrid.createEl('div', { cls: 'paperforge-action-card' });
          card.createEl('div', { cls: 'paperforge-action-card-icon', text: a.icon });
          card.createEl('div', { cls: 'paperforge-action-card-title', text: a.title });
          card.createEl('div', { cls: 'paperforge-action-card-desc', text: a.desc });
          card.createEl('div', { cls: 'paperforge-action-card-hint', text: 'Click to run' });
          card.addEventListener('click', () => this._runAction(a, card));
      }
  }
  ```

  Actually this is exactly what the existing code in `_buildPanel()` does (lines 271-279). The refactored `_buildPanel()` calls `_renderActions()` instead. The `ACTIONS` constant is already defined in module scope. Use the exact same iteration as lines 271-279.

  **=== Step 4: Add _invalidateIndex() helper ===**

  ```javascript
  /* ── Invalidate cached index (D-14) ── */
  _invalidateIndex() {
      this._cachedItems = null;
  }
  ```

  **=== Step 5: Add _currentMode state variables ===**

  Add these as instance property initializations. The cleanest approach is to initialize them at the top of the constructor. The current constructor (line 211) is `constructor(leaf) { super(leaf); }`. Replace it with:

  ```javascript
  constructor(leaf) {
      super(leaf);
      this._currentMode = null;       // 'global' | 'paper' | 'collection' (D-05)
      this._currentDomain = null;     // domain name when in collection mode (D-15)
      this._currentPaperKey = null;   // zotero_key when in per-paper mode (D-03)
      this._currentPaperEntry = null; // full entry when in per-paper mode
      this._cachedItems = null;       // lazy-loaded index items (Plan 28-01)
      this._modeSubscribers = [];     // event handler refs for cleanup
      this._leafChangeTimer = null;   // debounce timer for active-leaf-change
  }
  ```

  **=== Step 6: Add _detectAndSwitch() — core context detection (D-01, D-02, D-03, D-04, D-10) ===**

  Insert this method after `_renderActions()`:

  ```javascript
  /* ── Context Detection & Mode Switch (D-01, D-02, D-03, D-04, D-10) ── */
  _detectAndSwitch() {
      const activeFile = this.app.workspace.getActiveFile();

      if (!activeFile) {
          // No active file -> global mode (D-04)
          this._switchMode('global');
          return;
      }

      const ext = activeFile.extension;

      if (ext === 'base') {
          // .base file -> collection mode (D-02, D-15)
          this._currentDomain = activeFile.basename;
          this._currentPaperKey = null;
          this._currentPaperEntry = null;
          this._switchMode('collection');
          return;
      }

      if (ext === 'md') {
          // .md file — check for zotero_key in frontmatter (D-03)
          const cache = this.app.metadataCache.getFileCache(activeFile);
          const key = cache && cache.frontmatter && cache.frontmatter.zotero_key;

          if (key) {
              // Has zotero_key -> per-paper mode (D-03)
              this._currentPaperKey = key;
              this._currentPaperEntry = this._findEntry(key);
              this._currentDomain = null;
              this._switchMode('paper');
          } else {
              // .md without zotero_key -> global mode (D-04)
              this._currentDomain = null;
              this._currentPaperKey = null;
              this._currentPaperEntry = null;
              this._switchMode('global');
          }
          return;
      }

      // Any other file type -> global mode (D-04)
      this._currentDomain = null;
      this._currentPaperKey = null;
      this._currentPaperEntry = null;
      this._switchMode('global');
  }
  ```

  **=== Step 7: Add _switchMode() — renders the correct view for the detected mode (D-05, D-06) ===**

  ```javascript
  /* ── Mode Switching (D-05, D-06) ── */
  _switchMode(mode) {
      if (this._currentMode === mode) {
          // Already in this mode — just refresh if needed (D-04)
          this._refreshCurrentMode();
          return;
      }

      this._currentMode = mode;

      // Clear existing content (D-06)
      this._contentEl.empty();
      this._contentEl.removeClass('switching');

      // Update header (D-07)
      this._renderModeHeader(mode);

      // Render mode-specific content (D-06)
      switch (mode) {
          case 'global':
              this._renderGlobalMode();
              break;
          case 'paper':
              this._renderPaperMode();
              break;
          case 'collection':
              this._renderCollectionMode();
              break;
      }
  }
  ```

  **=== Step 8: Add mode-specific render stubs (placeholder for Phase 29/30) ===**

  These are initial implementations. Phase 29 (Per-Paper View) and Phase 30 (Collection View) will expand these with actual lifecycle, health, maturity, and chart components.

  ```javascript
  /* ── Global Mode Render (existing dashboard, extracted from _fetchStats + _renderOcr) ── */
  _renderGlobalMode() {
      // Metric cards
      this._metricsEl = this._contentEl.createEl('div', { cls: 'paperforge-metrics' });
      const ocrSection = this._contentEl.createEl('div', { cls: 'paperforge-ocr-section' });
      ocrSection.style.display = 'none';

      // Reuse existing _fetchStats logic — it reads data and renders to _metricsEl
      this._cachedStats = null; // force fresh load
      this._fetchStats();
  }

  /* ── Per-Paper Mode Render (placeholder — Phase 29 fills this in) ── */
  _renderPaperMode() {
      if (this._currentPaperEntry) {
          // Entry found — render paper info placeholder (D-18)
          this._contentEl.createEl('div', {
              cls: 'paperforge-content-placeholder',
              text: `Per-paper view for "${this._currentPaperEntry.title || this._currentPaperKey}" — Phase 29 will add lifecycle stepper, health matrix, maturity gauge, and next-step panel here.`,
          });
      } else if (this._currentPaperKey) {
          // Key exists but entry not found in index (D-18)
          this._contentEl.createEl('div', {
              cls: 'paperforge-content-placeholder',
              text: `Paper "${this._currentPaperKey}" not found in canonical index. The paper may not have been synced yet.`,
          });
      } else {
          // No key and no entry (defensive fallback)
          this._contentEl.createEl('div', {
              cls: 'paperforge-content-placeholder',
              text: 'No paper data available.',
          });
      }
  }

  /* ── Collection Mode Render (placeholder — Phase 30 fills this in) ── */
  _renderCollectionMode() {
      const domain = this._currentDomain || 'Unknown';
      const domainItems = this._filterByDomain(domain);

      if (domainItems.length > 0) {
          this._contentEl.createEl('div', {
              cls: 'paperforge-content-placeholder',
              text: `Collection view for "${domain}" — ${domainItems.length} papers. Phase 30 will add lifecycle distribution bar chart, aggregated health summary, and paper count here.`,
          });
      } else {
          this._contentEl.createEl('div', {
              cls: 'paperforge-content-placeholder',
              text: `No papers found in domain "${domain}". Sync some papers first.`,
          });
      }
  }
  ```

  **=== Step 9: Add _refreshCurrentMode() — re-renders current mode without switching ===**

  ```javascript
  /* ── Refresh current mode (called on index change, D-09, REFR-01) ── */
  _refreshCurrentMode() {
      if (!this._currentMode) return;
      this._contentEl.empty();
      this._contentEl.addClass('switching');
      this._invalidateIndex(); // force fresh data load

      switch (this._currentMode) {
          case 'global':
              this._renderGlobalMode();
              break;
          case 'paper':
              this._renderPaperMode();
              break;
          case 'collection':
              this._renderCollectionMode();
              break;
      }

      // Brief delay before removing switching class for smooth fade transition
      setTimeout(() => {
          if (this._contentEl) this._contentEl.removeClass('switching');
      }, 50);
  }
  ```

  **IMPORTANT — Operation order:**
  1. Replace constructor (add instance vars)
  2. Replace `onOpen()` (use _detectAndSwitch)
  3. Replace `_buildPanel()` (add _modeContextEl, _headerTitle, _contentEl, _actionsGrid; remove _metricsEl, _ocrSection)
  4. Add `_renderActions()` (extracted from old _buildPanel loop)
  5. Add `_invalidateIndex()` 
  6. Add `_detectAndSwitch()` 
  7. Add `_switchMode()`
  8. Add `_renderGlobalMode()`, `_renderPaperMode()`, `_renderCollectionMode()`
  9. Add `_refreshCurrentMode()`

  Do NOT modify `_fetchStats()`, `_renderStats()`, `_renderOcr()`, or any action-related methods yet.
  </action>

  <acceptance_criteria>
  - `main.js` PaperForgeStatusView constructor initializes: `_currentMode`, `_currentDomain`, `_currentPaperKey`, `_currentPaperEntry`, `_cachedItems`, `_modeSubscribers`, `_leafChangeTimer`
  - `onOpen()` calls `this._detectAndSwitch()` and sets up `this._contentEl`
  - `_buildPanel()` creates `this._modeContextEl` (div.paperforge-mode-context), `this._headerTitle` (h3.paperforge-header-title), `this._contentEl` (div.paperforge-content-area), `this._actionsGrid` (div.paperforge-actions-grid)
  - `_buildPanel()` calls `this._renderActions()` instead of inline rendering
  - `_renderActions()` iterates over `ACTIONS` constant and renders action cards
  - `_detectAndSwitch()` checks `this.app.workspace.getActiveFile()` (D-01)
  - `_detectAndSwitch()` checks `activeFile.extension === 'base'` (D-02) and `activeFile.extension === 'md'` (D-03)
  - `_detectAndSwitch()` reads frontmatter via `this.app.metadataCache.getFileCache(activeFile)` (D-03)
  - `_detectAndSwitch()` calls `this._switchMode(mode)` with appropriate mode
  - `_switchMode()` checks `this._currentMode === mode` to avoid redundant switches (D-04)
  - `_switchMode()` calls `this._contentEl.empty()` on mode change (D-06)
  - `_switchMode()` calls `this._renderModeHeader(mode)` (D-07)
  - `_switchMode()` switch/case handles 'global', 'paper', 'collection'
  - `_renderGlobalMode()` creates div.paperforge-metrics and renders _fetchStats()
  - `_renderPaperMode()` checks `this._currentPaperEntry` for "not found" case (D-18)
  - `_renderCollectionMode()` calls `this._filterByDomain(domain)` for data
  - `_refreshCurrentMode()` calls `this._invalidateIndex()` then re-renders current mode
  - `_invalidateIndex()` sets `this._cachedItems = null`
  - grep: `this._detectAndSwitch()` is called from `onOpen`
  - grep: `this._switchMode(` appears in _detectAndSwitch and _switchMode body
  - grep: `this._renderModeHeader(` appears in _switchMode body
  - grep: `this._renderActions()` appears in _buildPanel
  - grep: `this._invalidateIndex()` appears in _refreshCurrentMode and refresh button
  - grep: `this._currentMode` appears in _switchMode, _detectAndSwitch, _refreshCurrentMode
  </acceptance_criteria>

  <verify>
  <automated>python -c "
with open('paperforge/plugin/main.js') as f:
    c = f.read()
# Core methods
assert '_detectAndSwitch()' in c or 'this._detectAndSwitch' in c, 'Missing _detectAndSwitch method'
assert '_switchMode(' in c or 'this._switchMode' in c, 'Missing _switchMode method'
assert '_refreshCurrentMode' in c, 'Missing _refreshCurrentMode method'
assert '_invalidateIndex' in c, 'Missing _invalidateIndex method'
assert '_renderActions' in c or '_renderActions()' in c, 'Missing _renderActions method'
assert '_renderGlobalMode' in c, 'Missing _renderGlobalMode method'
assert '_renderPaperMode' in c, 'Missing _renderPaperMode method'
assert '_renderCollectionMode' in c, 'Missing _renderCollectionMode method'
# Mode state
assert '_currentMode' in c, 'Missing _currentMode state'
assert '_currentDomain' in c, 'Missing _currentDomain state'
assert '_currentPaperKey' in c, 'Missing _currentPaperKey state'
assert '_currentPaperEntry' in c, 'Missing _currentPaperEntry state'
# Detection logic
assert \"getActiveFile()\" in c or \"getActiveFile())\" in c, 'Missing getActiveFile call (D-01)'
assert \"'base'\" in c or \"\\\"base\\\"\" in c, 'Missing .base extension check (D-02)'
assert \"getFileCache\" in c, 'Missing getFileCache call (D-03)'
assert \"zotero_key\" in c, 'Missing zotero_key check (D-03)'
# Content management
assert \"empty()\" in c, 'Missing empty() call for content clear (D-06)'
assert '_modeContextEl' in c, 'Missing _modeContextEl (D-07)'
assert '_contentEl' in c, 'Missing _contentEl'
assert '_actionsGrid' in c, 'Missing _actionsGrid'
# Existing structural elements preserved
assert 'paperforge-status-panel' in c, 'Missing root panel class'
assert 'paperforge-header' in c, 'Missing header'
assert 'paperforge-actions-section' in c, 'Missing actions section'
print('Task 1 mode detection infrastructure: ALL CHECKS PASSED')
"</automated>
  </verify>

  <done>
  PaperForgeStatusView has mode detection infrastructure: constructor initializes mode state variables, _buildPanel() creates mode-switched content area and header context elements, onOpen() calls _detectAndSwitch(), _renderActions() replaces inline action card loop, _detectAndSwitch() resolves active file type (base/md-with-key/other), _switchMode() clears and renders correct mode, mode-specific render methods (_renderGlobalMode/_renderPaperMode/_renderCollectionMode) exist with content stubs, _refreshCurrentMode() and _invalidateIndex() support auto-refresh workflow.
  </done>
</task>

<task type="auto">
  <name>Task 2: Add event subscriptions (auto-refresh), mode-aware header rendering, and onClose cleanup</name>
  <files>paperforge/plugin/main.js</files>

  <read_first>
  - paperforge/plugin/main.js (output after Task 1 — has the mode infrastructure but no event subscriptions yet)
  - .planning/phases/28-dashboard-shell-context-detection/28-CONTEXT.md (decisions D-07, D-08, D-09, D-19; also Specific Ideas about contextual actions)
  </read_first>

  <action>
  Add event subscriptions, mode-aware header rendering, and lifecycle cleanup to PaperForgeStatusView.

  **WARNING:** There is a duplicate `_showMessage` method issue in the existing code. After the original `_showMessage` at lines 788-793, there's an exact duplicate at lines 795-800, followed by a duplicated `child.on('close', ...)` block (lines 754-786) that was accidentally left in from a previous edit. The executor MUST be careful to insert new code AFTER the first `_showMessage` (ending at line 793) and BEFORE the duplicate block (starting at line 754). To be safe, identify the true end of the class (the `static async open` method at lines 802-814) and insert the new methods immediately before the `static async open` method.

  Insert these methods in the PaperForgeStatusView class, right before the `static async open()` method.

  **Method A: _renderModeHeader(mode) — update header context area (D-07)**

  ```javascript
  /* ── Mode-Aware Header (D-07) ── */
  _renderModeHeader(mode) {
      this._modeContextEl.empty();

      // Build mode badge
      const badge = this._modeContextEl.createEl('span', { cls: 'paperforge-mode-badge' });
      let modeName = '';

      switch (mode) {
          case 'global':
              badge.addClass('global');
              badge.setText('Global');
              this._headerTitle.setText('PaperForge');
              break;

          case 'paper':
              badge.addClass('paper');
              badge.setText('Paper');
              if (this._currentPaperEntry && this._currentPaperEntry.title) {
                  modeName = this._currentPaperEntry.title;
              } else if (this._currentPaperKey) {
                  modeName = this._currentPaperKey;
                  // Show warning if entry not found (D-18)
                  this._modeContextEl.createEl('span', {
                      cls: 'paperforge-mode-warning',
                      text: 'Not found in index',
                  });
              } else {
                  modeName = 'Unknown paper';
              }
              this._headerTitle.setText(modeName);
              break;

          case 'collection':
              badge.addClass('collection');
              badge.setText('Collection');
              modeName = this._currentDomain || 'Unknown Domain';
              this._headerTitle.setText(modeName);
              break;
      }

      if (modeName) {
          this._modeContextEl.createEl('span', {
              cls: 'paperforge-mode-name',
              text: modeName,
          });
      }
  }
  ```

  NOTE: For the `paper` case, the modeName text is shown both in the header title AND in the `.paperforge-mode-name` span. This is intentional — the header title is the primary display (large text) and the mode context area shows the supplementary badge + truncated name. The Phase 29 development may adjust this.

  **Method B: _setupEventSubscriptions() — wire up workspace + vault events (D-08, D-09, D-19)**

  ```javascript
  /* ── Event Subscriptions (D-08, D-09, D-19) ── */
  _setupEventSubscriptions() {
      // D-08: Active leaf change — debounced with 300ms delay
      const leafHandler = this.app.workspace.on('active-leaf-change', () => {
          clearTimeout(this._leafChangeTimer);
          this._leafChangeTimer = setTimeout(() => {
              this._detectAndSwitch();
          }, 300);
      });
      this._modeSubscribers.push({ event: 'active-leaf-change', ref: leafHandler });

      // D-09: File modification — filter to formal-library.json only
      const modifyHandler = this.app.vault.on('modify', (file) => {
          if (file && file.path && file.path.endsWith('formal-library.json')) {
              this._invalidateIndex();  // D-14: invalidate cache
              this._refreshCurrentMode();
          }
      });
      this._modeSubscribers.push({ event: 'modify', ref: modifyHandler });
  }
  ```

  Note on event API: Obsidian's `workspace.on()` and `vault.on()` return an event ref that is passed to `off()` for cleanup. The returned ref may be a number (index) or a function, depending on the Obsidian version. Storing these refs in `this._modeSubscribers` and passing them to `off()` in `onClose()` handles both patterns.

  **Method C: Update onOpen() to call _setupEventSubscriptions()**

  The `onOpen()` method (refactored in Task 1) should also call `_setupEventSubscriptions()`:

  ```javascript
  async onOpen() {
      this._buildPanel();
      this._contentEl = this.containerEl.querySelector('.paperforge-content-area');
      this._modeSubscribers = [];
      this._leafChangeTimer = null;

      // Subscribe to file change events (D-08, D-09)
      this._setupEventSubscriptions();

      // Initial data load per D-10
      this._detectAndSwitch();
  }
  ```

  **Method D: Update onClose() — proper event cleanup with defensive guards**

  ```javascript
  onClose() {
      // Unsubscribe from all event subscriptions (D-08, D-09)
      if (this._modeSubscribers && this._modeSubscribers.length > 0) {
          for (const sub of this._modeSubscribers) {
              if (sub.event === 'active-leaf-change') {
                  this.app.workspace.off('active-leaf-change', sub.ref);
              } else if (sub.event === 'modify') {
                  this.app.vault.off('modify', sub.ref);
              }
          }
          this._modeSubscribers = [];
      }

      // Clear debounce timer
      if (this._leafChangeTimer) {
          clearTimeout(this._leafChangeTimer);
          this._leafChangeTimer = null;
      }

      // Clear cached data
      this._cachedItems = null;
      this._cachedStats = null;
  }
  ```

  **IMPORTANT — Insertion order:**
  1. Replace `onOpen()` with version that calls `_setupEventSubscriptions()`
  2. Replace `onClose()` with cleanup version
  3. Add `_renderModeHeader(mode)` before `static async open()`
  4. Add `_setupEventSubscriptions()` before `static async open()`

  The refactored `onOpen()` from Task 1 will now include the `_setupEventSubscriptions()` call. The executor should make sure both Task 1 and Task 2 changes are present in the final output — specifically, `onOpen()` must call BOTH `_buildPanel()` and `_setupEventSubscriptions()` and `_detectAndSwitch()`.

  The `onClose()` in current main.js (line 227) is a no-op `onClose() { /* no-op */ }`. Replace it completely.

  **No CSS changes** needed for this task. The Task 2 CSS additions from Plan 28-01 already define .paperforge-mode-badge, .paperforge-mode-name, and .paperforge-mode-warning classes.
  </action>

  <acceptance_criteria>
  - `onOpen()` calls `this._setupEventSubscriptions()` in addition to `_buildPanel()` and `_detectAndSwitch()`
  - `onClose()` iterates `this._modeSubscribers` and calls `this.app.workspace.off()` and `this.app.vault.off()` for each subscription
  - `onClose()` clears `this._leafChangeTimer` and nulls cached data
  - `_setupEventSubscriptions()` subscribes to `this.app.workspace.on('active-leaf-change', ...)` (D-08)
  - `_setupEventSubscriptions()` subscribes to `this.app.vault.on('modify', ...)` (D-09)
  - `modify` handler filters for `file.path.endsWith('formal-library.json')` (D-19)
  - `modify` handler calls `this._invalidateIndex()` and `this._refreshCurrentMode()` (D-09, REFR-01)
  - `active-leaf-change` handler debounces with `setTimeout(..., 300)` before calling `_detectAndSwitch()` (D-08, D-04)
  - Subscriptions are stored in `this._modeSubscribers` array as `{event, ref}` objects
  - `_renderModeHeader(mode)` creates `span.paperforge-mode-badge` with `.global/.paper/.collection` class
  - `_renderModeHeader(mode)` creates `span.paperforge-mode-name` with mode context text
  - `_renderModeHeader('paper')` shows warning when entry not found (D-18)
  - `_renderModeHeader('collection')` sets header title to `this._currentDomain`
  - `_renderModeHeader('global')` sets header title to 'PaperForge'
  - grep: `_setupEventSubscriptions` in main.js
  - grep: `'active-leaf-change'` in main.js
  - grep: `'modify'` in main.js
  - grep: `formal-library.json` in main.js (within event handler filter)
  - grep: `this._modeSubscribers.push` in main.js
  - grep: `this.app.vault.off(` and `this.app.workspace.off(` in onClose
  - grep: `this._leafChangeTimer` and `clearTimeout` in onClose
  - grep: `_renderModeHeader` in main.js
  - grep: `paperforge-mode-badge` in main.js
  - grep: `paperforge-mode-warning` in main.js (paper not found case)
  </acceptance_criteria>

  <verify>
  <automated>python -c "
with open('paperforge/plugin/main.js') as f:
    c = f.read()
# Event subscriptions
assert '_setupEventSubscriptions' in c, 'Missing _setupEventSubscriptions method'
assert \"'active-leaf-change'\" in c, 'Missing active-leaf-change subscription (D-08)'
assert \"'modify'\" in c, 'Missing modify subscription (D-09)'
assert 'formal-library.json' in c, 'Missing formal-library.json filter (D-19)'
assert '_invalidateIndex' in c, 'Missing _invalidateIndex call in modify handler'
assert '_refreshCurrentMode' in c, 'Missing _refreshCurrentMode call'
assert 'this._modeSubscribers.push' in c, 'Missing subscriber tracking'
# Debounce
assert '_leafChangeTimer' in c, 'Missing leaf debounce timer'
assert 'setTimeout' in c, 'Missing setTimeout debounce (D-08)'
# Cleanup
assert 'this.app.workspace.off(\"active-leaf-change\"' in c or 'this.app.workspace.off(' in c, 'Missing workspace off in onClose'
assert 'this.app.vault.off(' in c, 'Missing vault off in onClose'
# Header rendering
assert '_renderModeHeader' in c, 'Missing _renderModeHeader method'
assert \"'paperforge-mode-badge'\" in c, 'Missing mode badge class'
assert \"'paperforge-mode-name'\" in c, 'Missing mode name class'
assert 'addClass(\"global\")' in c, 'Missing global badge class'
assert 'addClass(\"paper\")' in c, 'Missing paper badge class'
assert 'addClass(\"collection\")' in c, 'Missing collection badge class'
assert 'paperforge-mode-warning' in c, 'Missing mode warning (D-18)'
# Lifecycle
assert '_setupEventSubscriptions()' in c or 'this._setupEventSubscriptions' in c, 'onOpen missing setup call'
print('Task 2 events + header + cleanup: ALL CHECKS PASSED')
"</automated>
  </verify>

  <done>
  PaperForgeStatusView has full auto-refresh: _setupEventSubscriptions() subscribes to active-leaf-change (debounced 300ms) and vault modify (filtered to formal-library.json), onClose() properly unsubscribes all events, clears timers, and nulls cached data. _renderModeHeader() renders mode-specific badge and context text in the header, with warning for missing papers.
  </done>
</task>

</tasks>

<verification>
1. PaperForgeStatusView detects active file via getActiveFile() (D-01) and switches to the correct mode:
   - .base file -> _switchMode('collection') (DASH-01)
   - .md with zotero_key -> _switchMode('paper') (DASH-02)
   - other/no file -> _switchMode('global') (DASH-03)
2. File switch via tab click or Ctrl+Tab triggers mode switch within 300ms debounce (DASH-04)
3. formal-library.json modification triggers refresh within 2 seconds (REFR-01) — reactive, no polling
4. Active file change triggers refresh within debounce window (REFR-02)
5. onOpen() calls _detectAndSwitch() for initial load (D-10)
6. _switchMode() clears content via empty() before rendering new mode (D-06)
7. Quick actions (Sync, OCR, Doctor, Repair) remain visible in all modes
8. Mode header shows: global -> "PaperForge", paper -> paper title, collection -> domain name (D-07)
9. Paper not found in index shows mode warning (D-18)
10. onClose() cleans up subscriptions, prevents memory leaks
11. Index cache invalidated on refresh button, file modify event, and onClose
</verification>

<success_criteria>
- [ ] .base file -> collection mode with domain name in header (DASH-01)
- [ ] .md with zotero_key -> per-paper mode with paper title in header (DASH-02)
- [ ] Other/no file -> global mode with existing dashboard (DASH-03)
- [ ] File switch triggers mode switch within 300ms (DASH-04)
- [ ] formal-library.json modify triggers refresh, mode preserved (REFR-01)
- [ ] Active leaf change triggers refresh (REFR-02)
- [ ] Quick actions visible in all modes
- [ ] Content cleared on mode switch (empty() + re-render)
- [ ] Mode context displayed in header area (badge + name)
- [ ] Index cache invalidated on modify events
- [ ] Proper event cleanup in onClose()
- [ ] No regressions in existing quick action execution
- [ ] Index not found shows appropriate warning to user
</success_criteria>

<output>
After completion, create `.planning/phases/28-dashboard-shell-context-detection/28-02-SUMMARY.md`
</output>
