# Browser Interaction Optimizations

Companion file for `/fluent-ui-test` and `/fluent-ui-record`. Reduces browser interaction token costs by ~90% through local computation and compact DOM summaries.

**Referenced by:** `fluent-ui-test/SKILL.md`, `fluent-ui-record/SKILL.md`, `fluent-mystique-preview/SKILL.md`

---

## FAST PATTERN: initScript + batch (MANDATORY)

**Every browser skill MUST use this pattern. No exceptions.**

The slowest part of browser automation is LLM round trips — each `evaluate_script`, `navigate_page`, `take_screenshot` is a full API call. Minimize round trips by:

1. **Use `initScript` on `navigate_page`** to auto-inject `__fcTest` during navigation (saves 1 round trip per page)
2. **Use `batch()` to combine operations** into a single `evaluate_script` (saves N-1 round trips)

### Before (SLOW — 4 round trips)

```
navigate_page(url)                              ← round trip 1
evaluate_script(inject __fcTest library)        ← round trip 2
evaluate_script(pageSummary())                  ← round trip 3
evaluate_script(findButton('Cancel'))           ← round trip 4
```

### After (FAST — 2 round trips)

```
navigate_page(url, initScript=HELPER_LIBRARY)   ← round trip 1 (injects __fcTest automatically)
evaluate_script(batch([                          ← round trip 2 (all queries in one call)
  {action:'pageSummary'},
  {action:'getActionButtons'}
]))
```

### The initScript parameter

`navigate_page` accepts `initScript` — JavaScript executed on the new document before any other scripts. Pass the entire `__fcTest` helper library (from Layer 1 below) as `initScript`. This means `__fcTest` is available immediately when the page loads — no separate injection step.

```
navigate_page:
  url: https://example.fluentcommerce.com/oms#/orders/123
  initScript: "(function(){if(window.__fcTest)return;window.__fcTest={pageSummary:function(){...},findButton:function(l){...},...};})();"
```

**Critical rule:** The `initScript` value must be a single expression or IIFE. Use the minified version of the Layer 1 helper library below.

### The batch pattern

Combine multiple operations into one `evaluate_script`:

```javascript
// ONE call does: page summary + find all buttons + highlight one
() => JSON.stringify(window.__fcTest.batch([
  { action: 'pageSummary' },
  { action: 'getActionButtons' },
  { action: 'highlight', args: ['Cancel Order', 'VERIFIED'] }
]))
```

### Navigation + verify in 2 calls (the standard pattern)

For every page interaction, use exactly this 2-call pattern:

```
Call 1: navigate_page(url, initScript=HELPER_LIBRARY)
Call 2: evaluate_script(() => JSON.stringify({
  page: window.__fcTest.pageSummary(),
  buttons: window.__fcTest.getActionButtons(),
  target: window.__fcTest.findButton('Cancel Order') ? 'found' : 'missing'
}))
```

If you need a screenshot for evidence, that's call 3. **Maximum 3 calls per page.**

---

## FAST LOGIN (saves 4-6 round trips)

Login is the biggest bottleneck. The old flow used 6 MCP calls. The optimized flow uses 2-3.

### Before (SLOW — 6 round trips)

```
navigate_page(app_url)                          ← round trip 1
take_snapshot()                                  ← round trip 2 (detect login form)
type_text(username_uid, user)                    ← round trip 3
type_text(password_uid, pass)                    ← round trip 4
click(submit_uid)                                ← round trip 5
take_snapshot()                                  ← round trip 6 (verify logged in)
```

### After (FAST — 2-3 round trips)

```
navigate_page(app_url, initScript=HELPER)        ← round trip 1 (helper auto-injected)
evaluate_script(batch([                           ← round trip 2 (detect + login in ONE call)
  {action:'isLoggedIn'},
  {action:'loginForm', args:[username, password]}
]))
  → If isLoggedIn.loggedIn=true: loginForm is skipped (already in app)
  → If isLoggedIn.hasForm=true: loginForm fills + submits
wait_for(2-3s) + evaluate_script(pageSummary)    ← round trip 3 (verify redirect)
```

### Session reuse (0 login calls)

Chrome DevTools MCP uses a persistent profile — cookies survive between sessions. **Always check session first:**

```javascript
// In the batch after navigate_page:
{action: 'isLoggedIn'}
// If loggedIn=true → skip loginForm entirely → 0 extra calls
```

### Token injection via initScript (0 login calls)

For CI/CD or pre-generated tokens, combine token injection + helper injection in ONE `initScript`:

```javascript
initScript: (function(){
  // Token injection (before app scripts load)
  localStorage.setItem('token', '<FLUENT_UI_TOKEN>');
  localStorage.setItem('accessToken', '<FLUENT_UI_TOKEN>');
  // Helper library injection
  if(window.__fcTest)return;
  window.__fcTest={pageSummary:function(){...},...};
})()
```

This does authentication + helper setup in a SINGLE `navigate_page` call. The app reads the token from localStorage and skips the login form entirely.

### Conditional login pattern (RECOMMENDED)

```javascript
// Call 1: navigate_page(url, initScript=HELPER)
// Call 2:
evaluate_script(() => {
  var status = window.__fcTest.isLoggedIn();
  if (status.loggedIn) {
    return JSON.stringify({ login: 'skipped', page: window.__fcTest.pageSummary() });
  }
  if (status.hasForm) {
    var result = window.__fcTest.loginForm(USERNAME, PASSWORD);
    return JSON.stringify({ login: result, page: null }); // page is null until redirect
  }
  return JSON.stringify({ login: 'unknown_state', url: location.href });
})
// Call 3 (only if login was submitted): wait_for + evaluate_script(pageSummary)
```

**Best case (session reuse): 2 calls.** Worst case (form login): 3 calls. Old pattern: 6 calls.

---

## FAST NAVIGATION (saves 1-2 round trips per page)

### SPA hash navigation (PREFERRED for in-app navigation)

Fluent apps are SPAs. After the initial `navigate_page`, use `__fcTest.navigateTo()` for in-app page changes:

```
// SLOW — 2 round trips + page reload + re-injection:
navigate_page(about:blank)
navigate_page(target_url, initScript=HELPER)

// FAST — 1 round trip, no page reload, helper stays injected:
evaluate_script(() => {
  window.__fcTest.navigateTo('/orders/123/2/ORDER-REF');
  return 'navigated';
})
```

### When to use which

| Scenario | Method | Calls |
|---|---|---|
| **First page load** (login/initial) | `navigate_page(url, initScript=HELPER)` | 1 |
| **In-app navigation** (same domain) | `evaluate_script → navigateTo(hash)` | 1 |
| **Different app** (OMS → Store) | `navigate_page(new_url, initScript=HELPER)` | 1 |
| **Fresh manifest test** (need full reload) | `navigate_page(about:blank)` + `navigate_page(url)` | 2 |

### Skip about:blank bounce

The old pattern bounces through `about:blank` before every navigation to "clear SPA state". This adds an extra round trip per page. **Only use it when explicitly testing fresh manifest loads.** For normal navigation, go directly to the target URL or use `navigateTo()`.

---

## The Problem

Each `take_snapshot` returns the full accessibility tree — **20-40K tokens** per call. A typical UI test takes 3-5 snapshots. The LLM then spends 5-10K tokens reasoning about each snapshot to find elements. This means **150-250K tokens** just on DOM discovery for a single page test.

Most of this is wasted because we **already know what's on the page** from the manifest JSON and workflow definitions.

## Cost Comparison

| Pattern | Tokens per interaction | 10 interactions | Notes |
|---|---|---|---|
| **Current** (take_snapshot + reason) | ~35K | ~350K | Full a11y tree every time |
| **Optimized** (pageSummary + known selectors) | ~3.5K | ~35K | Compact JSON + pre-computed selectors |
| **Savings** | **~90%** | **~315K saved** | |

## Solution: Three-Layer Optimization

### Layer 1: Helper Library (inject once per session)

Inject this library with a single `evaluate_script` call at session start. All subsequent interactions use its functions instead of raw Chrome DevTools MCP calls.

```javascript
(function() {
  if (window.__fcTest) return; // already injected

  window.__fcTest = {

    // ── Layer 2: Compact Page Summary ──────────────────────────
    // Returns ~200-500 tokens instead of 20-40K from take_snapshot.
    // Use this FIRST. Only fall back to take_snapshot when pageSummary
    // shows unexpected state that needs deeper investigation.

    pageSummary: function() {
      var headingEl = document.querySelector('h1, h2, [class*="PageTitle"], [class*="page-title"], [class*="Title"]');
      var tabs = [];
      document.querySelectorAll('[role="tab"]').forEach(function(t) {
        tabs.push({ label: t.textContent.trim(), active: t.getAttribute('aria-selected') === 'true' });
      });
      var cards = [];
      document.querySelectorAll('[class*="Card"], [class*="card-"]').forEach(function(c) {
        var title = c.querySelector('h2, h3, h4, [class*="title"], [class*="Title"]');
        if (title) cards.push(title.textContent.trim());
      });
      var buttons = [];
      document.querySelectorAll('button:not([disabled])').forEach(function(b) {
        var text = b.textContent.trim();
        if (text && text.length < 60) {
          buttons.push({
            text: text,
            primary: (b.className || '').indexOf('primary') !== -1 || (b.className || '').indexOf('Primary') !== -1
          });
        }
      });
      var tables = [];
      document.querySelectorAll('table').forEach(function(t) {
        var headers = [];
        t.querySelectorAll('th').forEach(function(th) { headers.push(th.textContent.trim()); });
        tables.push({ headers: headers, rowCount: t.querySelectorAll('tbody tr').length });
      });
      var forms = [];
      document.querySelectorAll('form, [role="dialog"]').forEach(function(f) {
        var fields = [];
        f.querySelectorAll('input, select, textarea').forEach(function(i) {
          fields.push({ name: i.name || i.getAttribute('aria-label') || '', type: i.type || i.tagName.toLowerCase() });
        });
        if (fields.length > 0) forms.push({ fields: fields });
      });
      var alerts = [];
      document.querySelectorAll('[role="alert"], [class*="error"], [class*="Error"], [class*="warning"]').forEach(function(a) {
        var text = a.textContent.trim();
        if (text.length < 200) alerts.push(text);
      });
      var sidebar = [];
      document.querySelectorAll('nav a, [class*="sidebar"] a, [class*="Sidebar"] a, [class*="drawer"] a').forEach(function(a) {
        var text = a.textContent.trim();
        if (text) sidebar.push({ text: text, href: a.getAttribute('href') || '', active: (a.className || '').indexOf('active') !== -1 || (a.className || '').indexOf('selected') !== -1 });
      });

      return {
        url: location.pathname + location.search,
        title: headingEl ? headingEl.textContent.trim() : document.title,
        sidebar: sidebar.slice(0, 20),
        tabs: tabs,
        cards: cards,
        buttons: buttons,
        tables: tables,
        forms: forms,
        alerts: alerts,
        loading: !!document.querySelector('[class*="loading"], [class*="Loading"], [class*="spinner"], [class*="Spinner"], [role="progressbar"]')
      };
    },

    // ── Layer 2b: Custom Component Detection ──────────────────
    // Detects Mystique OOTB and custom SDK components by MUI classes,
    // section headings, and React rendering patterns. Returns ~300-600
    // tokens. Use AFTER pageSummary() when testing custom component pages.

    scanComponents: function() {
      var components = [];
      // MUI Cards (fc.card.*, fc.card.attribute, etc.)
      document.querySelectorAll('[class*="MuiCard"]').forEach(function(c) {
        var heading = c.querySelector('h2, h3, h4, [class*="MuiCardHeader"] [class*="title"]');
        components.push({
          type: 'card',
          title: heading ? heading.textContent.trim() : '',
          hasContent: c.querySelector('[class*="MuiCardContent"]') !== null
        });
      });
      // MUI Tables (fc.list, fc.quantity.list)
      document.querySelectorAll('[class*="MuiTable"]').forEach(function(t) {
        var headers = [];
        t.querySelectorAll('th, [class*="MuiTableCell"][class*="head"]').forEach(function(th) {
          var text = th.textContent.trim();
          if (text) headers.push(text);
        });
        components.push({
          type: 'list',
          headers: headers,
          rowCount: t.querySelectorAll('tbody tr, [class*="MuiTableBody"] [class*="MuiTableRow"]').length
        });
      });
      // MUI Tabs (fc.tabs, fc.tabs.card)
      document.querySelectorAll('[class*="MuiTabs"]').forEach(function(t) {
        var tabLabels = [];
        t.querySelectorAll('[class*="MuiTab"]').forEach(function(tab) {
          tabLabels.push({
            label: tab.textContent.trim(),
            selected: tab.getAttribute('aria-selected') === 'true'
          });
        });
        components.push({ type: 'tabs', tabs: tabLabels });
      });
      // MUI Accordion (fc.accordion)
      document.querySelectorAll('[class*="MuiAccordion"]').forEach(function(a) {
        var summary = a.querySelector('[class*="MuiAccordionSummary"]');
        components.push({
          type: 'accordion',
          title: summary ? summary.textContent.trim() : '',
          expanded: a.getAttribute('aria-expanded') === 'true' || (a.className || '').indexOf('expanded') !== -1
        });
      });
      // MUI Dialog (user action forms, fc.modal.button)
      document.querySelectorAll('[class*="MuiDialog"], [role="dialog"]').forEach(function(d) {
        var title = d.querySelector('[class*="MuiDialogTitle"], h2');
        var fields = this._scanDialogFields(d);
        components.push({
          type: 'dialog',
          title: title ? title.textContent.trim() : '',
          open: d.offsetParent !== null || (d.className || '').indexOf('open') !== -1,
          fields: fields
        });
      }.bind(this));
      // Page sections (fc.page.section, fc.page.section.header)
      document.querySelectorAll('[class*="section"] > h2, [class*="section"] > h3, [class*="Section"] > h2').forEach(function(h) {
        components.push({ type: 'section', title: h.textContent.trim() });
      });
      // Custom components — look for data-testid (SDK convention) and
      // React error boundaries (component failed to render)
      document.querySelectorAll('[data-testid]').forEach(function(el) {
        if (el.offsetParent === null) return;
        var testId = el.getAttribute('data-testid');
        if (testId && testId.indexOf('fc-') === 0) {
          components.push({ type: 'custom', testId: testId, tag: el.tagName.toLowerCase() });
        }
      });
      // React error boundaries (custom components that crashed)
      document.querySelectorAll('[class*="error-boundary"], [class*="ErrorBoundary"]').forEach(function(e) {
        components.push({ type: 'error-boundary', text: e.textContent.trim().substring(0, 100) });
      });
      return components;
    },

    // Scan dialog fields including MUI and custom FieldRegistry components.
    // Returns array of {name, type, muiType, value, options} — covers standard
    // inputs AND MUI Autocomplete, Select, custom contentEditable fields.
    _scanDialogFields: function(container) {
      var fields = [];
      var root = container || document.querySelector('[role="dialog"], [class*="MuiDialog"]');
      if (!root) return fields;
      // Standard inputs
      root.querySelectorAll('input, select, textarea').forEach(function(el) {
        fields.push({
          name: el.name || el.getAttribute('aria-label') || el.getAttribute('placeholder') || '',
          type: el.type || el.tagName.toLowerCase(),
          value: el.value || ''
        });
      });
      // MUI Autocomplete (custom FieldRegistry dropdowns)
      root.querySelectorAll('[class*="MuiAutocomplete"]').forEach(function(el) {
        var input = el.querySelector('input');
        var label = el.closest('[class*="MuiFormControl"]');
        var labelText = label ? label.querySelector('label') : null;
        fields.push({
          name: (labelText ? labelText.textContent.trim() : '') || (input ? input.getAttribute('placeholder') || '' : ''),
          type: 'autocomplete',
          value: input ? input.value : ''
        });
      });
      // MUI Select (dropdown fields)
      root.querySelectorAll('[class*="MuiSelect"]').forEach(function(el) {
        var label = el.closest('[class*="MuiFormControl"]');
        var labelText = label ? label.querySelector('label') : null;
        fields.push({
          name: labelText ? labelText.textContent.trim() : '',
          type: 'select',
          value: el.textContent.trim()
        });
      });
      // Contenteditable (rare custom fields)
      root.querySelectorAll('[contenteditable="true"]').forEach(function(el) {
        fields.push({
          name: el.getAttribute('aria-label') || '',
          type: 'contenteditable',
          value: el.textContent.trim().substring(0, 100)
        });
      });
      return fields;
    },

    // Check if a specific Mystique component rendered on the page.
    // Pass the manifest component alias or a heading/title text.
    // Returns {found, element, details} or {found: false}.
    verifyComponent: function(aliasOrTitle) {
      // Strategy 1: data-testid matching fc-<alias-slug>
      var slug = aliasOrTitle.replace(/\./g, '-').toLowerCase();
      var safe = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(slug) : slug.replace(/["'\\]/g, '');
      var byTestId = document.querySelector('[data-testid*="' + safe + '"]');
      if (byTestId && byTestId.offsetParent !== null) {
        return { found: true, method: 'testId', tag: byTestId.tagName.toLowerCase() };
      }
      // Strategy 2: card/section title text
      var el = this.find(aliasOrTitle);
      if (el) return { found: true, method: 'title', tag: el.tagName.toLowerCase() };
      // Strategy 3: MUI class matching
      var muiMap = {
        'fc.list': '[class*="MuiTable"]',
        'fc.tabs': '[class*="MuiTabs"]',
        'fc.tabs.card': '[class*="MuiTabs"]',
        'fc.card': '[class*="MuiCard"]',
        'fc.card.attribute': '[class*="MuiCard"]',
        'fc.accordion': '[class*="MuiAccordion"]',
        'fc.modal.button': '[class*="MuiDialog"]',
        'fc.settingForm': 'form',
        'fc.stepper': '[class*="MuiStepper"]',
        'fc.chart.bar': '[class*="recharts"], canvas, svg',
        'fc.chart.line': '[class*="recharts"], canvas, svg',
        'fc.chart.gauge': '[class*="recharts"], canvas, svg'
      };
      if (muiMap[aliasOrTitle]) {
        var muiEl = document.querySelector(muiMap[aliasOrTitle]);
        if (muiEl && muiEl.offsetParent !== null) {
          return { found: true, method: 'mui', tag: muiEl.tagName.toLowerCase() };
        }
      }
      return { found: false };
    },

    // ── Layer 2c: Compact Page State (MUI-aware) ──────────────
    // Even lighter than pageSummary() — returns just headings, buttons,
    // tabs, table/dialog counts. ~150 tokens vs ~500 for pageSummary.
    // Use for quick "what's on screen?" checks between actions.

    pageState: function() {
      var MUI = this._MUI;
      var headings = [];
      document.querySelectorAll(MUI.heading).forEach(function(h) {
        var t = h.textContent.trim();
        if (t) headings.push(t);
      });
      var buttons = [];
      document.querySelectorAll(MUI.button).forEach(function(b) {
        var t = b.textContent.trim();
        if (t && t.length < 60) buttons.push(t);
      });
      var tabs = [];
      document.querySelectorAll(MUI.tab).forEach(function(t) {
        tabs.push({ label: t.textContent.trim(), active: t.classList.contains('Mui-selected') });
      });
      return {
        url: window.location.hash || window.location.pathname,
        headings: headings,
        buttons: buttons,
        tabs: tabs,
        tables: document.querySelectorAll(MUI.table).length,
        dialogs: document.querySelectorAll(MUI.dialog).length
      };
    },

    // ── Layer 2d: MUI-Aware Element Finders ─────────────────
    // These use Material-UI class selectors and heading text matching
    // instead of generic DOM traversal. More reliable on Fluent UX apps
    // because OOTB components use MUI classes, not data-* attributes.
    //
    // Pre-compute the labels from manifest selector-cache-slim.json
    // (generated by fluent-ai-skills/tools/manifest-selector-map.mjs) so you know the
    // exact strings before touching the browser.

    // MUI selector constants (shared across all finders)
    _MUI: {
      card: '.MuiPaper-root.MuiCard-root, .MuiPaper-root',
      heading: 'h3.MuiTypography-root, h2.MuiTypography-root, h4.MuiTypography-root',
      button: 'button.MuiButton-root, button.MuiButtonBase-root',
      table: '.MuiTable-root, [role="grid"]',
      tabs: '.MuiTabs-root',
      tab: '.MuiTab-root, [role="tab"]',
      input: '.MuiInputBase-input, input.MuiInput-input',
      select: '.MuiSelect-root',
      dialog: '.MuiDialog-root',
      navItem: 'a[href], [role="menuitem"], .MuiListItem-root'
    },

    // Find a card container by its heading text (case-insensitive partial match).
    // Walks up from the heading element to find the MuiCard/MuiPaper ancestor.
    findCard: function(title) {
      var MUI = this._MUI;
      var headings = document.querySelectorAll(MUI.heading);
      for (var i = 0; i < headings.length; i++) {
        var h = headings[i];
        if (h.textContent.trim().toLowerCase().indexOf(title.toLowerCase()) !== -1) {
          var el = h;
          for (var j = 0; j < 5; j++) {
            el = el.parentElement;
            if (!el) return null;
            if (el.matches && el.matches(MUI.card)) return el;
          }
          return h.parentElement && h.parentElement.parentElement ? h.parentElement.parentElement : null;
        }
      }
      return null;
    },

    // Find a button by its exact label text (case-insensitive).
    // Falls back to aria-label match.
    findButton: function(label) {
      var MUI = this._MUI;
      var buttons = document.querySelectorAll(MUI.button);
      for (var i = 0; i < buttons.length; i++) {
        if (buttons[i].textContent.trim().toLowerCase() === label.toLowerCase()) return buttons[i];
      }
      var safeLabel = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(label) : label.replace(/["'\\]/g, '');
      return document.querySelector('[aria-label="' + safeLabel + '"]');
    },

    // Find a tab by its label text (case-insensitive partial match).
    findTab: function(label) {
      var MUI = this._MUI;
      var tabs = document.querySelectorAll(MUI.tab);
      for (var i = 0; i < tabs.length; i++) {
        if (tabs[i].textContent.trim().toLowerCase().indexOf(label.toLowerCase()) !== -1) return tabs[i];
      }
      return null;
    },

    // Find a navigation item by label text (sidebar links, menu items).
    findNav: function(label) {
      var MUI = this._MUI;
      var items = document.querySelectorAll(MUI.navItem);
      for (var i = 0; i < items.length; i++) {
        if (items[i].textContent.trim().toLowerCase().indexOf(label.toLowerCase()) !== -1) return items[i];
      }
      return null;
    },

    // Find an input by its associated label text.
    findInput: function(label) {
      var labels = document.querySelectorAll('label');
      for (var i = 0; i < labels.length; i++) {
        var lbl = labels[i];
        if (lbl.textContent.trim().toLowerCase().indexOf(label.toLowerCase()) !== -1) {
          var forId = lbl.getAttribute('for');
          if (forId) return document.getElementById(forId);
          return lbl.querySelector('input, textarea, select') ||
                 (lbl.parentElement ? lbl.parentElement.querySelector('input, textarea, select') : null);
        }
      }
      return null;
    },

    // Find a table column header by text. Returns {element, index} or null.
    findTableColumn: function(headerText) {
      var headers = document.querySelectorAll('th, [role="columnheader"]');
      for (var i = 0; i < headers.length; i++) {
        if (headers[i].textContent.trim().toLowerCase().indexOf(headerText.toLowerCase()) !== -1) {
          return { element: headers[i], index: Array.from(headers[i].parentElement.children).indexOf(headers[i]) };
        }
      }
      return null;
    },

    // Find a table row by matching a cell value in a specific column.
    findTableRow: function(columnHeader, cellValue) {
      var col = this.findTableColumn(columnHeader);
      if (!col) return null;
      var rows = document.querySelectorAll('tbody tr, [role="row"]');
      for (var i = 0; i < rows.length; i++) {
        var cells = rows[i].querySelectorAll('td, [role="gridcell"]');
        if (cells[col.index] && cells[col.index].textContent.trim().indexOf(cellValue) !== -1) return rows[i];
      }
      return null;
    },

    // ── MUI-Aware Actions (find + interact) ─────────────────

    clickNav: function(label) { var el = this.findNav(label); if (el) { el.click(); this.log('clickNav', label); return true; } return false; },
    clickButton: function(label) { var el = this.findButton(label); if (el) { el.click(); this.log('clickButton', label); return true; } return false; },
    clickTab: function(label) { var el = this.findTab(label); if (el) { el.click(); this.log('clickTab', label); return true; } return false; },
    clickTableRow: function(col, val) { var row = this.findTableRow(col, val); if (row) { var link = row.querySelector('a') || row; link.click(); this.log('clickTableRow', col + ':' + val); return true; } return false; },

    fillInput: function(label, value) {
      var el = this.findInput(label);
      if (!el) return false;
      var proto = el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
      var desc = Object.getOwnPropertyDescriptor(proto, 'value');
      if (desc && desc.set) desc.set.call(el, value); else el.value = value;
      el.dispatchEvent(new Event('input', { bubbles: true }));
      el.dispatchEvent(new Event('change', { bubbles: true }));
      this.log('fillInput', label, value);
      return true;
    },

    navigateTo: function(path) {
      window.location.hash = path.indexOf('#') === 0 ? path : '#' + path;
      this.log('navigateTo', path);
      return true;
    },

    // ── Session & Login Helpers ───────────────────────────────
    // Detect login state and perform programmatic login in ONE
    // evaluate_script call — replaces 4-6 separate MCP round trips.

    // Check if we're on a login page or already authenticated.
    // Returns { loggedIn: true/false, hasForm: true/false }
    isLoggedIn: function() {
      var pwField = document.querySelector('input[type="password"]');
      var loginBtn = document.querySelector('button[type="submit"], button[class*="login"], button[class*="Login"]');
      var hasForm = !!(pwField && loginBtn);
      var hasNav = document.querySelectorAll('nav a, [class*="sidebar"] a, [class*="Sidebar"] a').length > 2;
      var hasAppShell = !!document.querySelector('[class*="AppBar"], [class*="appbar"], header[role="banner"]');
      return {
        loggedIn: !hasForm && (hasNav || hasAppShell),
        hasForm: hasForm,
        url: location.href
      };
    },

    // Fill and submit the login form programmatically.
    // Replaces: take_snapshot + type_text(user) + type_text(pass) + click(submit) = 4 MCP calls → 1 call.
    // Returns { submitted: true/false, error?: string }
    loginForm: function(username, password) {
      var emailField = document.querySelector('input[type="email"], input[type="text"][name*="user"], input[type="text"][name*="email"], input[name="username"]');
      var pwField = document.querySelector('input[type="password"]');
      var submitBtn = document.querySelector('button[type="submit"], button[class*="login"], button[class*="Login"], input[type="submit"]');
      if (!emailField || !pwField || !submitBtn) {
        return { submitted: false, error: 'Login form fields not found' };
      }
      var desc = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value');
      var setVal = function(el, val) {
        if (desc && desc.set) desc.set.call(el, val); else el.value = val;
        el.dispatchEvent(new Event('input', { bubbles: true }));
        el.dispatchEvent(new Event('change', { bubbles: true }));
      };
      setVal(emailField, username);
      setVal(pwField, password);
      submitBtn.click();
      this.log('loginForm', username);
      return { submitted: true };
    },

    // Convenience wrapper: check login state + submit form in ONE evaluate_script call.
    // Replaces: take_snapshot + detect form + type_text(user) + type_text(pass) + click = 5 calls → 1 call.
    // Returns { skipped: true, alreadyLoggedIn: true } if already logged in.
    // Returns { submitted: true } if form was filled and submitted.
    // Returns { submitted: false, error: '...' } if form not found or error.
    login: function(username, password) {
      var state = this.isLoggedIn();
      if (state.loggedIn) return { skipped: true, alreadyLoggedIn: true };
      if (!state.hasForm) return { submitted: false, error: 'No login form detected on page' };
      return this.loginForm(username, password);
    },

    // Execute multiple operations in one evaluate_script call.
    // Biggest token saver — N operations for ~50 tokens each instead of N round-trips.
    // operations: [{action: 'clickTab', args: ['Details']}, {action: 'findCard', args: ['Summary']}]
    batch: function(operations) {
      var results = [];
      for (var i = 0; i < operations.length; i++) {
        var op = operations[i];
        try {
          var fn = this[op.action];
          if (typeof fn === 'function') {
            results.push({ action: op.action, success: fn.apply(this, op.args || []) });
          } else {
            results.push({ action: op.action, success: false, error: 'unknown action' });
          }
        } catch (e) {
          results.push({ action: op.action, success: false, error: e.message });
        }
      }
      return results;
    },

    // ── Layer 3: Element Operations (no snapshot needed) ──────

    // Find element by text content + optional role filter.
    // Tries: button text, aria-label, data-testid, data-ref, XPath text.
    // Returns the DOM element or null.
    find: function(text, role) {
      // Strategy 1: role-based query (e.g., "button", "a", "[role='tab']")
      if (role) {
        var els = document.querySelectorAll(role);
        for (var i = 0; i < els.length; i++) {
          if (els[i].textContent.trim().indexOf(text) !== -1 && els[i].offsetParent !== null) return els[i];
        }
      }
      // Strategy 2: aria-label (escape to prevent selector injection)
      var safe = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(text) : text.replace(/["'\\]/g, '');
      var byLabel = document.querySelector('[aria-label*="' + safe + '"]');
      if (byLabel && byLabel.offsetParent !== null) return byLabel;
      // Strategy 3: data-testid / data-ref
      var slug = text.toLowerCase().replace(/\s+/g, '-');
      var safeSlug = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(slug) : slug.replace(/["'\\]/g, '');
      var byTestId = document.querySelector('[data-testid*="' + safeSlug + '"], [data-ref*="' + safeSlug + '"]');
      if (byTestId && byTestId.offsetParent !== null) return byTestId;
      // Strategy 4: visible text via TreeWalker (fast, no XPath)
      var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false);
      var node;
      while ((node = walker.nextNode())) {
        if (node.offsetParent !== null && node.children.length === 0 && node.textContent.trim() === text) return node;
      }
      return null;
    },

    // Check if element matching text exists and is visible (returns boolean).
    // Cheap — no snapshot needed. Use for existence checks before clicking.
    exists: function(text, role) {
      return this.find(text, role) !== null;
    },

    // Get all visible user action buttons (workflow-driven).
    // Returns compact array: [{text, enabled, primary}]
    getActionButtons: function() {
      var results = [];
      document.querySelectorAll('button').forEach(function(b) {
        if (b.offsetParent === null) return;
        var text = b.textContent.trim();
        if (!text || text.length > 60) return;
        results.push({
          text: text,
          enabled: !b.disabled,
          primary: (b.className || '').indexOf('primary') !== -1 || (b.className || '').indexOf('Primary') !== -1
        });
      });
      return results;
    },

    // ── DOM Injection Preview (native MUI cloning) ─────────────
    //
    // Injects new UI elements that look identical to Mystique-rendered
    // components by cloning MUI classes from existing same-type elements.
    // Only the outer wrapper gets a highlight/glow — content is native.
    //
    // Supported types: card, column, tab, row, chip, section
    //
    // Usage:
    //   __fcTest.inject('card', { title: 'Order Channel', after: 'SUMMARY', attributes: [{label:'Channel',value:'WEB'}] })
    //   __fcTest.inject('column', { header: 'Channel', after: 'Status', values: ['WEB','MOBILE','POS'] })
    //   __fcTest.inject('tab', { label: 'Appeasements', after: 'Returns' })

    _injectStyles: null,

    _ensureInjectStyles: function() {
      if (this._injectStyles) return;
      var s = document.createElement('style');
      s.id = '__fc-inject-styles';
      s.textContent = [
        '@keyframes fcGlow{0%,100%{box-shadow:0 0 8px 2px rgba(76,175,80,.5)}50%{box-shadow:0 0 16px 4px rgba(76,175,80,.2)}}',
        '.__fc-injected{position:relative;animation:fcGlow 2s infinite}',
        '.__fc-injected-badge{position:absolute;top:-10px;right:-10px;background:#4caf50;color:#fff;font:bold 9px/1 system-ui;padding:3px 7px;border-radius:10px;z-index:1000;text-transform:uppercase;letter-spacing:.5px;pointer-events:none}'
      ].join('\n');
      document.head.appendChild(s);
      this._injectStyles = s;
    },

    // Wrap an element with glow border + "NEW" badge
    _wrapWithGlow: function(el, label) {
      el.classList.add('__fc-injected');
      el.style.outline = '2px solid #4caf50';
      el.style.outlineOffset = '2px';
      var badge = document.createElement('span');
      badge.className = '__fc-injected-badge';
      badge.textContent = label || 'NEW';
      el.appendChild(badge);
      return el;
    },

    // Clone classes from a donor element onto a new element
    _cloneClasses: function(donor, target) {
      if (donor && donor.className) {
        target.className = donor.className.split(' ').filter(function(c) {
          return c && c.indexOf('__fc-') === -1;
        }).join(' ');
      }
    },

    // Main inject dispatcher
    inject: function(type, opts) {
      this._ensureInjectStyles();
      opts = opts || {};
      var result;
      switch (type) {
        case 'card': result = this._injectCard(opts); break;
        case 'column': result = this._injectColumn(opts); break;
        case 'tab': result = this._injectTab(opts); break;
        case 'row': result = this._injectRow(opts); break;
        case 'chip': result = this._injectChip(opts); break;
        case 'section': result = this._injectSection(opts); break;
        default: return { success: false, error: 'Unknown type: ' + type + '. Supported: card, column, tab, row, chip, section' };
      }
      if (result.success) this.log('inject', type, opts.title || opts.header || opts.label || '');
      return result;
    },

    // Remove all injected preview elements
    clearInjected: function() {
      document.querySelectorAll('.__fc-injected').forEach(function(el) { el.remove(); });
      var s = document.getElementById('__fc-inject-styles');
      if (s) s.remove();
      this._injectStyles = null;
      return { cleared: true };
    },

    // ── inject('card') ─────────────────────────────────────
    // Clones MUI Paper/Card structure from an existing card on the page.
    // opts: { title, after?, attributes: [{label, value}], content? }
    _injectCard: function(opts) {
      // Find a donor card to clone classes from
      var donor = document.querySelector('.MuiPaper-root.MuiCard-root') || document.querySelector('.MuiPaper-root');
      if (!donor) return { success: false, error: 'No MUI card found on page to clone from' };

      // Build the card
      var card = document.createElement('div');
      this._cloneClasses(donor, card);

      // Clone card content structure — find the content wrapper
      var donorContent = donor.querySelector('.MuiCardContent-root');
      var content = document.createElement('div');
      if (donorContent) this._cloneClasses(donorContent, content);
      else content.style.padding = '16px';

      // Title — clone from existing card header/typography
      var donorTitle = donor.querySelector('.MuiTypography-root');
      var title = document.createElement('div');
      if (donorTitle) {
        this._cloneClasses(donorTitle, title);
      } else {
        title.style.cssText = 'font-weight:500;font-size:14px;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px';
      }
      title.textContent = opts.title || 'New Card';
      content.appendChild(title);

      // Attributes — clone grid/typography from existing attribute pairs
      if (opts.attributes && opts.attributes.length) {
        var grid = document.createElement('div');
        // Look for an attribute grid in existing cards
        var donorGrid = donor.querySelector('[class*="Grid-container"], [class*="grid"]');
        if (donorGrid) {
          this._cloneClasses(donorGrid, grid);
        } else {
          grid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px 16px';
        }

        var self = this;
        opts.attributes.forEach(function(attr) {
          var labelEl = document.createElement('div');
          var valueEl = document.createElement('div');

          // Clone typography from existing label/value pairs
          var donorLabel = donor.querySelector('[class*="caption"], [class*="body2"]');
          var donorValue = donor.querySelector('[class*="body1"], [class*="subtitle"]');
          if (donorLabel) self._cloneClasses(donorLabel, labelEl);
          else labelEl.style.cssText = 'font-size:12px;color:rgba(0,0,0,.54)';
          if (donorValue) self._cloneClasses(donorValue, valueEl);
          else valueEl.style.cssText = 'font-size:14px;color:rgba(0,0,0,.87)';

          labelEl.textContent = attr.label;
          valueEl.textContent = attr.value;
          grid.appendChild(labelEl);
          grid.appendChild(valueEl);
        });
        content.appendChild(grid);
      }

      // Custom content
      if (opts.content) {
        var custom = document.createElement('div');
        custom.textContent = opts.content;
        content.appendChild(custom);
      }

      card.appendChild(content);

      // Insert after specified card, or append to cards container
      var inserted = false;
      if (opts.after) {
        var cards = document.querySelectorAll('.MuiPaper-root.MuiCard-root, .MuiPaper-root');
        for (var i = 0; i < cards.length; i++) {
          var heading = cards[i].querySelector('.MuiTypography-root');
          if (heading && heading.textContent.trim().toUpperCase().indexOf(opts.after.toUpperCase()) !== -1) {
            cards[i].parentNode.insertBefore(this._wrapWithGlow(card, 'NEW CARD'), cards[i].nextSibling);
            inserted = true;
            break;
          }
        }
      }
      if (!inserted) {
        var container = donor.parentNode;
        container.appendChild(this._wrapWithGlow(card, 'NEW CARD'));
      }
      return { success: true, type: 'card', title: opts.title };
    },

    // ── inject('column') ───────────────────────────────────
    // Clones MUI table header/cell structure from existing columns.
    // opts: { header, after?, values: string[] | fn(row,i) }
    _injectColumn: function(opts) {
      var table = document.querySelector('table');
      if (!table) return { success: false, error: 'No table found on page' };

      var headerRow = table.querySelector('thead tr');
      if (!headerRow) return { success: false, error: 'No table header row found' };

      // Find insertion index
      var insertIdx = -1;
      if (opts.after) {
        var ths = headerRow.querySelectorAll('th');
        for (var i = 0; i < ths.length; i++) {
          if (ths[i].textContent.trim().toUpperCase().indexOf(opts.after.toUpperCase()) !== -1) {
            insertIdx = i + 1;
            break;
          }
        }
      }

      // Clone header cell from existing th
      var donorTh = headerRow.querySelector('th');
      var th = document.createElement('th');
      if (donorTh) this._cloneClasses(donorTh, th);
      th.textContent = opts.header || 'New Column';
      this._wrapWithGlow(th, 'NEW');

      // Insert header
      if (insertIdx >= 0 && insertIdx < headerRow.children.length) {
        headerRow.insertBefore(th, headerRow.children[insertIdx]);
      } else {
        headerRow.appendChild(th);
        insertIdx = headerRow.children.length - 1;
      }

      // Insert data cells
      var rows = table.querySelectorAll('tbody tr');
      var values = opts.values || [];
      var rowCount = 0;
      var self = this;
      rows.forEach(function(row, i) {
        var donorTd = row.querySelector('td');
        var td = document.createElement('td');
        if (donorTd) self._cloneClasses(donorTd, td);

        // Get value — array or function
        var val = '';
        if (typeof values === 'function') val = values(row, i);
        else if (Array.isArray(values)) val = values[i % values.length] || '';
        else val = String(values);

        td.textContent = val;
        td.style.outline = '2px solid rgba(76,175,80,.3)';

        if (insertIdx >= 0 && insertIdx < row.children.length) {
          row.insertBefore(td, row.children[insertIdx]);
        } else {
          row.appendChild(td);
        }
        rowCount++;
      });

      return { success: true, type: 'column', header: opts.header, rowsUpdated: rowCount };
    },

    // ── inject('tab') ──────────────────────────────────────
    // Clones MUI Tab structure from existing tabs on the page.
    // opts: { label, after? }
    _injectTab: function(opts) {
      var tabBar = document.querySelector('[role="tablist"]');
      if (!tabBar) return { success: false, error: 'No tab bar found on page' };

      // Find donor tab
      var donorTab = tabBar.querySelector('[role="tab"]');
      if (!donorTab) return { success: false, error: 'No tabs found in tab bar' };

      var tab = document.createElement('button');
      this._cloneClasses(donorTab, tab);
      tab.setAttribute('role', 'tab');
      tab.setAttribute('aria-selected', 'false');
      tab.textContent = opts.label || 'New Tab';

      // Insert after specified tab
      var inserted = false;
      if (opts.after) {
        var tabs = tabBar.querySelectorAll('[role="tab"]');
        for (var i = 0; i < tabs.length; i++) {
          if (tabs[i].textContent.trim().toUpperCase().indexOf(opts.after.toUpperCase()) !== -1) {
            tabs[i].parentNode.insertBefore(this._wrapWithGlow(tab, 'NEW TAB'), tabs[i].nextSibling);
            inserted = true;
            break;
          }
        }
      }
      if (!inserted) {
        tabBar.appendChild(this._wrapWithGlow(tab, 'NEW TAB'));
      }
      return { success: true, type: 'tab', label: opts.label };
    },

    // ── inject('row') ──────────────────────────────────────
    // Adds a new row to an existing table, cloning cell classes.
    // opts: { values: {colHeader: value}, after?: rowIndex, highlight?: true }
    _injectRow: function(opts) {
      var table = document.querySelector('table');
      if (!table) return { success: false, error: 'No table found on page' };

      var headerRow = table.querySelector('thead tr');
      var tbody = table.querySelector('tbody');
      if (!headerRow || !tbody) return { success: false, error: 'No table header or body found' };

      var headers = [];
      headerRow.querySelectorAll('th').forEach(function(th) {
        headers.push(th.textContent.trim());
      });

      var donorRow = tbody.querySelector('tr');
      var tr = document.createElement('tr');
      if (donorRow) this._cloneClasses(donorRow, tr);

      var self = this;
      var values = opts.values || {};
      headers.forEach(function(h, i) {
        var donorTd = donorRow ? donorRow.children[i] : null;
        var td = document.createElement('td');
        if (donorTd) self._cloneClasses(donorTd, td);
        td.textContent = values[h] || '';
        tr.appendChild(td);
      });

      // Insert position
      if (typeof opts.after === 'number' && opts.after < tbody.children.length) {
        tbody.insertBefore(tr, tbody.children[opts.after + 1]);
      } else {
        tbody.insertBefore(tr, tbody.firstChild); // prepend by default
      }

      this._wrapWithGlow(tr, 'NEW ROW');
      return { success: true, type: 'row', columns: headers.length };
    },

    // ── inject('chip') ─────────────────────────────────────
    // Adds a MUI Chip inside a target element (found by text or selector).
    // opts: { text, color?, target?, after? }
    _injectChip: function(opts) {
      // Find donor chip on page
      var donorChip = document.querySelector('.MuiChip-root');
      var chip = document.createElement('span');
      if (donorChip) {
        this._cloneClasses(donorChip, chip);
      } else {
        chip.style.cssText = 'display:inline-flex;align-items:center;height:24px;padding:0 10px;border-radius:16px;font-size:12px;font-weight:500;background:rgba(0,0,0,.08)';
      }

      // Apply color
      if (opts.color) {
        chip.style.backgroundColor = opts.color;
        chip.style.color = '#fff';
      }

      var label = document.createElement('span');
      var donorLabel = donorChip ? donorChip.querySelector('.MuiChip-label') : null;
      if (donorLabel) this._cloneClasses(donorLabel, label);
      label.textContent = opts.text || 'Chip';
      chip.appendChild(label);

      // Place inside target
      var target = opts.target
        ? (document.querySelector(opts.target) || this.find(opts.target))
        : document.body;
      if (target) {
        target.appendChild(this._wrapWithGlow(chip, 'NEW'));
      }
      return { success: true, type: 'chip', text: opts.text };
    },

    // ── inject('section') ──────────────────────────────────
    // Adds a new page section/accordion, cloning from existing sections.
    // opts: { title, after?, content? }
    _injectSection: function(opts) {
      // Find an existing section header
      var donorSection = document.querySelector('[class*="Accordion"]') || document.querySelector('.MuiPaper-root');
      if (!donorSection) return { success: false, error: 'No section found on page to clone from' };

      var section = document.createElement('div');
      this._cloneClasses(donorSection, section);

      var header = document.createElement('div');
      var donorHeader = donorSection.querySelector('[class*="AccordionSummary"], [class*="CardContent"]');
      if (donorHeader) this._cloneClasses(donorHeader, header);
      else header.style.cssText = 'padding:16px;font-weight:500;font-size:16px';
      header.textContent = opts.title || 'New Section';
      section.appendChild(header);

      if (opts.content) {
        var body = document.createElement('div');
        body.style.padding = '0 16px 16px';
        body.textContent = opts.content;
        section.appendChild(body);
      }

      // Insert after specified section
      var inserted = false;
      if (opts.after) {
        var all = document.querySelectorAll('.MuiPaper-root');
        for (var i = 0; i < all.length; i++) {
          var heading = all[i].querySelector('.MuiTypography-root, [class*="AccordionSummary"]');
          if (heading && heading.textContent.trim().toUpperCase().indexOf(opts.after.toUpperCase()) !== -1) {
            all[i].parentNode.insertBefore(this._wrapWithGlow(section, 'NEW SECTION'), all[i].nextSibling);
            inserted = true;
            break;
          }
        }
      }
      if (!inserted) {
        var main = document.querySelector('[class*="content"], main, [role="main"]') || document.body;
        main.appendChild(this._wrapWithGlow(section, 'NEW SECTION'));
      }
      return { success: true, type: 'section', title: opts.title };
    },

    // ── Click Telemetry (highlight + click + log) ─────────────

    // Highlight element with pulsing red border + label badge.
    // Returns {found, rect} or {found: false}.
    highlight: function(selectorOrText, label) {
      this.clearHighlight();
      var el = typeof selectorOrText === 'string'
        ? (document.querySelector(selectorOrText) || this.find(selectorOrText, 'button'))
        : selectorOrText;
      if (!el) return { found: false };
      var r = el.getBoundingClientRect();
      var div = document.createElement('div');
      div.id = '__fc-highlight';
      div.style.cssText = 'position:fixed;top:' + (r.top - 4) + 'px;left:' + (r.left - 4) + 'px;width:' + (r.width + 8) + 'px;height:' + (r.height + 8) + 'px;border:3px solid #e94560;border-radius:4px;z-index:2147483646;pointer-events:none;animation:fcPulse .8s infinite';
      var badge = document.createElement('span');
      badge.textContent = label || '';
      badge.style.cssText = 'position:absolute;top:-22px;left:0;background:#e94560;color:#fff;font:bold 10px/1 sans-serif;padding:3px 8px;border-radius:3px;text-transform:uppercase;white-space:nowrap';
      div.appendChild(badge);
      document.body.appendChild(div);
      this.log('highlight', selectorOrText, label);
      return { found: true, rect: { top: r.top, left: r.left, width: r.width, height: r.height } };
    },

    clearHighlight: function() {
      var el = document.getElementById('__fc-highlight');
      if (el) el.remove();
    },

    // ── Annotation Banner (top of page) ───────────────────────

    annotate: function(text, type) {
      this.clearAnnotation();
      var div = document.createElement('div');
      div.id = '__fc-annotation';
      div.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:2147483647;background:linear-gradient(135deg,#1a1a2e,#16213e);padding:8px 16px;display:flex;align-items:center;gap:12px;border-bottom:3px solid #e94560;font-family:system-ui,-apple-system,sans-serif';
      var badge = document.createElement('span');
      badge.style.cssText = 'background:#e94560;color:#fff;font:bold 11px/1 sans-serif;padding:4px 10px;border-radius:4px;text-transform:uppercase';
      badge.textContent = type || 'STEP';
      var msg = document.createElement('span');
      msg.style.cssText = 'color:#eee;font-size:13px';
      msg.textContent = text;
      var ts = document.createElement('span');
      ts.style.cssText = 'margin-left:auto;color:#888;font-size:11px';
      ts.textContent = new Date().toLocaleTimeString();
      div.appendChild(badge);
      div.appendChild(msg);
      div.appendChild(ts);
      document.body.appendChild(div);
    },

    clearAnnotation: function() {
      var el = document.getElementById('__fc-annotation');
      if (el) el.remove();
    },

    // Clear all visual overlays (highlights, badges, annotations).
    clearAll: function() {
      this.clearHighlight();
      this.clearAnnotation();
      // Also clear multi-highlight badges from batch highlight operations
      document.querySelectorAll('[data-fc-highlight]').forEach(function(el) {
        el.style.outline = '';
        el.style.outlineOffset = '';
        el.removeAttribute('data-fc-highlight');
      });
    },

    // ── Action Timeline ───────────────────────────────────────

    _timeline: [],
    _start: Date.now(),

    log: function(action, target, label) {
      this._timeline.push({
        seq: this._timeline.length + 1,
        elapsed: Date.now() - this._start,
        action: action,
        target: typeof target === 'string' ? target : (target && target.textContent ? target.textContent.trim().substring(0, 50) : ''),
        label: label || ''
      });
    },

    getTimeline: function() {
      return {
        totalActions: this._timeline.length,
        durationMs: Date.now() - this._start,
        actions: this._timeline
      };
    },

    // ── State Diff (for before/after comparison in ui-record) ──
    // Compare two pageState() snapshots and return only what changed.
    // Saves ~5K tokens per comparison vs LLM reasoning about raw snapshots.
    // Usage: var before = __fcTest.pageState(); /* ...change... */ var after = __fcTest.pageState(); __fcTest.diffState(before, after);
    diffState: function(before, after) {
      var diff = { added: {}, removed: {}, changed: {} };
      ['headings', 'buttons'].forEach(function(key) {
        var b = before[key] || [];
        var a = after[key] || [];
        var added = a.filter(function(x) { return b.indexOf(x) === -1; });
        var removed = b.filter(function(x) { return a.indexOf(x) === -1; });
        if (added.length) diff.added[key] = added;
        if (removed.length) diff.removed[key] = removed;
      });
      var bTabs = (before.tabs || []).map(function(t) { return t.label; });
      var aTabs = (after.tabs || []).map(function(t) { return t.label; });
      var addedTabs = aTabs.filter(function(t) { return bTabs.indexOf(t) === -1; });
      var removedTabs = bTabs.filter(function(t) { return aTabs.indexOf(t) === -1; });
      if (addedTabs.length) diff.added.tabs = addedTabs;
      if (removedTabs.length) diff.removed.tabs = removedTabs;
      if (before.tables !== after.tables) diff.changed.tables = { before: before.tables, after: after.tables };
      if (before.dialogs !== after.dialogs) diff.changed.dialogs = { before: before.dialogs, after: after.dialogs };
      if (before.url !== after.url) diff.changed.url = { before: before.url, after: after.url };
      diff.hasChanges = Object.keys(diff.added).length > 0 || Object.keys(diff.removed).length > 0 || Object.keys(diff.changed).length > 0;
      return diff;
    },

    // ── SPA Navigation Guard ─────────────────────────────────
    // Tracks hash-change navigations and preserves timeline across page reloads.
    // Call once after injection. Idempotent — safe to call multiple times.
    installGuard: function() {
      if (window.__fcTestGuardInstalled) return { installed: true, alreadyInstalled: true };
      window.__fcTestGuardInstalled = true;
      var self = this;
      window.addEventListener('hashchange', function() {
        if (window.__fcTest) window.__fcTest.log('navigate', window.location.hash, 'hashchange');
      });
      window.addEventListener('beforeunload', function() {
        try { sessionStorage.setItem('__fcTestTimeline', JSON.stringify(self._timeline)); } catch(e) {}
      });
      try {
        var saved = sessionStorage.getItem('__fcTestTimeline');
        if (saved) { self._timeline = JSON.parse(saved); sessionStorage.removeItem('__fcTestTimeline'); }
      } catch(e) {}
      self.log('guard', 'installed', 'SPA navigation guard active');
      return { installed: true, hashChangeListener: true };
    }
  };

  // Inject CSS keyframes (once)
  var style = document.createElement('style');
  style.id = '__fc-test-styles';
  style.textContent = '@keyframes fcPulse{0%,100%{box-shadow:0 0 0 0 rgba(233,69,96,.4)}50%{box-shadow:0 0 0 8px rgba(233,69,96,0)}}';
  document.head.appendChild(style);
})();
```

**Size:** ~5KB minified. Inject once → use for entire session.

---

## Login Persistence (one-time setup)

Add `--storage-state` to the Chrome DevTools MCP config to persist cookies/session across MCP restarts. This avoids re-running the login flow every time.

```json
{
  "chrome-devtools": {
    "command": "npx",
    "args": [
      "-y", "chrome-devtools-mcp@latest",
      "--storage-state=./chrome-session.json"
    ]
  }
}
```

**Savings:** ~15 seconds + ~10K tokens per session (skips login navigation, form fill, redirect wait, post-login verify).

**How it works:** Chrome DevTools MCP serializes cookies and localStorage to the JSON file on exit and restores them on next launch. The Fluent app sees a valid session and skips login entirely. `isLoggedIn()` returns `{loggedIn: true}` immediately.

**When to regenerate:** If `isLoggedIn()` returns `{loggedIn: false, hasForm: true}` despite `--storage-state` being set, the stored session has expired. Delete `chrome-session.json` and log in once — the file is recreated automatically.

---

## SPA Navigation and Helper Persistence

Fluent UX apps use hash-based SPA routing (`#/orders/123`). The `__fcTest` helper library persists across SPA navigations because hash changes don't cause a full page reload.

**When `__fcTest` is lost:**
- Full page reload (`navigate_page` to a new URL)
- Browser crash or tab close
- Navigation away from the Fluent app domain

**When `__fcTest` persists:**
- Hash navigation via `navigateTo()` or clicking sidebar links
- Tab switching, dialog open/close, back/forward within the SPA
- `evaluate_script` calls (reads but doesn't modify the page)

**Guard pattern:** Always pass `initScript` on every `navigate_page` call. The IIFE starts with `if (window.__fcTest) return;` — zero overhead when already injected. This makes re-injection automatic and invisible.

---

## Component Compatibility Matrix

Which optimization works for which Mystique component type? This determines whether `pageSummary()` is enough or you need `scanComponents()` or `take_snapshot`.

### OOTB Components (built into Fluent)

| Component Alias | Category | `pageSummary()` | `scanComponents()` | `take_snapshot` needed? |
|---|---|---|---|---|
| `fc.page` | page | `title` ✓ | — | No |
| `fc.tabs` / `fc.tabs.card` | layout | `tabs[]` ✓ | `type: "tabs"` ✓ | No |
| `fc.list` | content | `tables[]` ✓ | `type: "list"` ✓ | Only for cell values |
| `fc.card` / `fc.card.attribute` | content | `cards[]` ✓ | `type: "card"` ✓ | Only for attribute values |
| `fc.card.multi` | content | `cards[]` ✓ | `type: "card"` ✓ | Only for nested content |
| `fc.accordion` | layout | Partial (as `cards`) | `type: "accordion"` ✓ | No |
| `fc.page.section` / `.header` | layout | Partial (in `title`) | `type: "section"` ✓ | No |
| `fc.modal.button` | action | `buttons[]` ✓ | `type: "dialog"` ✓ | Only for dialog field discovery |
| `fc.settingForm` | content | `forms[]` ✓ | — | No |
| `fc.filterPanel` | filter | `forms[]` ✓ | — | No |
| `fc.chart.*` (bar/line/gauge) | content | Not detected | `verifyComponent()` ✓ | Only for chart data |
| `fc.stepper` | content | Not detected | `verifyComponent()` ✓ | Only for step content |
| `fc.conditional` | layout | Not detected directly | — | Yes (hidden children) |
| `fc.repeater` | layout | Not detected directly | — | Yes (repeated content) |

### Custom SDK Components

| Component Type | `pageSummary()` | `scanComponents()` | `verifyComponent()` | `take_snapshot`? |
|---|---|---|---|---|
| Custom `ComponentRegistry` (renders MUI Card) | `cards[]` ✓ | `type: "card"` ✓ | By title ✓ | No |
| Custom `ComponentRegistry` (renders MUI Table) | `tables[]` ✓ | `type: "list"` ✓ | By header ✓ | No |
| Custom `ComponentRegistry` (renders custom HTML) | **Miss** | `type: "custom"` (if `data-testid`) | By title or testId | **Yes for initial discovery** |
| Custom `FieldRegistry` (standard input) | `forms[].fields[]` ✓ | `_scanDialogFields()` ✓ | By name ✓ | No |
| Custom `FieldRegistry` (MUI Autocomplete) | **Miss** | `type: "autocomplete"` ✓ | By label ✓ | No (after scanComponents) |
| Custom `FieldRegistry` (fully custom widget) | **Miss** | **Miss** | — | **Yes** |
| Custom `TemplateRegistry` helper | N/A (renders inside other components) | N/A | N/A | Depends on parent |
| React error boundary (crashed component) | `alerts[]` (if role=alert) | `type: "error-boundary"` ✓ | — | Yes (for debugging) |

### Decision Guide

```
Is the component an OOTB fc.* with a standard MUI rendering?
├── YES → pageSummary() handles it                           (~500 tokens)
│
├── PARTIALLY (chart, stepper, conditional) →
│   Use verifyComponent("fc.chart.bar") to check existence   (~100 tokens)
│   → take_snapshot ONLY if you need chart data values       (~30K tokens)
│
└── NO (custom SDK component) →
    ├── Does it use data-testid? → scanComponents() finds it (~400 tokens)
    ├── Does it have a visible heading? → find(title) works  (~100 tokens)
    └── Fully custom DOM? → take_snapshot for first discovery (~30K tokens)
        → Then use find() with discovered selectors for subsequent interactions
```

**Rule for custom components:** Use `take_snapshot` ONCE to discover the DOM structure. Then build selectors from what you find and use `find()` / `verifyComponent()` for all subsequent checks. Don't snapshot every time — the structure won't change between interactions.

---

## Layer 4: Manifest Selector Cache (Pre-Analysis)

Before touching the browser, pre-analyze the deployed manifests to get correct element labels. This eliminates the "guess the label" problem (e.g., "Ref" vs "Order #") and replaces the first `take_snapshot` call entirely.

### Generate the cache

```bash
node fluent-ai-skills/fluent-ai-skills/tools/manifest-selector-map.mjs --profile <PROFILE>
```

Outputs three files in `accounts/<PROFILE>/manifests/`:

| File | Size | Purpose |
|---|---|---|
| `selector-cache.json` | ~490KB | Full analysis (reference, not for LLM) |
| `selector-cache-slim.json` | ~58KB (~15K tokens) | Navigation, buttons, cards, lists — **read this once per session** |
| `selector-cache-helper.js` | ~9KB | Browser helper JS (same methods now merged into `__fcTest`) |

### Use in Phase 0a (before helper injection)

```
1. Check if accounts/<PROFILE>/manifests/selector-cache-slim.json exists
2. If missing → run: node fluent-ai-skills/fluent-ai-skills/tools/manifest-selector-map.mjs --profile <PROFILE>
3. Read selector-cache-slim.json (~15K tokens, once per session)
4. Use the labels from the cache for all subsequent __fcTest calls
```

The slim cache provides:
- **navigation[]** — exact sidebar link labels and paths
- **actionButtons[]** — user action button labels and event names per page
- **cardTitles[]** — exact card heading text per page
- **listPages[]** — exact table column headers per page

Example: instead of guessing `findTableColumn('Ref')` (MISS), the cache tells you the correct label is `"Order #"`.

### Token savings

| Without cache | With cache |
|---|---|
| `take_snapshot` to discover labels: **~30K tokens** per page | Read slim cache once: **~15K tokens** total for ALL pages |
| Repeat per page × 10 pages = **~300K tokens** | One-time read + exact finders = **~15K + ~1K = ~16K tokens** |
| **Savings: ~95%** for label discovery | |

---

## Decision Tree: When to Use What

```
Quick check — what's on screen right now?
├── pageState() → headings, buttons, tabs, table/dialog count  (~150 tokens)
│   ├── Enough to decide next step → DONE (cheapest option)
│   └── Need more detail → pageSummary()                       (~500 tokens)

Need to verify page loaded correctly?
├── pageState() → check headings, buttons, tabs               (~150 tokens)
│   ├── Matches expectations → PASS, no snapshot needed
│   └── Unexpected state → pageSummary() for detail            (~500 tokens)
│       └── Still unclear → ESCALATE to take_snapshot          (~30K tokens)

Need to find a specific element?
├── Know the type? Use a specific MUI finder:
│   ├── Card     → findCard("Summary")                         (~50 tokens)
│   ├── Button   → findButton("Cancel Order")                  (~50 tokens)
│   ├── Tab      → findTab("Attributes")                       (~50 tokens)
│   ├── Nav link → findNav("Orders")                           (~50 tokens)
│   ├── Input    → findInput("Reason")                         (~50 tokens)
│   ├── Column   → findTableColumn("Status")                   (~50 tokens)
│   └── Table row → findTableRow("Status", "CREATED")          (~50 tokens)
├── Don't know the type?
│   ├── __fcTest.find("Cancel Order", "button")                (~100 tokens)
│   └── pageState() to discover what's on page                 (~150 tokens)
│       └── Still can't find → take_snapshot fallback          (~30K tokens)

Need to click something?
├── clickButton("Cancel Order")                                (~50 tokens)
├── clickTab("Attributes")                                     (~50 tokens)
├── clickNav("Orders")                                         (~50 tokens)
├── clickTableRow("Status", "CREATED")                         (~50 tokens)
└── batch([{action:'clickTab',args:['Details']},{action:'pageState'}])  (~100 tokens, 2 ops)

Need to fill a form?
├── Standard fields (input/select/textarea)?
│   ├── YES → findInput("fieldName")                           (~50 tokens)
│   └── Has MUI Autocomplete/custom FieldRegistry?
│       ├── __fcTest._scanDialogFields(dialogEl)               (~400 tokens)
│       └── Still can't interact → take_snapshot               (~30K tokens)

Need to verify a CUSTOM SDK component?
├── Know the component alias (from manifest)?
│   ├── verifyComponent("fc.custom.my-widget")            (~100 tokens)
│   │   ├── {found: true} → PASS, component rendered
│   │   └── {found: false} → scanComponents() for full scan (~400 tokens)
│   │       └── Still not found → take_snapshot ONCE      (~30K tokens)
│   │           → Build selectors from discovered DOM
│   │           → Use find() for subsequent checks
│   └── Don't know the alias?
│       ├── scanComponents() to enumerate what's on page  (~400 tokens)
│       └── Need more detail → take_snapshot              (~30K tokens)

Need to preview a manifest change before deploying?
├── Adding a card?     → inject('card', {title, after, attributes})     (~100 tokens)
├── Adding a column?   → inject('column', {header, after, values})      (~100 tokens)
├── Adding a tab?      → inject('tab', {label, after})                  (~100 tokens)
├── Adding a row?      → inject('row', {values: {col: val}})            (~100 tokens)
├── Multiple changes?  → batch([inject, inject, inject, annotate])      (~200 tokens)
└── Clear preview?     → clearInjected()                                (~50 tokens)

Need before/after comparison (ui-record)?
├── diffState(pageState(), pageState())                  (~300 tokens — cheapest diff)
│   ├── hasChanges=true → shows added/removed headings, buttons, tabs, table/dialog/url changes
│   └── Need precise DOM delta → take_snapshot both      (~60K tokens)

Need to verify data loaded?
├── pageSummary().tables[0].rowCount > 0                 (~500 tokens)
│   └── Need column values → take_snapshot               (~30K tokens)

Need to debug a failure?
├── ALWAYS use take_snapshot                             (~30K tokens)
│   └── Plus list_console_messages + list_network_requests
│       └── Check scanComponents() for error-boundary type
```

**Rule of thumb:** Start with `pageState()` (~150 tokens). If you need more detail, use `pageSummary()` (~500 tokens). Use specific MUI finders (`findCard`, `findButton`, `findTab`) for element interaction (~50 tokens each). Escalate to `take_snapshot` (~30K tokens) only when lightweight methods don't give enough information.

---

## Manifest-to-Selector Guide

The Mystique manifest defines the component tree. Use it to **pre-compute selectors locally** before touching the browser.

### Route → URL Mapping

```
Manifest route                          → Browser URL
─────────────────────────────────────────────────────
type: "section", path: "orders"         → /<app>/orders
type: "page", path: ":id"              → /<app>/orders/<id>
type: "section", path: "fulfilments"   → /<app>/fulfilments
```

**Usage:** Navigate directly to URLs. No sidebar click needed, no snapshot to find links.

### Component → Selector Mapping

| Manifest Component | DOM Selector Strategy | Tokens |
|---|---|---|
| `fc.page` (with entityType) | URL navigation — no selector needed | 0 |
| `fc.tabs.card` (with tabs[]) | `[role="tab"]` + tab label text | ~50 |
| `fc.list` (with columns[]) | `th` + column header text | ~50 |
| `fc.card` (with title) | Card heading text via `__fcTest.find(title)` | ~50 |
| `fc.accordion` | `__fcTest.verifyComponent("fc.accordion")` | ~100 |
| `fc.chart.*` | `__fcTest.verifyComponent("fc.chart.bar")` | ~100 |
| `fc.modal.button` (user action form) | `__fcTest.find("Button Label", "button")` → `scanComponents()` for dialog fields | ~400 |
| Custom SDK component (with title) | `__fcTest.verifyComponent("Component Title")` | ~100 |
| Custom SDK component (with data-testid) | `__fcTest.scanComponents()` → find `type: "custom"` | ~400 |
| Custom SDK component (unknown DOM) | `take_snapshot` ONCE → then `__fcTest.find(discovered_text)` | ~30K first, ~100 after |
| User action button | `__fcTest.find("Button Label", "button")` | ~50 |
| Sidebar link | `__fcTest.find("Link Text", "nav a")` or direct URL | ~50 |

### Pre-Computing Selectors from Manifest

Before interacting with the browser, build a local selector map from the manifest:

```
Given manifest fragment:
  routes: [{
    type: "section", path: "orders",
    descendants: [{
      type: "page", path: ":id",
      component: "fc.page",
      props: { entityType: "ORDER" },
      descendants: [{
        component: "fc.tabs.card",
        props: { tabs: ["Summary", "Items", "Fulfilments"] },
        descendants: [...]
      }]
    }]
  }]

Pre-computed selector map:
  orderList    → URL: /oms/orders
  orderDetail  → URL: /oms/orders/{id}
  summaryTab   → __fcTest.find("Summary", "[role='tab']")
  itemsTab     → __fcTest.find("Items", "[role='tab']")
  fulfilTab    → __fcTest.find("Fulfilments", "[role='tab']")

Given workflow user actions for ORDER::HD at BOOKED:
  cancelBtn    → __fcTest.find("Cancel Order", "button")
  confirmBtn   → __fcTest.find("Confirm Order", "button")
```

This map is built by the LLM from manifest + workflow JSON — **zero browser tokens**.

---

## Integration with Existing Click Telemetry

The helper library's `highlight()`, `annotate()`, and `log()` functions replace the inline JS injection patterns currently in both skills. The visual output is identical — pulsing red borders, label badges, annotation banners, action timeline.

### Before (current — per button, ~800 tokens of JS)

```javascript
// evaluate_script: inline JS to find element, calculate rect, create div,
// apply styles, add badge, append to body — ~40 lines of JS per call
```

### After (optimized — per button, ~100 tokens)

```javascript
// evaluate_script:
window.__fcTest.highlight("Cancel Order", "CANCEL ORDER")
```

### Timeline extraction

```javascript
// Before: inline JS to access __fcTimeline.getReport()
// After:
window.__fcTest.getTimeline()
```

---

## Phase → Layer Mapping

The skill phases (operational order) map to these documentation layers (architectural grouping):

| Execution Phase | Documentation Layer | What It Does | Token Cost |
|---|---|---|---|
| **Phase 0a** | Layer 4: Manifest Selector Cache | Read `selector-cache-slim.json` — all page labels offline | ~15K (once) |
| **Phase 0b** | Layer 1: Helper Library | Inject `__fcTest` via `initScript` on `navigate_page` | ~0 (embedded in navigate) |
| **Phase 1** | Session & Login Helpers | `isLoggedIn()` + `loginForm()` — detect + login in 1 call | ~200 |
| **Phase 2+** | Layer 2c: `pageState()` | Quick page read — headings, buttons, tabs | ~150 |
| **Phase 2+** | Layer 2d: MUI Finders | `findCard`, `findButton`, `findTab`, etc. | ~50 each |
| **Phase 2+** | Layer 2: `pageSummary()` | Detailed page state with loading/errors | ~500 |
| **Phase 2+** | Layer 2b: `scanComponents()` | Custom SDK component detection | ~400 |
| **Phase 2+** | Layer 3: Element Operations | `find()`, `exists()`, `highlight()` | ~100 each |
| **Preview** | Layer 5: DOM Injection | `inject('card'/'column'/'tab')` — native MUI preview | ~100 each |
| **Fallback** | (none) | `take_snapshot` — full DOM tree | ~30K |

**Execution order:** 0a → 0b → pageState → MUI finders → pageSummary (if needed) → inject (preview) → take_snapshot (last resort).

---

## Fallback Rules

1. **Always inject the helper library** at session start (Phase 0b). If injection fails, fall back to raw Chrome DevTools MCP patterns.
2. **`pageState()` is the cheapest page check** (~150 tokens). Use `pageSummary()` (~500) for more detail. Use MUI finders (`findCard`, `findButton`, etc.) for element interaction (~50 each). Use `take_snapshot` (~30K) only when the decision tree says to.
3. **If pageSummary shows `loading: true`**, wait 2-3 seconds and retry. Don't take_snapshot while loading.
4. **If pageSummary shows `alerts`**, escalate to `take_snapshot` + `list_console_messages` for debugging.
5. **For before/after comparison**, use `diffState(before, after)` on two `pageState()` snapshots (~300 tokens) for quick structural diffs (headings, buttons, tabs, tables, dialogs, URL changes). Only escalate to full `take_snapshot` both (~60K tokens) when element-level DOM delta is required.
6. **For form field discovery** in complex dialogs, try `__fcTest._scanDialogFields(dialogEl)` first (~400 tokens). Only use `take_snapshot` if custom FieldRegistry components have non-standard DOM.
7. **For custom SDK components**, use `verifyComponent()` to check existence (~100 tokens). Use `scanComponents()` for a full inventory (~400 tokens). Only `take_snapshot` once for initial DOM discovery of fully custom components — then cache selectors for reuse.
8. **Never snapshot twice for the same component.** After the first `take_snapshot`, extract selectors and use `find()` for all subsequent interactions with that component.

---

## Token Budget Guide

| Operation | Tokens | When to Use |
|---|---|---|
| Inject helper via `initScript` | ~0 (embedded) | Every `navigate_page` call |
| **`isLoggedIn()`** | **~100** | **Detect login vs app state — session reuse check** |
| **`loginForm(user, pass)`** | **~100** | **Programmatic login — replaces 4 MCP calls** |
| **`pageState()`** | **~150** | **Quick check — headings, buttons, tabs, table/dialog counts. Cheapest page read.** |
| `pageSummary()` | ~500 | Detailed page check with title, loading state, component inventory |
| `scanComponents()` | ~400 | Custom component pages, dialog fields |
| `verifyComponent(alias)` | ~100 | Check if specific component rendered |
| **`findCard/findButton/findTab/findNav/findInput`** | **~50 each** | **Find specific MUI element by label — fastest element lookup** |
| **`findTableColumn/findTableRow`** | **~50 each** | **Table element lookup by header text or cell value** |
| **`clickButton/clickTab/clickNav/clickTableRow`** | **~50 each** | **Find + click in one call** |
| **`batch([ops])`** | **~100 for 2-6 ops** | **Multiple operations in one evaluate_script call** |
| `__fcTest.find()` | ~100 | Generic element finder (when type unknown) |
| `__fcTest.highlight()` | ~100 | Click telemetry |
| `__fcTest.getActionButtons()` | ~200 | Verify workflow buttons |
| `__fcTest.annotate()` | ~100 | Step labels for screenshots |
| `__fcTest.getTimeline()` | ~200 | Action timeline for reports |
| **`diffState(before, after)`** | **~200** | **Compare two `pageState()` snapshots — returns only changes (added/removed/changed)** |
| **`inject('card'/'column'/'tab')`** | **~100 each** | **DOM injection preview — clones MUI classes from existing elements** |
| **`clearInjected()`** | **~50** | **Remove all injected preview elements** |
| `take_snapshot` (FALLBACK) | ~30K | Only when lightweight methods insufficient |
| `take_screenshot` | ~1.5K | Evidence capture |

### Recommended call sequence per page type

| Page Type | Calls | Total Tokens |
|---|---|---|
| **Quick page check** (any page) | pageState() → done | **~150** |
| **OOTB list page** (fc.list) | pageState() → findTableColumn() to verify columns | **~200** |
| **OOTB detail page** (fc.page + fc.tabs.card) | pageState() → findCard() to verify cards | **~200** |
| **Navigate + verify** | clickNav("Orders") → pageState() | **~200** |
| **Tab interaction** | clickTab("Attributes") → pageState() | **~200** |
| **Multiple actions** | batch([clickTab, pageState]) | **~100** |
| **Custom component page** (first visit) | pageSummary() → scanComponents() → verifyComponent() | ~1K |
| **Custom component page** (known structure) | pageState() → verifyComponent() | ~250 |
| **User action form** (standard fields) | findInput("Reason") per field | **~50/field** |
| **User action form** (custom FieldRegistry) | scanComponents() → __fcTest._scanDialogFields() | ~800 |
| **User action form** (unknown custom fields) | take_snapshot once → build selectors | ~30K first, ~50 after |
| **DOM preview** (inject card/column/tab) | inject('card', {...}) → annotate() → take_screenshot | **~250** |
| **DOM preview** (batch multiple) | batch([inject card, inject column, inject tab, annotate]) | **~200** |
| **Before/after comparison** (quick) | pageState() before + pageState() after + diffState() | **~300** |
| **Before/after comparison** (precise) | take_snapshot both + DOM diff | ~60K |
| **Debug/failure investigation** | take_snapshot + list_console_messages + list_network_requests | ~35K |

---

## Layer 5: DOM Injection Preview (`inject()`)

Preview manifest changes in the live browser **before deploying**. Injected elements look identical to Mystique-rendered components because they clone MUI classes from existing same-type elements on the page. Only the outer wrapper gets a green glow border + "NEW" badge — content inside is native MUI.

### How it works

1. **Clone** — Finds an existing element of the same type (card, th, tab, chip) and copies its `className`
2. **Build** — Creates the DOM structure matching MUI's hierarchy (Paper → CardContent → Typography for cards, th/td for columns, button[role=tab] for tabs)
3. **Insert** — Places after the specified anchor element (by heading text match)
4. **Glow** — Wraps the outer container with `.__fc-injected` class (green pulsing glow + "NEW" badge)

### Supported types

| Type | What it creates | Clones from | opts |
|---|---|---|---|
| `card` | MUI Paper/Card with title + attribute grid | `.MuiPaper-root.MuiCard-root` | `title`, `after?`, `attributes: [{label,value}]`, `content?` |
| `column` | Table header + data cells | Existing `th`/`td` | `header`, `after?`, `values: string[] \| fn(row,i)` |
| `tab` | MUI Tab button | `[role="tab"]` | `label`, `after?` |
| `row` | Full table row with matching columns | Existing `tr`/`td` | `values: {colHeader: value}`, `after?: rowIndex` |
| `chip` | MUI Chip | `.MuiChip-root` | `text`, `color?`, `target?` |
| `section` | MUI Paper/Accordion section | `.MuiPaper-root` or `[class*="Accordion"]` | `title`, `after?`, `content?` |

### Examples

#### Add a card to an order detail page

```javascript
__fcTest.inject('card', {
  title: 'Order Channel',
  after: 'SUMMARY',
  attributes: [
    { label: 'Channel', value: 'WEB' },
    { label: 'Source', value: 'Online Store' },
    { label: 'Device', value: 'Desktop' }
  ]
})
// → { success: true, type: 'card', title: 'Order Channel' }
```

#### Add a column to an order list

```javascript
__fcTest.inject('column', {
  header: 'Order Channel',
  after: 'Status',
  values: ['WEB', 'MOBILE', 'POS', 'WEB', 'CALL_CENTER']
})
// → { success: true, type: 'column', header: 'Order Channel', rowsUpdated: 100 }
```

#### Add a column with dynamic values

```javascript
__fcTest.inject('column', {
  header: 'Row #',
  values: function(row, i) { return '#' + (i + 1); }
})
```

#### Add a tab

```javascript
__fcTest.inject('tab', {
  label: 'Appeasements',
  after: 'Returns'
})
// → { success: true, type: 'tab', label: 'Appeasements' }
```

#### Batch multiple injections (one round trip)

```javascript
__fcTest.batch([
  { action: 'inject', args: ['card', { title: 'Loyalty', after: 'CUSTOMER', attributes: [{ label: 'Points', value: '2,450' }] }] },
  { action: 'inject', args: ['column', { header: 'Priority', after: 'Status', values: ['HIGH', 'LOW', 'MED'] }] },
  { action: 'inject', args: ['tab', { label: 'Loyalty', after: 'Attributes' }] },
  { action: 'annotate', args: ['Preview: 3 new elements injected', 'PREVIEW'] }
])
```

#### Clear all previews

```javascript
__fcTest.clearInjected()
// → { cleared: true }
```

### Visual appearance

Injected elements look identical to existing Mystique components — same fonts, spacing, borders, shadows. The only visual markers:

- **Green pulsing glow** border (2px solid #4caf50 with animation)
- **"NEW" badge** (green pill, top-right corner)

### Why clone MUI classes?

Mystique uses Material-UI. Class names like `MuiPaper-root MuiPaper-elevation1 MuiCard-root` carry all styling. By cloning from existing elements:

1. **Zero custom CSS** — inherits exact same styles the framework applies
2. **Theme-consistent** — custom MUI themes (colors, fonts) are reflected
3. **Responsive** — MUI grid/spacing classes adapt automatically
4. **Accurate** — preview matches what Mystique will render after deployment

### Integration with `/fluent-mystique-preview`

| Mode | inject() usage | Screenshot |
|---|---|---|
| **ADD** (new component) | `inject('card', {...})`, `inject('column', {...})` | `features/<slug>/assets/preview-add-<name>.png` |
| **MODIFY** (change existing) | Modify existing DOM + `highlight()` | `features/<slug>/assets/preview-modify-<name>.png` |
| **REMOVE** (delete component) | `el.remove()` + `annotate('Removed: ...')` | `features/<slug>/assets/preview-remove-<name>.png` |
