// Figma Desktop Bridge - MCP Plugin
// Bridges the Figma Plugin API to MCP clients via the plugin's UI iframe.
// Supports: Variables, Components, Styles, and more.
// Uses postMessage to communicate with ui.html (bypassing worker sandbox limitations),
// which then forwards messages to the MCP server over the WebSocket bridge.

// Plugin version — sent in FILE_INFO for server-side version compatibility checks.
// The server compares this against its own version to detect stale cached plugins.
var PLUGIN_VERSION = '1.28.1'; // Kept in sync with package.json by scripts/release.sh — see issue #62.

console.log('🌉 [Desktop Bridge] Plugin loaded (v' + PLUGIN_VERSION + ')');

// Show minimal UI - compact status indicator
figma.showUI(__html__, { width: 180, height: 50, visible: true, themeColors: true });

// ============================================================================
// CONSOLE CAPTURE — Intercept console.* in the QuickJS sandbox and forward
// to ui.html via postMessage so the WebSocket bridge can relay them to the MCP
// server. This is the only console-capture path in local mode (no browser
// process is involved).
// ============================================================================
(function() {
  var levels = ['log', 'info', 'warn', 'error', 'debug'];
  var originals = {};
  for (var i = 0; i < levels.length; i++) {
    originals[levels[i]] = console[levels[i]];
  }

  function safeSerialize(val) {
    if (val === null || val === undefined) return val;
    if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') return val;
    try {
      // Attempt JSON round-trip for objects/arrays (catches circular refs)
      return JSON.parse(JSON.stringify(val));
    } catch (e) {
      return String(val);
    }
  }

  for (var i = 0; i < levels.length; i++) {
    (function(level) {
      console[level] = function() {
        // Call the original so output still appears in Figma DevTools
        originals[level].apply(console, arguments);

        // Serialize arguments safely
        var args = [];
        for (var j = 0; j < arguments.length; j++) {
          args.push(safeSerialize(arguments[j]));
        }

        // Build message text from all arguments
        var messageParts = [];
        for (var j = 0; j < arguments.length; j++) {
          messageParts.push(typeof arguments[j] === 'string' ? arguments[j] : String(arguments[j]));
        }

        figma.ui.postMessage({
          type: 'CONSOLE_CAPTURE',
          level: level,
          message: messageParts.join(' '),
          args: args,
          timestamp: Date.now()
        });
      };
    })(levels[i]);
  }
})();

// Detect editor type (figma | figjam | slides | dev)
var __editorType = figma.editorType || 'figma';
console.log('🌉 [Desktop Bridge] Editor type:', __editorType);

// Shared sticky color map — used by CREATE_STICKY and CREATE_STICKIES
var __stickyColors = {
  'YELLOW': { r: 1, g: 0.85, b: 0.4 },
  'BLUE': { r: 0.53, g: 0.78, b: 1 },
  'GREEN': { r: 0.55, g: 0.87, b: 0.53 },
  'PINK': { r: 1, g: 0.6, b: 0.78 },
  'ORANGE': { r: 1, g: 0.71, b: 0.42 },
  'PURPLE': { r: 0.78, g: 0.65, b: 1 },
  'RED': { r: 1, g: 0.55, b: 0.55 },
  'LIGHT_GRAY': { r: 0.9, g: 0.9, b: 0.9 },
  'GRAY': { r: 0.7, g: 0.7, b: 0.7 }
};

// Immediately fetch and send variables data to UI (skip in FigJam — no variables API)
(async () => {
  if (__editorType === 'figjam' || __editorType === 'slides') {
    console.log('🌉 [Desktop Bridge] ' + __editorType + ' mode — skipping variables fetch');
    figma.ui.postMessage({
      type: 'VARIABLES_DATA',
      data: { success: true, timestamp: Date.now(), fileKey: figma.fileKey || null, variables: [], variableCollections: [], editorType: __editorType }
    });
    return;
  }
  try {
    console.log('🌉 [Desktop Bridge] Fetching variables...');

    // Get all local variables and collections
    const variables = await figma.variables.getLocalVariablesAsync();
    const collections = await figma.variables.getLocalVariableCollectionsAsync();

    console.log(`🌉 [Desktop Bridge] Found ${variables.length} variables in ${collections.length} collections`);

    // Format the data
    const variablesData = {
      success: true,
      timestamp: Date.now(),
      fileKey: figma.fileKey || null,
      variables: variables.map(v => ({
        id: v.id,
        name: v.name,
        key: v.key,
        resolvedType: v.resolvedType,
        valuesByMode: v.valuesByMode,
        variableCollectionId: v.variableCollectionId,
        scopes: v.scopes,
        codeSyntax: v.codeSyntax || {},
        description: v.description,
        hiddenFromPublishing: v.hiddenFromPublishing
      })),
      variableCollections: collections.map(c => ({
        id: c.id,
        name: c.name,
        key: c.key,
        modes: c.modes,
        defaultModeId: c.defaultModeId,
        variableIds: c.variableIds
      }))
    };

    // Send to UI via postMessage
    figma.ui.postMessage({
      type: 'VARIABLES_DATA',
      data: variablesData
    });

    console.log('🌉 [Desktop Bridge] Variables data sent to UI successfully');
    console.log('🌉 [Desktop Bridge] UI iframe now has variables data accessible via window.__figmaVariablesData');

  } catch (error) {
    console.error('🌉 [Desktop Bridge] Error fetching variables:', error);
    figma.ui.postMessage({
      type: 'ERROR',
      error: error.message || String(error)
    });
  }
})();

// Helper function to serialize a variable for response
function serializeVariable(v) {
  return {
    id: v.id,
    name: v.name,
    key: v.key,
    resolvedType: v.resolvedType,
    valuesByMode: v.valuesByMode,
    variableCollectionId: v.variableCollectionId,
    scopes: v.scopes,
    codeSyntax: v.codeSyntax || {},
    description: v.description,
    hiddenFromPublishing: v.hiddenFromPublishing
  };
}

// Helper function to serialize a collection for response
function serializeCollection(c) {
  return {
    id: c.id,
    name: c.name,
    key: c.key,
    modes: c.modes,
    defaultModeId: c.defaultModeId,
    variableIds: c.variableIds
  };
}

// Helper to convert hex color to Figma RGB (0-1 range)
function hexToFigmaRGB(hex) {
  // Remove # if present
  hex = hex.replace(/^#/, '');

  // Validate hex characters BEFORE parsing (prevents NaN values)
  if (!/^[0-9A-Fa-f]+$/.test(hex)) {
    throw new Error('Invalid hex color: "' + hex + '" contains non-hex characters. Use only 0-9 and A-F.');
  }

  // Parse hex values
  var r, g, b, a = 1;

  if (hex.length === 3) {
    // #RGB format
    r = parseInt(hex[0] + hex[0], 16) / 255;
    g = parseInt(hex[1] + hex[1], 16) / 255;
    b = parseInt(hex[2] + hex[2], 16) / 255;
  } else if (hex.length === 4) {
    // #RGBA format (CSS4 shorthand)
    r = parseInt(hex[0] + hex[0], 16) / 255;
    g = parseInt(hex[1] + hex[1], 16) / 255;
    b = parseInt(hex[2] + hex[2], 16) / 255;
    a = parseInt(hex[3] + hex[3], 16) / 255;
  } else if (hex.length === 6) {
    // #RRGGBB format
    r = parseInt(hex.substring(0, 2), 16) / 255;
    g = parseInt(hex.substring(2, 4), 16) / 255;
    b = parseInt(hex.substring(4, 6), 16) / 255;
  } else if (hex.length === 8) {
    // #RRGGBBAA format
    r = parseInt(hex.substring(0, 2), 16) / 255;
    g = parseInt(hex.substring(2, 4), 16) / 255;
    b = parseInt(hex.substring(4, 6), 16) / 255;
    a = parseInt(hex.substring(6, 8), 16) / 255;
  } else {
    throw new Error('Invalid hex color format: "' + hex + '". Expected 3, 4, 6, or 8 hex characters (e.g., #RGB, #RGBA, #RRGGBB, #RRGGBBAA).');
  }

  return { r: r, g: g, b: b, a: a };
}

// Listen for requests from UI (e.g., component data requests, write operations)
figma.ui.onmessage = async (msg) => {

  // ============================================================================
  // EXECUTE_CODE - Arbitrary code execution (Power Tool)
  // ============================================================================
  if (msg.type === 'EXECUTE_CODE') {
    try {
      console.log('🌉 [Desktop Bridge] Executing code, length:', msg.code.length);

      // Use eval with async IIFE wrapper instead of AsyncFunction constructor
      // AsyncFunction is restricted in Figma's plugin sandbox, but eval works
      // See: https://developers.figma.com/docs/plugins/resource-links

      // Wrap user code in an async IIFE that returns a Promise
      // This allows async/await in user code while using eval
      var wrappedCode = "(async function() {\n" + msg.code + "\n})()";

      console.log('🌉 [Desktop Bridge] Wrapped code for eval');

      // Execute with timeout
      var timeoutMs = msg.timeout || 5000;
      var timeoutPromise = new Promise(function(_, reject) {
        setTimeout(function() {
          reject(new Error('Execution timed out after ' + timeoutMs + 'ms'));
        }, timeoutMs);
      });

      var codePromise;
      try {
        // eval returns the Promise from the async IIFE
        codePromise = eval(wrappedCode);
      } catch (syntaxError) {
        // Log the actual syntax error message
        var syntaxErrorMsg = syntaxError && syntaxError.message ? syntaxError.message : String(syntaxError);
        console.error('🌉 [Desktop Bridge] Syntax error in code:', syntaxErrorMsg);
        figma.ui.postMessage({
          type: 'EXECUTE_CODE_RESULT',
          requestId: msg.requestId,
          success: false,
          error: 'Syntax error: ' + syntaxErrorMsg
        });
        return;
      }

      var result = await Promise.race([
        codePromise,
        timeoutPromise
      ]);

      console.log('🌉 [Desktop Bridge] Code executed successfully, result type:', typeof result);

      // Analyze result for potential silent failures
      var resultAnalysis = {
        type: typeof result,
        isNull: result === null,
        isUndefined: result === undefined,
        isEmpty: false,
        warning: null
      };

      // Check for empty results that might indicate a failed search/operation
      if (Array.isArray(result)) {
        resultAnalysis.isEmpty = result.length === 0;
        if (resultAnalysis.isEmpty) {
          resultAnalysis.warning = 'Code returned an empty array. If you were searching for nodes, none were found.';
        }
      } else if (result !== null && typeof result === 'object') {
        var keys = Object.keys(result);
        resultAnalysis.isEmpty = keys.length === 0;
        if (resultAnalysis.isEmpty) {
          resultAnalysis.warning = 'Code returned an empty object. The operation may not have found what it was looking for.';
        }
        // Check for common "found nothing" patterns
        if (result.length === 0 || result.count === 0 || result.foundCount === 0 || (result.nodes && result.nodes.length === 0)) {
          resultAnalysis.warning = 'Code returned a result indicating nothing was found (count/length is 0).';
        }
      } else if (result === null) {
        resultAnalysis.warning = 'Code returned null. The requested node or resource may not exist.';
      } else if (result === undefined) {
        resultAnalysis.warning = 'Code returned undefined. Make sure your code has a return statement.';
      }

      if (resultAnalysis.warning) {
        console.warn('🌉 [Desktop Bridge] ⚠️ Result warning:', resultAnalysis.warning);
      }

      figma.ui.postMessage({
        type: 'EXECUTE_CODE_RESULT',
        requestId: msg.requestId,
        success: true,
        result: result,
        resultAnalysis: resultAnalysis,
        // Include file context so users know which file this executed against
        fileContext: {
          fileName: figma.root.name,
          fileKey: figma.fileKey || null
        }
      });

    } catch (error) {
      // Extract error message explicitly - don't rely on console.error serialization
      var errorName = error && error.name ? error.name : 'Error';
      var errorMsg = error && error.message ? error.message : String(error);
      var errorStack = error && error.stack ? error.stack : '';

      // Log error details as strings so they survive intact through the WebSocket bridge into figma_get_console_logs
      console.error('🌉 [Desktop Bridge] Code execution error: [' + errorName + '] ' + errorMsg);
      if (errorStack) {
        console.error('🌉 [Desktop Bridge] Stack:', errorStack);
      }

      figma.ui.postMessage({
        type: 'EXECUTE_CODE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorName + ': ' + errorMsg
      });
    }
  }

  // ============================================================================
  // UPDATE_VARIABLE - Update a variable's value in a specific mode
  // ============================================================================
  else if (msg.type === 'UPDATE_VARIABLE') {
    try {
      console.log('🌉 [Desktop Bridge] Updating variable:', msg.variableId);

      var variable = await figma.variables.getVariableByIdAsync(msg.variableId);
      if (!variable) {
        throw new Error('Variable not found: ' + msg.variableId);
      }

      // Convert value based on variable type
      var value = msg.value;

      // Check if value is a variable alias (string starting with "VariableID:")
      if (typeof value === 'string' && value.startsWith('VariableID:')) {
        // Convert to VARIABLE_ALIAS format
        value = {
          type: 'VARIABLE_ALIAS',
          id: value
        };
        console.log('🌉 [Desktop Bridge] Converting to variable alias:', value.id);
      } else if (variable.resolvedType === 'COLOR' && typeof value === 'string') {
        // Convert hex string to Figma color
        value = hexToFigmaRGB(value);
      }

      // Set the value for the specified mode
      variable.setValueForMode(msg.modeId, value);

      console.log('🌉 [Desktop Bridge] Variable updated successfully');

      figma.ui.postMessage({
        type: 'UPDATE_VARIABLE_RESULT',
        requestId: msg.requestId,
        success: true,
        variable: serializeVariable(variable)
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Update variable error:', error);
      figma.ui.postMessage({
        type: 'UPDATE_VARIABLE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // ============================================================================
  // CREATE_VARIABLE - Create a new variable in a collection
  // ============================================================================
  else if (msg.type === 'CREATE_VARIABLE') {
    try {
      console.log('🌉 [Desktop Bridge] Creating variable:', msg.name);

      // Get the collection
      var collection = await figma.variables.getVariableCollectionByIdAsync(msg.collectionId);
      if (!collection) {
        throw new Error('Collection not found: ' + msg.collectionId);
      }

      // Create the variable
      var variable = figma.variables.createVariable(msg.name, collection, msg.resolvedType);

      // Set initial values if provided
      if (msg.valuesByMode) {
        for (var modeId in msg.valuesByMode) {
          var value = msg.valuesByMode[modeId];
          // Convert hex colors
          if (msg.resolvedType === 'COLOR' && typeof value === 'string') {
            value = hexToFigmaRGB(value);
          }
          variable.setValueForMode(modeId, value);
        }
      }

      // Set description if provided
      if (msg.description) {
        variable.description = msg.description;
      }

      // Set scopes if provided
      if (msg.scopes) {
        variable.scopes = msg.scopes;
      }

      console.log('🌉 [Desktop Bridge] Variable created:', variable.id);

      figma.ui.postMessage({
        type: 'CREATE_VARIABLE_RESULT',
        requestId: msg.requestId,
        success: true,
        variable: serializeVariable(variable)
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Create variable error:', error);
      figma.ui.postMessage({
        type: 'CREATE_VARIABLE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // ============================================================================
  // CREATE_VARIABLE_COLLECTION - Create a new variable collection
  // ============================================================================
  else if (msg.type === 'CREATE_VARIABLE_COLLECTION') {
    try {
      console.log('🌉 [Desktop Bridge] Creating collection:', msg.name);

      // Create the collection
      var collection = figma.variables.createVariableCollection(msg.name);

      // Rename the default mode if a name is provided
      if (msg.initialModeName && collection.modes.length > 0) {
        collection.renameMode(collection.modes[0].modeId, msg.initialModeName);
      }

      // Add additional modes if provided
      if (msg.additionalModes && msg.additionalModes.length > 0) {
        for (var i = 0; i < msg.additionalModes.length; i++) {
          collection.addMode(msg.additionalModes[i]);
        }
      }

      console.log('🌉 [Desktop Bridge] Collection created:', collection.id);

      figma.ui.postMessage({
        type: 'CREATE_VARIABLE_COLLECTION_RESULT',
        requestId: msg.requestId,
        success: true,
        collection: serializeCollection(collection)
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Create collection error:', error);
      figma.ui.postMessage({
        type: 'CREATE_VARIABLE_COLLECTION_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // ============================================================================
  // DELETE_VARIABLE - Delete a variable
  // ============================================================================
  else if (msg.type === 'DELETE_VARIABLE') {
    try {
      console.log('🌉 [Desktop Bridge] Deleting variable:', msg.variableId);

      var variable = await figma.variables.getVariableByIdAsync(msg.variableId);
      if (!variable) {
        throw new Error('Variable not found: ' + msg.variableId);
      }

      var deletedInfo = {
        id: variable.id,
        name: variable.name
      };

      variable.remove();

      console.log('🌉 [Desktop Bridge] Variable deleted');

      figma.ui.postMessage({
        type: 'DELETE_VARIABLE_RESULT',
        requestId: msg.requestId,
        success: true,
        deleted: deletedInfo
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Delete variable error:', error);
      figma.ui.postMessage({
        type: 'DELETE_VARIABLE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // ============================================================================
  // DELETE_VARIABLE_COLLECTION - Delete a variable collection
  // ============================================================================
  else if (msg.type === 'DELETE_VARIABLE_COLLECTION') {
    try {
      console.log('🌉 [Desktop Bridge] Deleting collection:', msg.collectionId);

      var collection = await figma.variables.getVariableCollectionByIdAsync(msg.collectionId);
      if (!collection) {
        throw new Error('Collection not found: ' + msg.collectionId);
      }

      var deletedInfo = {
        id: collection.id,
        name: collection.name,
        variableCount: collection.variableIds.length
      };

      collection.remove();

      console.log('🌉 [Desktop Bridge] Collection deleted');

      figma.ui.postMessage({
        type: 'DELETE_VARIABLE_COLLECTION_RESULT',
        requestId: msg.requestId,
        success: true,
        deleted: deletedInfo
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Delete collection error:', error);
      figma.ui.postMessage({
        type: 'DELETE_VARIABLE_COLLECTION_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // ============================================================================
  // RENAME_VARIABLE - Rename a variable
  // ============================================================================
  else if (msg.type === 'RENAME_VARIABLE') {
    try {
      console.log('🌉 [Desktop Bridge] Renaming variable:', msg.variableId, 'to', msg.newName);

      var variable = await figma.variables.getVariableByIdAsync(msg.variableId);
      if (!variable) {
        throw new Error('Variable not found: ' + msg.variableId);
      }

      var oldName = variable.name;
      variable.name = msg.newName;

      console.log('🌉 [Desktop Bridge] Variable renamed from "' + oldName + '" to "' + msg.newName + '"');

      var serializedVar = serializeVariable(variable);
      serializedVar.oldName = oldName;
      figma.ui.postMessage({
        type: 'RENAME_VARIABLE_RESULT',
        requestId: msg.requestId,
        success: true,
        variable: serializedVar,
        oldName: oldName
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Rename variable error:', error);
      figma.ui.postMessage({
        type: 'RENAME_VARIABLE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // ============================================================================
  // SET_VARIABLE_DESCRIPTION - Set description on a variable
  // ============================================================================
  else if (msg.type === 'SET_VARIABLE_DESCRIPTION') {
    try {
      console.log('🌉 [Desktop Bridge] Setting description on variable:', msg.variableId);

      var variable = await figma.variables.getVariableByIdAsync(msg.variableId);
      if (!variable) {
        throw new Error('Variable not found: ' + msg.variableId);
      }

      variable.description = msg.description || '';

      console.log('🌉 [Desktop Bridge] Variable description set successfully');

      figma.ui.postMessage({
        type: 'SET_VARIABLE_DESCRIPTION_RESULT',
        requestId: msg.requestId,
        success: true,
        variable: serializeVariable(variable)
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Set variable description error:', errorMsg);
      figma.ui.postMessage({
        type: 'SET_VARIABLE_DESCRIPTION_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // ADD_MODE - Add a mode to a variable collection
  // ============================================================================
  else if (msg.type === 'ADD_MODE') {
    try {
      console.log('🌉 [Desktop Bridge] Adding mode to collection:', msg.collectionId);

      var collection = await figma.variables.getVariableCollectionByIdAsync(msg.collectionId);
      if (!collection) {
        throw new Error('Collection not found: ' + msg.collectionId);
      }

      // Add the mode (returns the new mode ID)
      var newModeId = collection.addMode(msg.modeName);

      console.log('🌉 [Desktop Bridge] Mode "' + msg.modeName + '" added with ID:', newModeId);

      figma.ui.postMessage({
        type: 'ADD_MODE_RESULT',
        requestId: msg.requestId,
        success: true,
        collection: serializeCollection(collection),
        newMode: {
          modeId: newModeId,
          name: msg.modeName
        }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Add mode error:', error);
      figma.ui.postMessage({
        type: 'ADD_MODE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // ============================================================================
  // RENAME_MODE - Rename a mode in a variable collection
  // ============================================================================
  else if (msg.type === 'RENAME_MODE') {
    try {
      console.log('🌉 [Desktop Bridge] Renaming mode:', msg.modeId, 'in collection:', msg.collectionId);

      var collection = await figma.variables.getVariableCollectionByIdAsync(msg.collectionId);
      if (!collection) {
        throw new Error('Collection not found: ' + msg.collectionId);
      }

      // Find the current mode name
      var currentMode = collection.modes.find(function(m) { return m.modeId === msg.modeId; });
      if (!currentMode) {
        throw new Error('Mode not found: ' + msg.modeId);
      }

      var oldName = currentMode.name;
      collection.renameMode(msg.modeId, msg.newName);

      console.log('🌉 [Desktop Bridge] Mode renamed from "' + oldName + '" to "' + msg.newName + '"');

      var serializedCol = serializeCollection(collection);
      serializedCol.oldName = oldName;
      figma.ui.postMessage({
        type: 'RENAME_MODE_RESULT',
        requestId: msg.requestId,
        success: true,
        collection: serializedCol,
        oldName: oldName
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Rename mode error:', error);
      figma.ui.postMessage({
        type: 'RENAME_MODE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // ============================================================================
  // REFRESH_VARIABLES - Re-fetch and send all variables data
  // ============================================================================
  else if (msg.type === 'REFRESH_VARIABLES') {
    try {
      console.log('🌉 [Desktop Bridge] Refreshing variables data...');

      var variables = await figma.variables.getLocalVariablesAsync();
      var collections = await figma.variables.getLocalVariableCollectionsAsync();

      var variablesData = {
        success: true,
        timestamp: Date.now(),
        fileKey: figma.fileKey || null,
        variables: variables.map(serializeVariable),
        variableCollections: collections.map(serializeCollection)
      };

      // Update the UI's cached data
      figma.ui.postMessage({
        type: 'VARIABLES_DATA',
        data: variablesData
      });

      // Also send as a response to the request
      figma.ui.postMessage({
        type: 'REFRESH_VARIABLES_RESULT',
        requestId: msg.requestId,
        success: true,
        data: variablesData
      });

      console.log('🌉 [Desktop Bridge] Variables refreshed:', variables.length, 'variables in', collections.length, 'collections');

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Refresh variables error:', error);
      figma.ui.postMessage({
        type: 'REFRESH_VARIABLES_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // ============================================================================
  // GET_COMPONENT - Existing read operation
  // ============================================================================
  else if (msg.type === 'GET_COMPONENT') {
    try {
      console.log(`🌉 [Desktop Bridge] Fetching component: ${msg.nodeId}`);

      const node = await figma.getNodeByIdAsync(msg.nodeId);

      if (!node) {
        throw new Error(`Node not found: ${msg.nodeId}`);
      }

      if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET' && node.type !== 'INSTANCE') {
        throw new Error(`Node is not a component. Type: ${node.type}`);
      }

      // Detect if this is a variant (COMPONENT inside a COMPONENT_SET)
      // Note: Can't use optional chaining (?.) - Figma plugin sandbox doesn't support it
      const isVariant = node.type === 'COMPONENT' && node.parent && node.parent.type === 'COMPONENT_SET';

      // Extract component data including description fields and annotations
      const componentData = {
        success: true,
        timestamp: Date.now(),
        nodeId: msg.nodeId,
        component: {
          id: node.id,
          name: node.name,
          type: node.type,
          // Variants CAN have their own description
          description: node.description || null,
          descriptionMarkdown: node.descriptionMarkdown || null,
          visible: node.visible,
          locked: node.locked,
          // Dev Mode annotations
          annotations: node.annotations || [],
          // Flag to indicate if this is a variant
          isVariant: isVariant,
          // For component sets and non-variant components only (variants cannot access this)
          componentPropertyDefinitions: (node.type === 'COMPONENT_SET' || (node.type === 'COMPONENT' && !isVariant))
            ? node.componentPropertyDefinitions
            : undefined,
          // Get children info (lightweight) — skip unresolvable slot sublayers
          children: node.children ? node.children.reduce((acc, child) => {
            try {
              acc.push({ id: child.id, name: child.name, type: child.type });
            } catch (e) { /* slot sublayer or table cell — skip */ }
            return acc;
          }, []) : undefined
        }
      };

      console.log(`🌉 [Desktop Bridge] Component data ready. Has description: ${!!componentData.component.description}, annotations: ${componentData.component.annotations.length}`);

      // Send to UI
      figma.ui.postMessage({
        type: 'COMPONENT_DATA',
        requestId: msg.requestId, // Echo back the request ID
        data: componentData
      });

    } catch (error) {
      console.error(`🌉 [Desktop Bridge] Error fetching component:`, error);
      figma.ui.postMessage({
        type: 'COMPONENT_ERROR',
        requestId: msg.requestId,
        error: error.message || String(error)
      });
    }
  }

  // ============================================================================
  // ANALYZE_COMPONENT_SET - Variant state machine + cross-variant diff
  // For COMPONENT_SET nodes: maps variant states to CSS pseudo-classes,
  // computes visual diffs between default and other variants, and extracts
  // the component API surface (props, slots, conditional elements).
  // ============================================================================
  else if (msg.type === 'ANALYZE_COMPONENT_SET') {
    try {
      console.log('🌉 [Desktop Bridge] Analyzing component set: ' + msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) throw new Error('Node not found: ' + msg.nodeId);
      if (node.type !== 'COMPONENT_SET') throw new Error('Node is not a COMPONENT_SET. Type: ' + node.type);

      // Build variable name lookup
      var varNameMap = {};
      try {
        var allVars = await figma.variables.getLocalVariablesAsync();
        var allColls = await figma.variables.getLocalVariableCollectionsAsync();
        var collMap = {};
        for (var ci = 0; ci < allColls.length; ci++) collMap[allColls[ci].id] = allColls[ci].name;
        for (var vi = 0; vi < allVars.length; vi++) {
          var v = allVars[vi];
          varNameMap[v.id] = { name: v.name, collection: collMap[v.variableCollectionId] || null, type: v.resolvedType };
        }
      } catch(e) {}

      function resolveVarId(id) {
        return varNameMap[id] ? varNameMap[id].name : id;
      }

      function resolveBoundColor(bv) {
        if (!bv) return null;
        // Handle array format (fills/strokes)
        if (Array.isArray(bv)) {
          return bv.length > 0 && bv[0].id ? resolveVarId(bv[0].id) : null;
        }
        // Handle object format
        if (bv.id) return resolveVarId(bv.id);
        if (bv.color && bv.color.id) return resolveVarId(bv.color.id);
        return null;
      }

      // Extract visual signature from a variant for diffing
      function extractSignature(variant) {
        var sig = {};

        // Find the main interactive element — prioritize by: has strokes (input fields),
        // is a FRAME with fills and strokes, or is the largest visible non-text child
        var mainChild = null;
        if (variant.children) {
          // First pass: find child with strokes (likely the input/interactive element)
          for (var i = 0; i < variant.children.length; i++) {
            var child = variant.children[i];
            try {
              if (child.visible !== false && child.type !== 'TEXT' && child.strokes && child.strokes.length > 0) {
                mainChild = child;
                break;
              }
            } catch(e) {}
          }
          // Fallback: first visible FRAME child
          if (!mainChild) {
            for (var i2 = 0; i2 < variant.children.length; i2++) {
              var c2 = variant.children[i2];
              try {
                if (c2.visible !== false && c2.type === 'FRAME') {
                  mainChild = c2;
                  break;
                }
              } catch(e) {}
            }
          }
        }

        if (mainChild) {
              var child = mainChild;
              // This is the main interactive frame
              try {
                var bv = child.boundVariables || {};
                sig.fillToken = resolveBoundColor(bv.fills);
                sig.strokeToken = resolveBoundColor(bv.strokes);
                sig.strokeWeight = child.strokeWeight;

                // Raw colors as fallback
                if (!sig.fillToken && child.fills && child.fills.length > 0 && child.fills[0].color) {
                  var fc = child.fills[0].color;
                  sig.fillHex = '#' + Math.round(fc.r*255).toString(16).padStart(2,'0') + Math.round(fc.g*255).toString(16).padStart(2,'0') + Math.round(fc.b*255).toString(16).padStart(2,'0');
                }
                if (!sig.strokeToken && child.strokes && child.strokes.length > 0 && child.strokes[0].color) {
                  var sc = child.strokes[0].color;
                  sig.strokeHex = '#' + Math.round(sc.r*255).toString(16).padStart(2,'0') + Math.round(sc.g*255).toString(16).padStart(2,'0') + Math.round(sc.b*255).toString(16).padStart(2,'0');
                }
                sig.effects = child.effects && child.effects.length > 0 ? child.effects : null;
                sig.opacity = child.opacity < 1 ? child.opacity : null;
              } catch(e) {}

              // Check text children for color changes
              if (child.children) {
                for (var t = 0; t < child.children.length; t++) {
                  var textChild = child.children[t];
                  if (textChild.type === 'TEXT') {
                    try {
                      var tbv = textChild.boundVariables || {};
                      sig.textToken = resolveBoundColor(tbv.fills);
                      if (!sig.textToken && textChild.fills && textChild.fills.length > 0 && textChild.fills[0].color) {
                        var tc = textChild.fills[0].color;
                        sig.textHex = '#' + Math.round(tc.r*255).toString(16).padStart(2,'0') + Math.round(tc.g*255).toString(16).padStart(2,'0') + Math.round(tc.b*255).toString(16).padStart(2,'0');
                      }
                    } catch(e) {}
                    break; // First text node is enough
                  }
                }
              }
        }

        // Check element visibility changes
        sig.visibilityChanges = {};
        if (variant.children) {
          for (var j = 0; j < variant.children.length; j++) {
            var ch = variant.children[j];
            try {
              if (!ch.visible) sig.visibilityChanges[ch.name] = false;
            } catch(e) {}
          }
        }

        return sig;
      }

      // Parse variant property definitions
      var propDefs = node.componentPropertyDefinitions || {};
      var variantAxes = {};
      var componentProps = {};
      var propKeys = Object.keys(propDefs);
      for (var pk = 0; pk < propKeys.length; pk++) {
        var propKey = propKeys[pk];
        var propDef = propDefs[propKey];
        if (propDef.type === 'VARIANT') {
          variantAxes[propKey] = propDef.variantOptions || [];
        } else {
          componentProps[propKey] = {
            type: propDef.type,
            defaultValue: propDef.defaultValue
          };
        }
      }

      // CSS pseudo-class mapping for state variants
      var stateMapping = {
        'default': null,
        'hover': ':hover',
        'focus': ':focus-visible',
        'focus-visible': ':focus-visible',
        'focused': ':focus-visible',
        'active': ':active',
        'pressed': ':active',
        'disabled': ':disabled, [aria-disabled="true"]',
        'error': '[aria-invalid="true"]',
        'invalid': '[aria-invalid="true"]',
        'filled': '.has-value',
        'selected': '[aria-selected="true"]',
        'checked': ':checked',
        'loading': '[aria-busy="true"]',
        'readonly': '[readonly]',
        'open': '[aria-expanded="true"]',
        'closed': '[aria-expanded="false"]'
      };

      // Analyze variants grouped by axes
      var variants = node.children || [];
      var defaultVariant = null;
      var stateAxis = null;
      var sizeAxis = null;

      // Detect which axis is "state" and which is "size"
      var axisKeys = Object.keys(variantAxes);
      for (var ak = 0; ak < axisKeys.length; ak++) {
        var axisName = axisKeys[ak].toLowerCase();
        var axisValues = variantAxes[axisKeys[ak]];
        if (axisName === 'state' || axisName === 'status' || axisName === 'interaction') {
          stateAxis = axisKeys[ak];
        } else if (axisName === 'size' || axisName === 'scale') {
          sizeAxis = axisKeys[ak];
        }
      }

      // Build state machine from variants
      var stateMachine = { states: {}, defaultState: null, cssMapping: {} };
      var defaultSig = null;

      // Find the default variant (first size, default state)
      for (var di = 0; di < variants.length; di++) {
        var vName = variants[di].name;
        if (vName.indexOf('state=default') !== -1 && (sizeAxis ? vName.indexOf(sizeAxis + '=') !== -1 : true)) {
          // Pick the first default state variant
          if (!defaultVariant || vName.indexOf('large') !== -1) {
            defaultVariant = variants[di];
          }
        }
      }
      if (defaultVariant) {
        defaultSig = extractSignature(defaultVariant);
      }

      // Process each variant and compute diff from default
      var variantDiffs = [];
      for (var vdi = 0; vdi < variants.length; vdi++) {
        var variant = variants[vdi];
        var sig = extractSignature(variant);

        // Parse variant name to extract axis values
        var axisParts = variant.name.split(', ');
        var axisValues = {};
        for (var ap = 0; ap < axisParts.length; ap++) {
          var parts = axisParts[ap].split('=');
          if (parts.length === 2) axisValues[parts[0].trim()] = parts[1].trim();
        }

        var stateValue = stateAxis ? (axisValues[stateAxis] || 'default') : 'default';
        var cssSelector = stateMapping[stateValue.toLowerCase()] || null;

        // Compute diff from default
        var diff = {};
        if (defaultSig && variant.id !== (defaultVariant ? defaultVariant.id : null)) {
          if (sig.fillToken !== defaultSig.fillToken) diff.fillToken = sig.fillToken || sig.fillHex;
          if (sig.strokeToken !== defaultSig.strokeToken) diff.strokeToken = sig.strokeToken || sig.strokeHex;
          if (sig.strokeWeight !== defaultSig.strokeWeight) diff.strokeWeight = sig.strokeWeight;
          if (sig.textToken !== defaultSig.textToken) diff.textToken = sig.textToken || sig.textHex;
          if (sig.opacity !== defaultSig.opacity) diff.opacity = sig.opacity;
          if (JSON.stringify(sig.effects) !== JSON.stringify(defaultSig.effects)) diff.effects = sig.effects;
          // Visibility changes not in default
          var dvKeys = Object.keys(defaultSig.visibilityChanges);
          var svKeys = Object.keys(sig.visibilityChanges);
          for (var sk = 0; sk < svKeys.length; sk++) {
            if (!defaultSig.visibilityChanges[svKeys[sk]]) {
              if (!diff.visibilityChanges) diff.visibilityChanges = {};
              diff.visibilityChanges[svKeys[sk]] = sig.visibilityChanges[svKeys[sk]];
            }
          }
        }

        variantDiffs.push({
          name: variant.name,
          id: variant.id,
          axes: axisValues,
          state: stateValue,
          cssSelector: cssSelector,
          diffFromDefault: Object.keys(diff).length > 0 ? diff : null,
          signature: sig
        });

        // Build CSS mapping
        if (cssSelector) {
          stateMachine.cssMapping[stateValue] = cssSelector;
        }
        if (!stateMachine.states[stateValue]) {
          stateMachine.states[stateValue] = [];
        }
        stateMachine.states[stateValue].push(variant.id);
      }

      if (defaultVariant) {
        stateMachine.defaultState = 'default';
        stateMachine.defaultSignature = defaultSig;
      }

      var result = {
        nodeId: node.id,
        nodeName: node.name,
        variantCount: variants.length,
        variantAxes: variantAxes,
        componentProps: componentProps,
        stateMachine: stateMachine,
        variants: variantDiffs,
        ai_instruction: 'Use cssMapping to implement interaction states. diffFromDefault shows only what changes per state — apply these as CSS pseudo-class or attribute overrides. componentProps maps to React/Vue component props (BOOLEAN → boolean prop, TEXT → string prop, INSTANCE_SWAP → ReactNode/slot prop).'
      };

      console.log('🌉 [Desktop Bridge] Component set analysis complete. ' + variants.length + ' variants, ' + Object.keys(stateMachine.cssMapping).length + ' CSS mappings');

      figma.ui.postMessage({
        type: 'ANALYZE_COMPONENT_SET_RESULT',
        requestId: msg.requestId,
        success: true,
        data: result
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Analyze component set error:', errorMsg);
      figma.ui.postMessage({
        type: 'ANALYZE_COMPONENT_SET_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // DEEP_GET_COMPONENT - Full recursive component extraction for code generation
  // Extracts visual properties, boundVariables, reactions, instance references,
  // and annotations at every level of the component tree.
  // ============================================================================
  else if (msg.type === 'DEEP_GET_COMPONENT') {
    try {
      var maxDepth = msg.depth || 10;
      console.log('🌉 [Desktop Bridge] Deep component fetch: ' + msg.nodeId + ' (depth: ' + maxDepth + ')');

      var rootNode = await figma.getNodeByIdAsync(msg.nodeId);
      if (!rootNode) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      // Build a variable name lookup map for resolving boundVariables
      var varNameMap = {};
      try {
        var allVars = await figma.variables.getLocalVariablesAsync();
        var allCollections = await figma.variables.getLocalVariableCollectionsAsync();
        var collectionMap = {};
        for (var ci = 0; ci < allCollections.length; ci++) {
          collectionMap[allCollections[ci].id] = allCollections[ci].name;
        }
        for (var vi = 0; vi < allVars.length; vi++) {
          var v = allVars[vi];
          varNameMap[v.id] = {
            name: v.name,
            resolvedType: v.resolvedType,
            collection: collectionMap[v.variableCollectionId] || null,
            scopes: v.scopes || [],
            codeSyntax: v.codeSyntax || {}
          };
        }
      } catch (e) {
        console.log('🌉 [Desktop Bridge] Could not build variable map: ' + e.message);
      }

      // Resolve boundVariables to token names
      function resolveBoundVars(bv) {
        if (!bv) return null;
        var resolved = {};
        var keys = Object.keys(bv);
        for (var k = 0; k < keys.length; k++) {
          var prop = keys[k];
          var binding = bv[prop];
          if (Array.isArray(binding)) {
            resolved[prop] = [];
            for (var bi = 0; bi < binding.length; bi++) {
              var b = binding[bi];
              if (b && b.id) {
                var info = varNameMap[b.id];
                resolved[prop].push(info ? { id: b.id, name: info.name, collection: info.collection, resolvedType: info.resolvedType, codeSyntax: info.codeSyntax } : { id: b.id });
              }
            }
          } else if (binding && binding.id) {
            var info = varNameMap[binding.id];
            resolved[prop] = info ? { id: binding.id, name: info.name, collection: info.collection, resolvedType: info.resolvedType, codeSyntax: info.codeSyntax } : { id: binding.id };
          }
        }
        return Object.keys(resolved).length > 0 ? resolved : null;
      }

      // Extract visual properties from a node
      function extractNodeProps(n) {
        var props = {};

        // Layout
        if (n.layoutMode) props.layoutMode = n.layoutMode;
        if (n.primaryAxisSizingMode) props.primaryAxisSizingMode = n.primaryAxisSizingMode;
        if (n.counterAxisSizingMode) props.counterAxisSizingMode = n.counterAxisSizingMode;
        if (n.layoutSizingHorizontal) props.layoutSizingHorizontal = n.layoutSizingHorizontal;
        if (n.layoutSizingVertical) props.layoutSizingVertical = n.layoutSizingVertical;
        if (n.primaryAxisAlignItems) props.primaryAxisAlignItems = n.primaryAxisAlignItems;
        if (n.counterAxisAlignItems) props.counterAxisAlignItems = n.counterAxisAlignItems;
        if (n.paddingLeft !== undefined && n.paddingLeft !== 0) props.paddingLeft = n.paddingLeft;
        if (n.paddingRight !== undefined && n.paddingRight !== 0) props.paddingRight = n.paddingRight;
        if (n.paddingTop !== undefined && n.paddingTop !== 0) props.paddingTop = n.paddingTop;
        if (n.paddingBottom !== undefined && n.paddingBottom !== 0) props.paddingBottom = n.paddingBottom;
        if (n.itemSpacing !== undefined && n.itemSpacing !== 0) props.itemSpacing = n.itemSpacing;
        if (n.counterAxisSpacing !== undefined && n.counterAxisSpacing !== 0) props.counterAxisSpacing = n.counterAxisSpacing;
        if (n.layoutWrap && n.layoutWrap !== 'NO_WRAP') props.layoutWrap = n.layoutWrap;
        if (n.minWidth !== undefined) props.minWidth = n.minWidth;
        if (n.maxWidth !== undefined) props.maxWidth = n.maxWidth;
        if (n.minHeight !== undefined) props.minHeight = n.minHeight;
        if (n.maxHeight !== undefined) props.maxHeight = n.maxHeight;
        if (n.clipsContent) props.clipsContent = true;

        // Visual
        try {
          if (n.fills && n.fills !== figma.mixed && n.fills.length > 0) props.fills = n.fills;
        } catch (e) { /* mixed fills */ }
        try {
          if (n.strokes && n.strokes.length > 0) props.strokes = n.strokes;
        } catch (e) {}
        if (n.strokeWeight !== undefined && n.strokeWeight !== 0 && n.strokeWeight !== figma.mixed) props.strokeWeight = n.strokeWeight;
        if (n.cornerRadius !== undefined && n.cornerRadius !== 0 && n.cornerRadius !== figma.mixed) props.cornerRadius = n.cornerRadius;
        try {
          if (n.effects && n.effects.length > 0) props.effects = n.effects;
        } catch (e) {}
        if (n.opacity !== undefined && n.opacity < 1) props.opacity = n.opacity;

        // Typography
        if (n.type === 'TEXT') {
          try { props.characters = n.characters; } catch (e) {}
          try { if (n.fontSize !== figma.mixed) props.fontSize = n.fontSize; } catch (e) {}
          try { if (n.fontName !== figma.mixed) props.fontFamily = n.fontName.family; props.fontStyle = n.fontName.style; } catch (e) {}
          try { if (n.fontWeight !== figma.mixed) props.fontWeight = n.fontWeight; } catch (e) {}
          try { if (n.lineHeight !== figma.mixed) props.lineHeight = n.lineHeight; } catch (e) {}
          try { if (n.letterSpacing !== figma.mixed) props.letterSpacing = n.letterSpacing; } catch (e) {}
          try { if (n.textAlignHorizontal) props.textAlignHorizontal = n.textAlignHorizontal; } catch (e) {}
          try { if (n.textAlignVertical) props.textAlignVertical = n.textAlignVertical; } catch (e) {}
          try { if (n.textAutoResize && n.textAutoResize !== 'NONE') props.textAutoResize = n.textAutoResize; } catch (e) {}
          try { if (n.textTruncation && n.textTruncation !== 'DISABLED') props.textTruncation = n.textTruncation; } catch (e) {}
          try { if (n.textCase && n.textCase !== 'ORIGINAL') props.textCase = n.textCase; } catch (e) {}
          try { if (n.textDecoration && n.textDecoration !== 'NONE') props.textDecoration = n.textDecoration; } catch (e) {}
        }

        // Design tokens (resolved to names)
        try {
          var resolved = resolveBoundVars(n.boundVariables);
          if (resolved) props.boundVariables = resolved;
        } catch (e) {}

        // Prototype interactions
        try {
          if (n.reactions && n.reactions.length > 0) {
            props.reactions = n.reactions.map(function(r) {
              var reaction = { trigger: r.trigger };
              if (r.action) {
                reaction.action = { type: r.action.type };
                if (r.action.navigation) reaction.action.navigation = r.action.navigation;
                if (r.action.transition) reaction.action.transition = r.action.transition;
                if (r.action.destinationId) reaction.action.destinationId = r.action.destinationId;
              }
              return reaction;
            });
          }
        } catch (e) {}

        // Annotations
        try {
          if (n.annotations && n.annotations.length > 0) {
            props.annotations = n.annotations.map(function(a) {
              var ann = {};
              if (a.labelMarkdown) ann.labelMarkdown = a.labelMarkdown;
              else if (a.label) ann.label = a.label;
              if (a.properties) ann.properties = a.properties;
              if (a.categoryId) ann.categoryId = a.categoryId;
              return ann;
            });
          }
        } catch (e) {}

        // Component instance reference
        if (n.type === 'INSTANCE') {
          try {
            if (n.mainComponent) {
              props.mainComponent = {
                id: n.mainComponent.id,
                name: n.mainComponent.name,
                key: n.mainComponent.key || null,
                isVariant: n.mainComponent.parent && n.mainComponent.parent.type === 'COMPONENT_SET'
              };
              if (props.mainComponent.isVariant && n.mainComponent.parent) {
                props.mainComponent.componentSetName = n.mainComponent.parent.name;
                props.mainComponent.componentSetId = n.mainComponent.parent.id;
              }
            }
          } catch (e) {}
          try {
            if (n.componentProperties) props.componentProperties = n.componentProperties;
          } catch (e) {}
        }

        // Component definitions (for COMPONENT and COMPONENT_SET)
        if (n.type === 'COMPONENT_SET' || n.type === 'COMPONENT') {
          try {
            if (n.componentPropertyDefinitions) props.componentPropertyDefinitions = n.componentPropertyDefinitions;
          } catch (e) {}
          if (n.type === 'COMPONENT' && n.variantProperties) {
            props.variantProperties = n.variantProperties;
          }
        }

        // Dimensions
        try {
          props.width = Math.round(n.width);
          props.height = Math.round(n.height);
        } catch (e) {}

        return props;
      }

      // Recursive tree walker
      function walkNode(n, currentDepth) {
        var nodeData = {
          id: n.id,
          name: n.name,
          type: n.type,
          visible: n.visible
        };

        // Skip invisible nodes (unless they're component set variants)
        if (!n.visible && n.type !== 'COMPONENT') {
          nodeData._hidden = true;
          return nodeData;
        }

        // Extract all properties
        var props = extractNodeProps(n);
        var propKeys = Object.keys(props);
        for (var pk = 0; pk < propKeys.length; pk++) {
          nodeData[propKeys[pk]] = props[propKeys[pk]];
        }

        // Recurse into children
        if (n.children && currentDepth < maxDepth) {
          nodeData.children = [];
          for (var i = 0; i < n.children.length; i++) {
            try {
              nodeData.children.push(walkNode(n.children[i], currentDepth + 1));
            } catch (e) {
              // Skip inaccessible slot sublayers
            }
          }
        } else if (n.children) {
          // At max depth, include lightweight child summary
          nodeData.childCount = n.children.length;
          nodeData._depthLimitReached = true;
        }

        return nodeData;
      }

      var result = walkNode(rootNode, 0);
      result._variableMapSize = Object.keys(varNameMap).length;
      result._maxDepthUsed = maxDepth;

      var resultJson = JSON.stringify(result);
      console.log('🌉 [Desktop Bridge] Deep component data: ' + Math.round(resultJson.length / 1024) + 'KB, vars resolved: ' + Object.keys(varNameMap).length);

      figma.ui.postMessage({
        type: 'DEEP_GET_COMPONENT_RESULT',
        requestId: msg.requestId,
        success: true,
        data: result
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Deep component error:', errorMsg);
      figma.ui.postMessage({
        type: 'DEEP_GET_COMPONENT_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // GET_LOCAL_COMPONENTS - Get all local components for design system manifest
  // ============================================================================
  else if (msg.type === 'GET_LOCAL_COMPONENTS') {
    try {
      console.log('🌉 [Desktop Bridge] Fetching all local components for manifest...');

      // Find all component sets and standalone components in the file
      var components = [];
      var componentSets = [];

      // Helper to extract component data
      function extractComponentData(node, isPartOfSet) {
        var data = {
          key: node.key,
          nodeId: node.id,
          name: node.name,
          type: node.type,
          description: node.description || null,
          width: node.width,
          height: node.height
        };

        // Get property definitions for non-variant components
        if (!isPartOfSet && node.componentPropertyDefinitions) {
          data.properties = [];
          var propDefs = node.componentPropertyDefinitions;
          for (var propName in propDefs) {
            if (propDefs.hasOwnProperty(propName)) {
              var propDef = propDefs[propName];
              data.properties.push({
                name: propName,
                type: propDef.type,
                defaultValue: propDef.defaultValue
              });
            }
          }
        }

        return data;
      }

      // Helper to extract component set data with all variants
      function extractComponentSetData(node) {
        var variantAxes = {};
        var variants = [];

        // Parse variant properties from children names — skip unresolvable slot sublayers
        if (node.children) {
          node.children.forEach(function(child) {
            try {
              if (child.type === 'COMPONENT') {
                // Parse variant name (e.g., "Size=md, State=default")
                var variantProps = {};
                var parts = child.name.split(',').map(function(p) { return p.trim(); });
                parts.forEach(function(part) {
                  var kv = part.split('=');
                  if (kv.length === 2) {
                    var key = kv[0].trim();
                    var value = kv[1].trim();
                    variantProps[key] = value;

                    // Track all values for each axis
                    if (!variantAxes[key]) {
                      variantAxes[key] = [];
                    }
                    if (variantAxes[key].indexOf(value) === -1) {
                      variantAxes[key].push(value);
                    }
                  }
                });

                variants.push({
                  key: child.key,
                  nodeId: child.id,
                  name: child.name,
                  description: child.description || null,
                  variantProperties: variantProps,
                  width: child.width,
                  height: child.height
                });
              }
            } catch (e) { /* slot sublayer — skip */ }
          });
        }

        // Convert variantAxes object to array format
        var axes = [];
        for (var axisName in variantAxes) {
          if (variantAxes.hasOwnProperty(axisName)) {
            axes.push({
              name: axisName,
              values: variantAxes[axisName]
            });
          }
        }

        return {
          key: node.key,
          nodeId: node.id,
          name: node.name,
          type: 'COMPONENT_SET',
          description: node.description || null,
          variantAxes: axes,
          variants: variants,
          defaultVariant: variants.length > 0 ? variants[0] : null,
          properties: node.componentPropertyDefinitions ? Object.keys(node.componentPropertyDefinitions).map(function(propName) {
            var propDef = node.componentPropertyDefinitions[propName];
            return {
              name: propName,
              type: propDef.type,
              defaultValue: propDef.defaultValue
            };
          }) : []
        };
      }

      // Recursively search for components
      function findComponents(node) {
        if (!node) return;

        if (node.type === 'COMPONENT_SET') {
          componentSets.push(extractComponentSetData(node));
        } else if (node.type === 'COMPONENT') {
          // Only add standalone components (not variants inside component sets)
          if (!node.parent || node.parent.type !== 'COMPONENT_SET') {
            components.push(extractComponentData(node, false));
          }
        }

        // Recurse into children — skip unresolvable slot sublayers
        if (node.children) {
          node.children.forEach(function(child) {
            try { findComponents(child); } catch (e) { /* slot sublayer — skip */ }
          });
        }
      }

      // Load all pages first (required before accessing children)
      console.log('🌉 [Desktop Bridge] Loading all pages...');
      await figma.loadAllPagesAsync();

      // Process pages in batches with event loop yields to prevent UI freeze
      // This is critical for large design systems that could otherwise crash
      var pages = figma.root.children;
      var PAGE_BATCH_SIZE = 3;  // Process 3 pages at a time
      var totalPages = pages.length;

      console.log('🌉 [Desktop Bridge] Processing ' + totalPages + ' pages in batches of ' + PAGE_BATCH_SIZE + '...');

      for (var pageIndex = 0; pageIndex < totalPages; pageIndex += PAGE_BATCH_SIZE) {
        var batchEnd = Math.min(pageIndex + PAGE_BATCH_SIZE, totalPages);
        var batchPages = [];
        for (var j = pageIndex; j < batchEnd; j++) {
          batchPages.push(pages[j]);
        }

        // Process this batch of pages
        batchPages.forEach(function(page) {
          findComponents(page);
        });

        // Log progress for large files
        if (totalPages > PAGE_BATCH_SIZE) {
          console.log('🌉 [Desktop Bridge] Processed pages ' + (pageIndex + 1) + '-' + batchEnd + ' of ' + totalPages + ' (found ' + components.length + ' components so far)');
        }

        // Yield to event loop between batches to prevent UI freeze and allow cancellation
        if (batchEnd < totalPages) {
          await new Promise(function(resolve) { setTimeout(resolve, 0); });
        }
      }

      console.log('🌉 [Desktop Bridge] Found ' + components.length + ' components and ' + componentSets.length + ' component sets');

      figma.ui.postMessage({
        type: 'GET_LOCAL_COMPONENTS_RESULT',
        requestId: msg.requestId,
        success: true,
        data: {
          components: components,
          componentSets: componentSets,
          totalComponents: components.length,
          totalComponentSets: componentSets.length,
          // Include file metadata for context verification
          fileName: figma.root.name,
          fileKey: figma.fileKey || null,
          timestamp: Date.now()
        }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Get local components error:', errorMsg);
      figma.ui.postMessage({
        type: 'GET_LOCAL_COMPONENTS_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // INSTANTIATE_COMPONENT - Create a component instance with overrides
  // ============================================================================
  else if (msg.type === 'INSTANTIATE_COMPONENT') {
    try {
      console.log('🌉 [Desktop Bridge] Instantiating component:', msg.componentKey || msg.nodeId);

      var component = null;
      var instance = null;

      // Try published library first (by key), then fall back to local component (by nodeId)
      if (msg.componentKey) {
        // Try importComponentByKeyAsync first (for COMPONENT nodes)
        try {
          var importResult = await Promise.race([
            figma.importComponentByKeyAsync(msg.componentKey),
            new Promise(function(_, reject) {
              setTimeout(function() { reject(new Error('Import timed out after 15s — component may not be published to a team library')); }, 15000);
            })
          ]);
          component = importResult;
        } catch (importError) {
          var importErrMsg = importError && importError.message ? importError.message : String(importError);
          console.log('🌉 [Desktop Bridge] importComponentByKeyAsync failed: ' + importErrMsg);
        }

        // If that failed, try importComponentSetByKeyAsync (for COMPONENT_SET nodes)
        if (!component) {
          try {
            var setResult = await Promise.race([
              figma.importComponentSetByKeyAsync(msg.componentKey),
              new Promise(function(_, reject) {
                setTimeout(function() { reject(new Error('ComponentSet import timed out after 15s')); }, 15000);
              })
            ]);
            // Got the component set — use its default variant (first child)
            if (setResult && setResult.type === 'COMPONENT_SET') {
              console.log('🌉 [Desktop Bridge] Imported component set "' + setResult.name + '" with ' + setResult.children.length + ' variants');
              component = setResult.defaultVariant || setResult.children[0];
            }
          } catch (setError) {
            var setErrMsg = setError && setError.message ? setError.message : String(setError);
            console.log('🌉 [Desktop Bridge] importComponentSetByKeyAsync also failed: ' + setErrMsg + ', trying local...');
          }
        }
      }

      // Fall back to local component by nodeId
      if (!component && msg.nodeId) {
        var node = await figma.getNodeByIdAsync(msg.nodeId);
        if (node) {
          if (node.type === 'COMPONENT') {
            component = node;
          } else if (node.type === 'COMPONENT_SET') {
            // For component sets, find the right variant or use default
            if (msg.variant && node.children && node.children.length > 0) {
              // Build variant name from properties (e.g., "Type=Simple, State=Default")
              var variantParts = [];
              for (var prop in msg.variant) {
                if (msg.variant.hasOwnProperty(prop)) {
                  variantParts.push(prop + '=' + msg.variant[prop]);
                }
              }
              var targetVariantName = variantParts.join(', ');
              console.log('🌉 [Desktop Bridge] Looking for variant:', targetVariantName);

              // Find matching variant
              for (var i = 0; i < node.children.length; i++) {
                var child = node.children[i];
                if (child.type === 'COMPONENT' && child.name === targetVariantName) {
                  component = child;
                  console.log('🌉 [Desktop Bridge] Found exact variant match');
                  break;
                }
              }

              // If no exact match, try partial match
              if (!component) {
                for (var i = 0; i < node.children.length; i++) {
                  var child = node.children[i];
                  if (child.type === 'COMPONENT') {
                    var matches = true;
                    for (var prop in msg.variant) {
                      if (msg.variant.hasOwnProperty(prop)) {
                        var expected = prop + '=' + msg.variant[prop];
                        if (child.name.indexOf(expected) === -1) {
                          matches = false;
                          break;
                        }
                      }
                    }
                    if (matches) {
                      component = child;
                      console.log('🌉 [Desktop Bridge] Found partial variant match:', child.name);
                      break;
                    }
                  }
                }
              }
            }

            // Default to first variant if no match
            if (!component && node.children && node.children.length > 0) {
              component = node.children[0];
              console.log('🌉 [Desktop Bridge] Using default variant:', component.name);
            }
          }
        }
      }

      if (!component) {
        // Build detailed error message with actionable guidance
        var errorParts = ['Component not found.'];

        if (msg.componentKey && !msg.nodeId) {
          errorParts.push('Component key "' + msg.componentKey + '" not found. Note: componentKey only works for components from published libraries. For local/unpublished components, you must provide nodeId instead.');
        } else if (msg.componentKey && msg.nodeId) {
          errorParts.push('Neither componentKey "' + msg.componentKey + '" nor nodeId "' + msg.nodeId + '" resolved to a valid component. The identifiers may be stale from a previous session.');
        } else if (msg.nodeId) {
          errorParts.push('NodeId "' + msg.nodeId + '" does not exist in this file. NodeIds are session-specific and become stale when Figma restarts or the file is closed.');
        } else {
          errorParts.push('No componentKey or nodeId was provided.');
        }

        errorParts.push('SOLUTION: Call figma_search_components to get fresh identifiers, then pass BOTH componentKey AND nodeId together for reliable instantiation.');

        throw new Error(errorParts.join(' '));
      }

      // Create the instance
      instance = component.createInstance();

      // Apply position if specified
      if (msg.position) {
        instance.x = msg.position.x || 0;
        instance.y = msg.position.y || 0;
      }

      // Apply size override if specified
      if (msg.size) {
        instance.resize(msg.size.width, msg.size.height);
      }

      // Apply property overrides
      if (msg.overrides) {
        for (var propName in msg.overrides) {
          if (msg.overrides.hasOwnProperty(propName)) {
            try {
              instance.setProperties({ [propName]: msg.overrides[propName] });
            } catch (propError) {
              console.warn('🌉 [Desktop Bridge] Could not set property ' + propName + ':', propError.message);
            }
          }
        }
      }

      // Apply variant selection if specified
      if (msg.variant) {
        try {
          instance.setProperties(msg.variant);
        } catch (variantError) {
          console.warn('🌉 [Desktop Bridge] Could not set variant:', variantError.message);
        }
      }

      // Append to parent if specified
      if (msg.parentId) {
        var parent = await figma.getNodeByIdAsync(msg.parentId);
        if (parent && 'appendChild' in parent) {
          parent.appendChild(instance);
        }
      }

      console.log('🌉 [Desktop Bridge] Component instantiated:', instance.id);

      figma.ui.postMessage({
        type: 'INSTANTIATE_COMPONENT_RESULT',
        requestId: msg.requestId,
        success: true,
        instance: {
          id: instance.id,
          name: instance.name,
          x: instance.x,
          y: instance.y,
          width: instance.width,
          height: instance.height
        }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Instantiate component error:', errorMsg);
      figma.ui.postMessage({
        type: 'INSTANTIATE_COMPONENT_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // SET_NODE_DESCRIPTION - Set description on component/style
  // ============================================================================
  else if (msg.type === 'SET_NODE_DESCRIPTION') {
    try {
      console.log('🌉 [Desktop Bridge] Setting description on node:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      // Check if node supports description
      if (!('description' in node)) {
        throw new Error('Node type ' + node.type + ' does not support description');
      }

      // Set description (and markdown if supported)
      node.description = msg.description || '';
      if (msg.descriptionMarkdown && 'descriptionMarkdown' in node) {
        node.descriptionMarkdown = msg.descriptionMarkdown;
      }

      console.log('🌉 [Desktop Bridge] Description set successfully');

      figma.ui.postMessage({
        type: 'SET_NODE_DESCRIPTION_RESULT',
        requestId: msg.requestId,
        success: true,
        node: { id: node.id, name: node.name, description: node.description }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Set description error:', errorMsg);
      figma.ui.postMessage({
        type: 'SET_NODE_DESCRIPTION_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // GET_ANNOTATIONS - Read annotations from a node (and optionally children)
  // ============================================================================
  else if (msg.type === 'GET_ANNOTATIONS') {
    try {
      console.log('🌉 [Desktop Bridge] Getting annotations for node:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      // Get annotation categories for name resolution
      var categories = [];
      try {
        categories = await figma.annotations.getAnnotationCategoriesAsync();
      } catch (e) {
        console.log('🌉 [Desktop Bridge] Could not fetch annotation categories:', e.message);
      }

      // Build category lookup map
      var categoryMap = {};
      for (var ci = 0; ci < categories.length; ci++) {
        categoryMap[categories[ci].id] = categories[ci].name;
      }

      // Helper to extract annotations from a single node
      function extractAnnotations(n) {
        var anns = n.annotations || [];
        var result = [];
        for (var ai = 0; ai < anns.length; ai++) {
          var ann = anns[ai];
          var props = [];
          if (ann.properties) {
            for (var pi = 0; pi < ann.properties.length; pi++) {
              props.push({ type: ann.properties[pi].type });
            }
          }
          result.push({
            label: ann.label || null,
            labelMarkdown: ann.labelMarkdown || null,
            properties: props.length > 0 ? props : null,
            categoryId: ann.categoryId || null,
            categoryName: ann.categoryId && categoryMap[ann.categoryId] ? categoryMap[ann.categoryId] : null
          });
        }
        return result;
      }

      // Collect annotations from the node itself
      var nodeAnnotations = extractAnnotations(node);
      var childAnnotations = [];

      // Optionally walk children
      var includeChildren = msg.includeChildren || false;
      var maxDepth = msg.depth || 1;

      if (includeChildren && 'children' in node && node.children) {
        function walkChildren(parent, currentDepth) {
          if (currentDepth > maxDepth) return;
          for (var i = 0; i < parent.children.length; i++) {
            var child = parent.children[i];
            try {
              var anns = extractAnnotations(child);
              if (anns.length > 0) {
                childAnnotations.push({
                  nodeId: child.id,
                  nodeName: child.name,
                  nodeType: child.type,
                  annotations: anns
                });
              }
              if ('children' in child && child.children) {
                walkChildren(child, currentDepth + 1);
              }
            } catch (e) {
              // Skip inaccessible children (slot sublayers, etc.)
            }
          }
        }
        walkChildren(node, 1);
      }

      var result = {
        nodeId: node.id,
        nodeName: node.name,
        nodeType: node.type,
        annotations: nodeAnnotations,
        annotationCount: nodeAnnotations.length,
        children: includeChildren ? childAnnotations : undefined,
        childAnnotationCount: includeChildren ? childAnnotations.reduce(function(sum, c) { return sum + c.annotations.length; }, 0) : undefined,
        availableCategories: categories.map(function(c) { return { id: c.id, name: c.name }; })
      };

      console.log('🌉 [Desktop Bridge] Annotations retrieved. Node: ' + nodeAnnotations.length + ', Children: ' + (childAnnotations.length || 0));

      figma.ui.postMessage({
        type: 'GET_ANNOTATIONS_RESULT',
        requestId: msg.requestId,
        success: true,
        data: result
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Get annotations error:', errorMsg);
      figma.ui.postMessage({
        type: 'GET_ANNOTATIONS_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // SET_ANNOTATIONS - Write annotations to a node
  // ============================================================================
  else if (msg.type === 'SET_ANNOTATIONS') {
    try {
      console.log('🌉 [Desktop Bridge] Setting annotations on node:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      // Verify node supports annotations
      if (!('annotations' in node)) {
        throw new Error('Node type ' + node.type + ' does not support annotations');
      }

      // Build the annotations array
      var newAnnotations = [];
      var inputAnnotations = msg.annotations || [];

      for (var i = 0; i < inputAnnotations.length; i++) {
        var input = inputAnnotations[i];
        var ann = {};

        if (input.label) {
          ann.label = input.label;
        }
        if (input.labelMarkdown) {
          ann.labelMarkdown = input.labelMarkdown;
        }
        if (input.properties && input.properties.length > 0) {
          ann.properties = [];
          for (var p = 0; p < input.properties.length; p++) {
            ann.properties.push({ type: input.properties[p].type });
          }
        }
        if (input.categoryId) {
          ann.categoryId = input.categoryId;
        }

        newAnnotations.push(ann);
      }

      // Optionally append to existing annotations instead of replacing
      if (msg.mode === 'append') {
        var existing = node.annotations || [];
        var merged = [];
        for (var e = 0; e < existing.length; e++) {
          var ex = existing[e];
          var copy = {};
          // Figma auto-populates both label and labelMarkdown on read,
          // but rejects writing both — prefer labelMarkdown when both exist
          if (ex.labelMarkdown) {
            copy.labelMarkdown = ex.labelMarkdown;
          } else if (ex.label) {
            copy.label = ex.label;
          }
          if (ex.properties) copy.properties = ex.properties;
          if (ex.categoryId) copy.categoryId = ex.categoryId;
          merged.push(copy);
        }
        for (var n = 0; n < newAnnotations.length; n++) {
          merged.push(newAnnotations[n]);
        }
        newAnnotations = merged;
      }

      // Set annotations on the node
      node.annotations = newAnnotations;

      console.log('🌉 [Desktop Bridge] Annotations set successfully. Count: ' + newAnnotations.length);

      figma.ui.postMessage({
        type: 'SET_ANNOTATIONS_RESULT',
        requestId: msg.requestId,
        success: true,
        data: {
          nodeId: node.id,
          nodeName: node.name,
          annotationCount: newAnnotations.length,
          mode: msg.mode || 'replace'
        }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Set annotations error:', errorMsg);
      figma.ui.postMessage({
        type: 'SET_ANNOTATIONS_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // GET_ANNOTATION_CATEGORIES - List available annotation categories
  // ============================================================================
  else if (msg.type === 'GET_ANNOTATION_CATEGORIES') {
    try {
      console.log('🌉 [Desktop Bridge] Fetching annotation categories');

      var categories = await figma.annotations.getAnnotationCategoriesAsync();
      var result = categories.map(function(c) { return { id: c.id, name: c.name }; });

      console.log('🌉 [Desktop Bridge] Found ' + result.length + ' annotation categories');

      figma.ui.postMessage({
        type: 'GET_ANNOTATION_CATEGORIES_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { categories: result }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Get annotation categories error:', errorMsg);
      figma.ui.postMessage({
        type: 'GET_ANNOTATION_CATEGORIES_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // ADD_COMPONENT_PROPERTY - Add property to component
  // ============================================================================
  else if (msg.type === 'ADD_COMPONENT_PROPERTY') {
    try {
      console.log('🌉 [Desktop Bridge] Adding component property:', msg.propertyName, 'type:', msg.propertyType);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET') {
        throw new Error('Node must be a COMPONENT or COMPONENT_SET. Got: ' + node.type);
      }

      // Check if it's a variant (can't add properties to variants)
      if (node.type === 'COMPONENT' && node.parent && node.parent.type === 'COMPONENT_SET') {
        throw new Error('Cannot add properties to variant components. Add to the parent COMPONENT_SET instead.');
      }

      // Build options if preferredValues provided
      var options = undefined;
      if (msg.preferredValues) {
        options = { preferredValues: msg.preferredValues };
      }

      // Use msg.propertyType (not msg.type which is the message type 'ADD_COMPONENT_PROPERTY')
      var propertyNameWithId = node.addComponentProperty(msg.propertyName, msg.propertyType, msg.defaultValue, options);

      console.log('🌉 [Desktop Bridge] Property added:', propertyNameWithId);

      figma.ui.postMessage({
        type: 'ADD_COMPONENT_PROPERTY_RESULT',
        requestId: msg.requestId,
        success: true,
        propertyName: propertyNameWithId
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Add component property error:', errorMsg);
      figma.ui.postMessage({
        type: 'ADD_COMPONENT_PROPERTY_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // EDIT_COMPONENT_PROPERTY - Edit existing component property
  // ============================================================================
  else if (msg.type === 'EDIT_COMPONENT_PROPERTY') {
    try {
      console.log('🌉 [Desktop Bridge] Editing component property:', msg.propertyName);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET') {
        throw new Error('Node must be a COMPONENT or COMPONENT_SET. Got: ' + node.type);
      }

      var propertyNameWithId = node.editComponentProperty(msg.propertyName, msg.newValue);

      console.log('🌉 [Desktop Bridge] Property edited:', propertyNameWithId);

      figma.ui.postMessage({
        type: 'EDIT_COMPONENT_PROPERTY_RESULT',
        requestId: msg.requestId,
        success: true,
        propertyName: propertyNameWithId
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Edit component property error:', errorMsg);
      figma.ui.postMessage({
        type: 'EDIT_COMPONENT_PROPERTY_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // DELETE_COMPONENT_PROPERTY - Delete a component property
  // ============================================================================
  else if (msg.type === 'DELETE_COMPONENT_PROPERTY') {
    try {
      console.log('🌉 [Desktop Bridge] Deleting component property:', msg.propertyName);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET') {
        throw new Error('Node must be a COMPONENT or COMPONENT_SET. Got: ' + node.type);
      }

      node.deleteComponentProperty(msg.propertyName);

      console.log('🌉 [Desktop Bridge] Property deleted');

      figma.ui.postMessage({
        type: 'DELETE_COMPONENT_PROPERTY_RESULT',
        requestId: msg.requestId,
        success: true
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Delete component property error:', errorMsg);
      figma.ui.postMessage({
        type: 'DELETE_COMPONENT_PROPERTY_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // RESIZE_NODE - Resize any node
  // ============================================================================
  else if (msg.type === 'RESIZE_NODE') {
    try {
      console.log('🌉 [Desktop Bridge] Resizing node:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      if (!('resize' in node)) {
        throw new Error('Node type ' + node.type + ' does not support resize');
      }

      if (msg.withConstraints) {
        node.resize(msg.width, msg.height);
      } else {
        node.resizeWithoutConstraints(msg.width, msg.height);
      }

      console.log('🌉 [Desktop Bridge] Node resized to:', msg.width, 'x', msg.height);

      figma.ui.postMessage({
        type: 'RESIZE_NODE_RESULT',
        requestId: msg.requestId,
        success: true,
        node: { id: node.id, name: node.name, width: node.width, height: node.height }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Resize node error:', errorMsg);
      figma.ui.postMessage({
        type: 'RESIZE_NODE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // MOVE_NODE - Move/position a node
  // ============================================================================
  else if (msg.type === 'MOVE_NODE') {
    try {
      console.log('🌉 [Desktop Bridge] Moving node:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      if (!('x' in node)) {
        throw new Error('Node type ' + node.type + ' does not support positioning');
      }

      node.x = msg.x;
      node.y = msg.y;

      console.log('🌉 [Desktop Bridge] Node moved to:', msg.x, ',', msg.y);

      figma.ui.postMessage({
        type: 'MOVE_NODE_RESULT',
        requestId: msg.requestId,
        success: true,
        node: { id: node.id, name: node.name, x: node.x, y: node.y }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Move node error:', errorMsg);
      figma.ui.postMessage({
        type: 'MOVE_NODE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // SET_NODE_FILLS - Set fills (colors) on a node
  // ============================================================================
  else if (msg.type === 'SET_NODE_FILLS') {
    try {
      console.log('🌉 [Desktop Bridge] Setting fills on node:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      if (!('fills' in node)) {
        throw new Error('Node type ' + node.type + ' does not support fills');
      }

      // Process fills - convert hex colors if needed
      var processedFills = msg.fills.map(function(fill) {
        if (fill.type === 'SOLID' && typeof fill.color === 'string') {
          // Convert hex to RGB
          var rgb = hexToFigmaRGB(fill.color);
          return {
            type: 'SOLID',
            color: { r: rgb.r, g: rgb.g, b: rgb.b },
            opacity: rgb.a !== undefined ? rgb.a : (fill.opacity !== undefined ? fill.opacity : 1)
          };
        }
        return fill;
      });

      node.fills = processedFills;

      console.log('🌉 [Desktop Bridge] Fills set successfully');

      figma.ui.postMessage({
        type: 'SET_NODE_FILLS_RESULT',
        requestId: msg.requestId,
        success: true,
        node: { id: node.id, name: node.name }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Set fills error:', errorMsg);
      figma.ui.postMessage({
        type: 'SET_NODE_FILLS_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // SET_IMAGE_FILL - Set an image fill on one or more nodes
  // Receives raw image bytes (as Array) from ui.html which decodes base64
  // ============================================================================
  else if (msg.type === 'SET_IMAGE_FILL') {
    try {
      console.log('🌉 [Desktop Bridge] Setting image fill, bytes:', msg.imageBytes.length);

      // Convert the plain array back to Uint8Array
      var bytes = new Uint8Array(msg.imageBytes);

      // Create the image in Figma
      var image = figma.createImage(bytes);
      var imageHash = image.hash;

      var fill = {
        type: 'IMAGE',
        scaleMode: msg.scaleMode || 'FILL',
        imageHash: imageHash
      };

      // Resolve target nodes
      var nodeIds = msg.nodeIds || (msg.nodeId ? [msg.nodeId] : []);
      var updatedCount = 0;
      var updatedNodes = [];

      for (var i = 0; i < nodeIds.length; i++) {
        var node = await figma.getNodeByIdAsync(nodeIds[i]);
        if (node && 'fills' in node) {
          node.fills = [fill];
          updatedCount++;
          updatedNodes.push({ id: node.id, name: node.name });
        }
      }

      console.log('🌉 [Desktop Bridge] Image fill applied to', updatedCount, 'node(s), hash:', imageHash);

      figma.ui.postMessage({
        type: 'SET_IMAGE_FILL_RESULT',
        requestId: msg.requestId,
        success: true,
        imageHash: imageHash,
        updatedCount: updatedCount,
        nodes: updatedNodes
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Set image fill error:', errorMsg);
      figma.ui.postMessage({
        type: 'SET_IMAGE_FILL_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // SET_NODE_STROKES - Set strokes on a node
  // ============================================================================
  else if (msg.type === 'SET_NODE_STROKES') {
    try {
      console.log('🌉 [Desktop Bridge] Setting strokes on node:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      if (!('strokes' in node)) {
        throw new Error('Node type ' + node.type + ' does not support strokes');
      }

      // Process strokes - convert hex colors if needed
      var processedStrokes = msg.strokes.map(function(stroke) {
        if (stroke.type === 'SOLID' && typeof stroke.color === 'string') {
          var rgb = hexToFigmaRGB(stroke.color);
          return {
            type: 'SOLID',
            color: { r: rgb.r, g: rgb.g, b: rgb.b },
            opacity: rgb.a !== undefined ? rgb.a : (stroke.opacity !== undefined ? stroke.opacity : 1)
          };
        }
        return stroke;
      });

      node.strokes = processedStrokes;

      if (msg.strokeWeight !== undefined) {
        node.strokeWeight = msg.strokeWeight;
      }

      console.log('🌉 [Desktop Bridge] Strokes set successfully');

      figma.ui.postMessage({
        type: 'SET_NODE_STROKES_RESULT',
        requestId: msg.requestId,
        success: true,
        node: { id: node.id, name: node.name }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Set strokes error:', errorMsg);
      figma.ui.postMessage({
        type: 'SET_NODE_STROKES_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // SET_NODE_OPACITY - Set opacity on a node
  // ============================================================================
  else if (msg.type === 'SET_NODE_OPACITY') {
    try {
      console.log('🌉 [Desktop Bridge] Setting opacity on node:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      if (!('opacity' in node)) {
        throw new Error('Node type ' + node.type + ' does not support opacity');
      }

      node.opacity = Math.max(0, Math.min(1, msg.opacity));

      console.log('🌉 [Desktop Bridge] Opacity set to:', node.opacity);

      figma.ui.postMessage({
        type: 'SET_NODE_OPACITY_RESULT',
        requestId: msg.requestId,
        success: true,
        node: { id: node.id, name: node.name, opacity: node.opacity }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Set opacity error:', errorMsg);
      figma.ui.postMessage({
        type: 'SET_NODE_OPACITY_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // SET_NODE_CORNER_RADIUS - Set corner radius on a node
  // ============================================================================
  else if (msg.type === 'SET_NODE_CORNER_RADIUS') {
    try {
      console.log('🌉 [Desktop Bridge] Setting corner radius on node:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      if (!('cornerRadius' in node)) {
        throw new Error('Node type ' + node.type + ' does not support corner radius');
      }

      node.cornerRadius = msg.radius;

      console.log('🌉 [Desktop Bridge] Corner radius set to:', msg.radius);

      figma.ui.postMessage({
        type: 'SET_NODE_CORNER_RADIUS_RESULT',
        requestId: msg.requestId,
        success: true,
        node: { id: node.id, name: node.name, cornerRadius: node.cornerRadius }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Set corner radius error:', errorMsg);
      figma.ui.postMessage({
        type: 'SET_NODE_CORNER_RADIUS_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // CLONE_NODE - Clone/duplicate a node
  // ============================================================================
  else if (msg.type === 'CLONE_NODE') {
    try {
      console.log('🌉 [Desktop Bridge] Cloning node:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      if (!('clone' in node)) {
        throw new Error('Node type ' + node.type + ' does not support cloning');
      }

      var clonedNode = node.clone();

      console.log('🌉 [Desktop Bridge] Node cloned:', clonedNode.id);

      figma.ui.postMessage({
        type: 'CLONE_NODE_RESULT',
        requestId: msg.requestId,
        success: true,
        node: { id: clonedNode.id, name: clonedNode.name, x: clonedNode.x, y: clonedNode.y }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Clone node error:', errorMsg);
      figma.ui.postMessage({
        type: 'CLONE_NODE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // DELETE_NODE - Delete a node
  // ============================================================================
  else if (msg.type === 'DELETE_NODE') {
    try {
      console.log('🌉 [Desktop Bridge] Deleting node:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      var deletedInfo = { id: node.id, name: node.name };

      node.remove();

      console.log('🌉 [Desktop Bridge] Node deleted');

      figma.ui.postMessage({
        type: 'DELETE_NODE_RESULT',
        requestId: msg.requestId,
        success: true,
        deleted: deletedInfo
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Delete node error:', errorMsg);
      figma.ui.postMessage({
        type: 'DELETE_NODE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // RENAME_NODE - Rename a node
  // ============================================================================
  else if (msg.type === 'RENAME_NODE') {
    try {
      console.log('🌉 [Desktop Bridge] Renaming node:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      var oldName = node.name;
      node.name = msg.newName;

      console.log('🌉 [Desktop Bridge] Node renamed from "' + oldName + '" to "' + msg.newName + '"');

      figma.ui.postMessage({
        type: 'RENAME_NODE_RESULT',
        requestId: msg.requestId,
        success: true,
        node: { id: node.id, name: node.name, oldName: oldName }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Rename node error:', errorMsg);
      figma.ui.postMessage({
        type: 'RENAME_NODE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // SET_TEXT_CONTENT - Set text on a text node
  // ============================================================================
  else if (msg.type === 'SET_TEXT_CONTENT') {
    try {
      console.log('🌉 [Desktop Bridge] Setting text content on node:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      if (node.type !== 'TEXT') {
        throw new Error('Node must be a TEXT node. Got: ' + node.type);
      }

      // Load the font first
      await figma.loadFontAsync(node.fontName);

      node.characters = msg.text;

      // Apply font properties if specified
      if (msg.fontSize) {
        node.fontSize = msg.fontSize;
      }

      console.log('🌉 [Desktop Bridge] Text content set');

      figma.ui.postMessage({
        type: 'SET_TEXT_CONTENT_RESULT',
        requestId: msg.requestId,
        success: true,
        node: { id: node.id, name: node.name, characters: node.characters }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Set text content error:', errorMsg);
      figma.ui.postMessage({
        type: 'SET_TEXT_CONTENT_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // CREATE_CHILD_NODE - Create a new child node
  // ============================================================================
  else if (msg.type === 'CREATE_CHILD_NODE') {
    try {
      console.log('🌉 [Desktop Bridge] Creating child node of type:', msg.nodeType);

      var parent = await figma.getNodeByIdAsync(msg.parentId);
      if (!parent) {
        throw new Error('Parent node not found: ' + msg.parentId);
      }

      if (!('appendChild' in parent)) {
        throw new Error('Parent node type ' + parent.type + ' does not support children');
      }

      var newNode;
      var props = msg.properties || {};

      switch (msg.nodeType) {
        case 'RECTANGLE':
          newNode = figma.createRectangle();
          break;
        case 'ELLIPSE':
          newNode = figma.createEllipse();
          break;
        case 'FRAME':
          newNode = figma.createFrame();
          break;
        case 'TEXT':
          newNode = figma.createText();
          // Load default font
          await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
          newNode.fontName = { family: 'Inter', style: 'Regular' };
          if (props.text) {
            newNode.characters = props.text;
          }
          break;
        case 'LINE':
          newNode = figma.createLine();
          break;
        case 'POLYGON':
          newNode = figma.createPolygon();
          break;
        case 'STAR':
          newNode = figma.createStar();
          break;
        case 'VECTOR':
          newNode = figma.createVector();
          break;
        default:
          throw new Error('Unsupported node type: ' + msg.nodeType);
      }

      // Apply common properties
      if (props.name) newNode.name = props.name;
      if (props.x !== undefined) newNode.x = props.x;
      if (props.y !== undefined) newNode.y = props.y;
      if (props.width !== undefined && props.height !== undefined) {
        newNode.resize(props.width, props.height);
      }

      // Apply fills if specified
      if (props.fills) {
        var processedFills = props.fills.map(function(fill) {
          if (fill.type === 'SOLID' && typeof fill.color === 'string') {
            var rgb = hexToFigmaRGB(fill.color);
            return {
              type: 'SOLID',
              color: { r: rgb.r, g: rgb.g, b: rgb.b },
              opacity: rgb.a !== undefined ? rgb.a : 1
            };
          }
          return fill;
        });
        newNode.fills = processedFills;
      }

      // Add to parent
      parent.appendChild(newNode);

      console.log('🌉 [Desktop Bridge] Child node created:', newNode.id);

      figma.ui.postMessage({
        type: 'CREATE_CHILD_NODE_RESULT',
        requestId: msg.requestId,
        success: true,
        node: {
          id: newNode.id,
          name: newNode.name,
          type: newNode.type,
          x: newNode.x,
          y: newNode.y,
          width: newNode.width,
          height: newNode.height
        }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Create child node error:', errorMsg);
      figma.ui.postMessage({
        type: 'CREATE_CHILD_NODE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // CAPTURE_SCREENSHOT - Capture node screenshot using plugin exportAsync
  // This captures the CURRENT plugin runtime state (not cloud state like REST API)
  // ============================================================================
  else if (msg.type === 'CAPTURE_SCREENSHOT') {
    try {
      console.log('🌉 [Desktop Bridge] Capturing screenshot for node:', msg.nodeId);

      var node = msg.nodeId ? await figma.getNodeByIdAsync(msg.nodeId) : figma.currentPage;
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      // Verify node supports export
      if (!('exportAsync' in node)) {
        throw new Error('Node type ' + node.type + ' does not support export');
      }

      // Configure export settings — AI-optimized defaults (PNG 1x)
      var format = msg.format || 'PNG';
      var scale = msg.scale || 1;

      // AI vision cap: Claude API resizes images to 1568px on the longest side
      // before processing, so exporting larger just wastes bandwidth and tokens.
      var AI_MAX_DIMENSION = 1568;
      var nodeWidth = 0;
      var nodeHeight = 0;

      if (node.type === 'PAGE') {
        // Pages don't have fixed dimensions — calculate from visible children
        var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
        for (var i = 0; i < node.children.length; i++) {
          var child = node.children[i];
          if (child.visible !== false && 'absoluteBoundingBox' in child && child.absoluteBoundingBox) {
            var bb = child.absoluteBoundingBox;
            minX = Math.min(minX, bb.x);
            minY = Math.min(minY, bb.y);
            maxX = Math.max(maxX, bb.x + bb.width);
            maxY = Math.max(maxY, bb.y + bb.height);
          }
        }
        if (minX !== Infinity) {
          nodeWidth = maxX - minX;
          nodeHeight = maxY - minY;
        }
      } else if ('width' in node && 'height' in node) {
        nodeWidth = node.width;
        nodeHeight = node.height;
      }

      // Cap scale so the longest exported side doesn't exceed the AI processing ceiling
      if (nodeWidth > 0 && nodeHeight > 0) {
        var longestSide = Math.max(nodeWidth, nodeHeight);
        var exportedLongest = longestSide * scale;
        if (exportedLongest > AI_MAX_DIMENSION) {
          var cappedScale = AI_MAX_DIMENSION / longestSide;
          console.log('🌉 [Desktop Bridge] Capping scale from', scale, 'to', cappedScale.toFixed(3),
            '(node ' + Math.round(longestSide) + 'px, cap ' + AI_MAX_DIMENSION + 'px)');
          scale = cappedScale;
        }
      }

      // Analyze node content to recommend optimal format
      var imageCount = 0;
      var gradientCount = 0;
      var textCount = 0;
      var vectorCount = 0;
      var maxDepth = 3; // Don't recurse too deep — top-level composition is enough

      function analyzeContent(n, depth) {
        if (depth > maxDepth) return;
        if (n.type === 'TEXT') { textCount++; return; }
        if (n.type === 'VECTOR' || n.type === 'LINE' || n.type === 'STAR' ||
            n.type === 'POLYGON' || n.type === 'ELLIPSE' || n.type === 'BOOLEAN_OPERATION') {
          vectorCount++; return;
        }
        // Check fills for images and gradients
        if ('fills' in n && Array.isArray(n.fills)) {
          for (var f = 0; f < n.fills.length; f++) {
            var fill = n.fills[f];
            if (fill.visible === false) continue;
            if (fill.type === 'IMAGE') imageCount++;
            if (fill.type === 'GRADIENT_LINEAR' || fill.type === 'GRADIENT_RADIAL' ||
                fill.type === 'GRADIENT_ANGULAR' || fill.type === 'GRADIENT_DIAMOND') gradientCount++;
          }
        }
        // Recurse into children
        if ('children' in n) {
          for (var c = 0; c < n.children.length; c++) {
            if (n.children[c].visible !== false) {
              analyzeContent(n.children[c], depth + 1);
            }
          }
        }
      }
      analyzeContent(node, 0);

      var totalElements = imageCount + gradientCount + textCount + vectorCount;
      var photoHeavy = totalElements > 0 && (imageCount + gradientCount) / totalElements > 0.5;
      var adviceParts = [];

      // Format advice
      if (photoHeavy) {
        adviceParts.push('Image/gradient-heavy content — try format: "JPG" for smaller file.');
      }

      // Scale capping advice
      if (scale < (msg.scale || 1)) {
        adviceParts.push('Scale capped from ' + (msg.scale || 1) + 'x to ' +
          scale.toFixed(2) + 'x (AI vision max: 1568px).');
      }

      // Scope advice for full-page captures with heavy downscaling
      if (node.type === 'PAGE' && scale < 0.5) {
        adviceParts.push('Full-page capture at ' + scale.toFixed(2) +
          'x — text may be unreadable. Pass a nodeId to target a specific frame or component.');
      }

      var formatAdvice = adviceParts.join(' ');

      var exportSettings = {
        format: format,
        constraint: { type: 'SCALE', value: scale }
      };

      // Export the node
      var bytes = await node.exportAsync(exportSettings);

      // Convert to base64
      var base64 = figma.base64Encode(bytes);

      // Get node bounds for context
      var bounds = null;
      if ('absoluteBoundingBox' in node) {
        bounds = node.absoluteBoundingBox;
      }

      console.log('🌉 [Desktop Bridge] Screenshot captured:', bytes.length, 'bytes (' + format + ' @ ' + scale.toFixed(2) + 'x)');

      figma.ui.postMessage({
        type: 'CAPTURE_SCREENSHOT_RESULT',
        requestId: msg.requestId,
        success: true,
        image: {
          base64: base64,
          format: format,
          scale: scale,
          byteLength: bytes.length,
          node: {
            id: node.id,
            name: node.name,
            type: node.type
          },
          bounds: bounds,
          formatAdvice: formatAdvice
        }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Screenshot capture error:', errorMsg);
      figma.ui.postMessage({
        type: 'CAPTURE_SCREENSHOT_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // GET_FILE_INFO - Report which file this plugin instance is running in
  // Used by WebSocket bridge to identify the connected file
  // ============================================================================
  else if (msg.type === 'GET_FILE_INFO') {
    try {
      var selection = figma.currentPage.selection;
      figma.ui.postMessage({
        type: 'GET_FILE_INFO_RESULT',
        requestId: msg.requestId,
        success: true,
        fileInfo: {
          fileName: figma.root.name,
          fileKey: figma.fileKey || null,
          currentPage: figma.currentPage.name,
          currentPageId: figma.currentPage.id,
          selectionCount: selection ? selection.length : 0,
          pluginVersion: PLUGIN_VERSION,
          editorType: __editorType
        }
      });
    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      figma.ui.postMessage({
        type: 'GET_FILE_INFO_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // RESIZE_UI - Dynamically resize the plugin window (e.g., Cloud Mode toggle)
  // ============================================================================
  else if (msg.type === 'RESIZE_UI') {
    figma.ui.resize(msg.width || 120, msg.height || 36);
  }

  // ============================================================================
  // STORE_CLOUD_CONFIG - Persist cloud pairing config in clientStorage
  // ============================================================================
  else if (msg.type === 'STORE_CLOUD_CONFIG') {
    figma.clientStorage.setAsync('cloudConfig', { code: msg.code, timestamp: Date.now() })
      .catch(function() { /* non-critical */ });
  }

  // ============================================================================
  // RELOAD_UI - Reload the plugin UI iframe (re-establishes WebSocket connection)
  // Uses figma.showUI(__html__) to reload without restarting code.js
  // ============================================================================
  else if (msg.type === 'RELOAD_UI') {
    try {
      console.log('🌉 [Desktop Bridge] Reloading plugin UI');
      figma.ui.postMessage({
        type: 'RELOAD_UI_RESULT',
        requestId: msg.requestId,
        success: true
      });
      // Short delay to let the response message be sent before reload
      setTimeout(function() {
        figma.showUI(__html__, { width: 180, height: 50, visible: true, themeColors: true });
      }, 100);
    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      figma.ui.postMessage({
        type: 'RELOAD_UI_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // SET_INSTANCE_PROPERTIES - Update component properties on an instance
  // Uses instance.setProperties() to update TEXT, BOOLEAN, INSTANCE_SWAP, VARIANT
  // ============================================================================
  else if (msg.type === 'SET_INSTANCE_PROPERTIES') {
    try {
      console.log('🌉 [Desktop Bridge] Setting instance properties on:', msg.nodeId);

      var node = await figma.getNodeByIdAsync(msg.nodeId);
      if (!node) {
        throw new Error('Node not found: ' + msg.nodeId);
      }

      if (node.type !== 'INSTANCE') {
        throw new Error('Node must be an INSTANCE. Got: ' + node.type);
      }

      // Load main component first (required for documentAccess: dynamic-page)
      var mainComponent = await node.getMainComponentAsync();

      // Get current properties for reference
      var currentProps = node.componentProperties;
      console.log('🌉 [Desktop Bridge] Current properties:', JSON.stringify(Object.keys(currentProps)));

      // Build the properties object
      // Note: TEXT, BOOLEAN, INSTANCE_SWAP properties use the format "PropertyName#nodeId"
      // VARIANT properties use just "PropertyName"
      var propsToSet = {};
      var propUpdates = msg.properties || {};

      for (var propName in propUpdates) {
        var newValue = propUpdates[propName];

        // Check if this exact property name exists
        if (currentProps[propName] !== undefined) {
          propsToSet[propName] = newValue;
          console.log('🌉 [Desktop Bridge] Setting property:', propName, '=', newValue);
        } else {
          // Try to find a matching property with a suffix (for TEXT/BOOLEAN/INSTANCE_SWAP)
          var foundMatch = false;
          for (var existingProp in currentProps) {
            // Check if this is the base property name with a node ID suffix
            if (existingProp.startsWith(propName + '#')) {
              propsToSet[existingProp] = newValue;
              console.log('🌉 [Desktop Bridge] Found suffixed property:', existingProp, '=', newValue);
              foundMatch = true;
              break;
            }
          }

          if (!foundMatch) {
            console.warn('🌉 [Desktop Bridge] Property not found:', propName, '- Available:', Object.keys(currentProps).join(', '));
          }
        }
      }

      if (Object.keys(propsToSet).length === 0) {
        throw new Error('No valid properties to set. Available properties: ' + Object.keys(currentProps).join(', '));
      }

      // Apply the properties
      node.setProperties(propsToSet);

      // Get updated properties
      var updatedProps = node.componentProperties;

      console.log('🌉 [Desktop Bridge] Instance properties updated');

      figma.ui.postMessage({
        type: 'SET_INSTANCE_PROPERTIES_RESULT',
        requestId: msg.requestId,
        success: true,
        instance: {
          id: node.id,
          name: node.name,
          componentId: mainComponent ? mainComponent.id : null,
          propertiesSet: Object.keys(propsToSet),
          currentProperties: Object.keys(updatedProps).reduce(function(acc, key) {
            acc[key] = {
              type: updatedProps[key].type,
              value: updatedProps[key].value
            };
            return acc;
          }, {})
        }
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Set instance properties error:', errorMsg);
      figma.ui.postMessage({
        type: 'SET_INSTANCE_PROPERTIES_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // LINT_DESIGN - Accessibility and design quality checks on node tree
  // ============================================================================
  else if (msg.type === 'LINT_DESIGN') {
    try {
      console.log('🌉 [Desktop Bridge] Running design lint...');

      // ---- Helper functions ----

      // sRGB linearization
      function lintLinearize(c) {
        return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
      }

      // Relative luminance (r, g, b in 0-1 range)
      function lintLuminance(r, g, b) {
        return 0.2126 * lintLinearize(r) + 0.7152 * lintLinearize(g) + 0.0722 * lintLinearize(b);
      }

      // Contrast ratio between two colors (each r, g, b in 0-1)
      function lintContrastRatio(r1, g1, b1, r2, g2, b2) {
        var l1 = lintLuminance(r1, g1, b1);
        var l2 = lintLuminance(r2, g2, b2);
        var lighter = Math.max(l1, l2);
        var darker = Math.min(l1, l2);
        return (lighter + 0.05) / (darker + 0.05);
      }

      // Convert 0-1 RGB to hex string
      function lintRgbToHex(r, g, b) {
        var rr = Math.round(r * 255).toString(16);
        var gg = Math.round(g * 255).toString(16);
        var bb = Math.round(b * 255).toString(16);
        if (rr.length === 1) rr = '0' + rr;
        if (gg.length === 1) gg = '0' + gg;
        if (bb.length === 1) bb = '0' + bb;
        return '#' + rr.toUpperCase() + gg.toUpperCase() + bb.toUpperCase();
      }

      // Walk up ancestors to find nearest solid fill background color
      function lintGetEffectiveBg(node) {
        var current = node.parent;
        while (current) {
          try {
            if (current.fills && current.fills.length > 0) {
              // Iterate reverse (last = topmost visible fill in Figma's stack)
              for (var fi = current.fills.length - 1; fi >= 0; fi--) {
                var fill = current.fills[fi];
                if (fill.type === 'SOLID' && fill.visible !== false) {
                  var opacity = (fill.opacity !== undefined) ? fill.opacity : 1;
                  return { r: fill.color.r, g: fill.color.g, b: fill.color.b, opacity: opacity };
                }
              }
            }
          } catch (e) {
            // Slot sublayer — skip
          }
          current = current.parent;
        }
        // Default to white if no bg found
        return { r: 1, g: 1, b: 1, opacity: 1 };
      }

      // Check if text qualifies as "large" per WCAG (18pt=24px regular, 14pt≈18.66px bold 700+)
      function lintIsLargeText(fontSize, fontWeight) {
        if (fontSize >= 24) return true;
        if (fontSize >= 18.66 && fontWeight && (fontWeight === 'Bold' || fontWeight === 'Black' || fontWeight === 'ExtraBold')) return true;
        return false;
      }

      // ---- Rule configuration ----
      var allRuleIds = [
        'wcag-contrast', 'wcag-text-size', 'wcag-target-size', 'wcag-line-height',
        'wcag-non-text-contrast', 'wcag-color-only', 'wcag-focus-indicator',
        'wcag-letter-spacing', 'wcag-paragraph-spacing', 'wcag-image-alt',
        'wcag-heading-hierarchy', 'wcag-reflow', 'wcag-reading-order',
        'wcag-disabled-no-context', 'token-misuse',
        'hardcoded-color', 'no-text-style', 'default-name', 'detached-component',
        'no-autolayout', 'empty-container'
      ];

      var ruleGroups = {
        'all': allRuleIds,
        'wcag': [
          'wcag-contrast', 'wcag-text-size', 'wcag-target-size', 'wcag-line-height',
          'wcag-non-text-contrast', 'wcag-color-only', 'wcag-focus-indicator',
          'wcag-letter-spacing', 'wcag-paragraph-spacing', 'wcag-image-alt',
          'wcag-heading-hierarchy', 'wcag-reflow', 'wcag-reading-order',
          'wcag-disabled-no-context'
        ],
        'design-system': ['hardcoded-color', 'no-text-style', 'default-name', 'detached-component', 'token-misuse'],
        'layout': ['no-autolayout', 'empty-container']
      };

      var severityMap = {
        'wcag-contrast': 'critical',
        'wcag-target-size': 'critical',
        'wcag-non-text-contrast': 'critical',
        'wcag-color-only': 'critical',
        'wcag-focus-indicator': 'critical',
        'wcag-text-size': 'warning',
        'wcag-letter-spacing': 'warning',
        'wcag-image-alt': 'warning',
        'wcag-heading-hierarchy': 'warning',
        'wcag-reflow': 'warning',
        'wcag-reading-order': 'warning',
        'wcag-disabled-no-context': 'warning',
        'wcag-line-height': 'info',
        'wcag-paragraph-spacing': 'info',
        'token-misuse': 'warning',
        'hardcoded-color': 'warning',
        'no-text-style': 'warning',
        'default-name': 'warning',
        'detached-component': 'warning',
        'no-autolayout': 'warning',
        'empty-container': 'info'
      };

      // WCAG conformance level per rule — lets teams filter by target level (AA vs AAA)
      var wcagLevelMap = {
        'wcag-contrast': 'aa',           // 1.4.3 Contrast (Minimum) — Level AA
        'wcag-target-size': 'aa',        // 2.5.8 Target Size (Minimum) — Level AA
        'wcag-non-text-contrast': 'aa',  // 1.4.11 Non-text Contrast — Level AA
        'wcag-color-only': 'a',          // 1.4.1 Use of Color — Level A
        'wcag-focus-indicator': 'aa',    // 2.4.7 Focus Visible — Level AA
        'wcag-text-size': 'best-practice', // Not actually 1.4.4; 12px minimum is a readability best practice
        'wcag-line-height': 'best-practice', // 1.4.12 is about supporting user overrides, not requiring specific values
        'wcag-letter-spacing': 'best-practice', // Negative spacing actively harms readability
        'wcag-paragraph-spacing': 'best-practice', // 1.4.12 is about supporting user overrides
        'wcag-image-alt': 'a',           // 1.1.1 Non-text Content — Level A
        'wcag-heading-hierarchy': 'a',   // 1.3.1 Info and Relationships — Level A
        'wcag-reflow': 'aa',            // 1.4.10 Reflow — Level AA
        'wcag-reading-order': 'a',       // 1.3.2 Meaningful Sequence — Level A
        'wcag-disabled-no-context': 'aa', // 4.1.2 Name, Role, Value — disabled elements need ARIA context
        'token-misuse': 'design-system',
        'hardcoded-color': 'design-system',
        'no-text-style': 'design-system',
        'default-name': 'design-system',
        'detached-component': 'design-system',
        'no-autolayout': 'design-system',
        'empty-container': 'design-system'
      };

      var ruleDescriptions = {
        'wcag-contrast': 'Text does not meet WCAG AA contrast ratio (4.5:1 normal, 3:1 large text ≥24px or ≥18.5px bold). Best practice: always target 4.5:1, especially in dark mode.',
        'wcag-text-size': 'Text size is below 12px — readability best practice. Note: WCAG 1.4.4 requires supporting 200% text-only zoom (use rem/em units), not a specific minimum size.',
        'wcag-target-size': 'Interactive element is smaller than 24x24px minimum target size (WCAG 2.5.8)',
        'wcag-line-height': 'Line height is below 1.5x font size — best practice for readability. Note: WCAG 1.4.12 requires that content does not break when users override spacing to 1.5x, not that designs must use 1.5x by default.',
        'wcag-non-text-contrast': 'UI component or graphical object does not meet 3:1 contrast ratio against adjacent color. Also applies to borders and chart elements against adjacent elements (WCAG 1.4.11)',
        'wcag-color-only': 'Information is conveyed only through color change (e.g., error state uses red border without an error message or icon). Color can supplement but must not be the sole indicator (WCAG 1.4.1)',
        'wcag-focus-indicator': 'Interactive component is missing a focus/focused variant or the focus indicator is insufficient. A visible focus state is critical — without it, keyboard users cannot navigate the interface (WCAG 2.4.7)',
        'wcag-letter-spacing': 'Negative letter spacing actively harms readability. WCAG 1.4.12 requires content to support user-overridden spacing without breaking.',
        'wcag-paragraph-spacing': 'Paragraph spacing is below 2x font size — best practice. WCAG 1.4.12 requires content to support user-overridden spacing to 2x without loss of content, not that designs must use 2x by default.',
        'wcag-image-alt': 'Image or image fill has no description annotation for alternative text. All images need alt text; decorative images should be explicitly marked as decorative. Graphs and charts also need long descriptions (e.g., a data table) (WCAG 1.1.1)',
        'wcag-heading-hierarchy': 'Heading levels skip a level (e.g., H1 followed by H3). Use H1 through H6 sequentially without skipping levels (WCAG 1.3.1)',
        'wcag-reflow': 'Frame uses fixed positioning without auto-layout. Content must support 400% zoom on 1280px viewport (equivalent to 320px minimum width) without horizontal scrolling or loss of content (WCAG 1.4.10)',
        'wcag-reading-order': 'Visual position of elements does not match layer order. Keyboard navigation and screen reader order must follow a logical sequence (WCAG 1.3.2)',
        'wcag-disabled-no-context': 'Disabled variant has no tooltip, helper text, or annotation explaining why the element is disabled. Use aria-disabled (not HTML disabled) to keep the element focusable for screen readers, and add a tooltip so all users understand the disabled reason.',
        'token-misuse': 'Variable name prefix does not match its usage context (e.g., a bg/* token used as a text fill, or a text/* token used as a background). This may cause contrast issues and indicates a misbound token.',
        'hardcoded-color': 'Fill color is not bound to a variable or style',
        'no-text-style': 'Text node is not using a text style',
        'default-name': 'Node has a default Figma name (e.g., "Frame 1")',
        'detached-component': 'Frame uses component naming convention but is not a component or instance',
        'no-autolayout': 'Frame with multiple children does not use auto-layout',
        'empty-container': 'Frame has no children'
      };

      var defaultNameRegex = /^(Frame|Rectangle|Ellipse|Line|Text|Group|Component|Instance|Vector|Polygon|Star|Section)(\s+\d+)?$/;
      var interactiveNameRegex = /button|link|input|checkbox|radio|switch|toggle|tab|menu-item/i;

      // ---- Additional helpers for new WCAG rules ----

      // Get the first visible solid fill color from a node (for non-text contrast)
      function lintGetNodeFillColor(node) {
        try {
          var fills = node.fills;
          if (fills && fills.length > 0) {
            for (var i = fills.length - 1; i >= 0; i--) {
              if (fills[i].type === 'SOLID' && fills[i].visible !== false) {
                return { r: fills[i].color.r, g: fills[i].color.g, b: fills[i].color.b };
              }
            }
          }
        } catch (e) { /* slot sublayer */ }
        return null;
      }

      // Get the first visible solid stroke color from a node
      function lintGetNodeStrokeColor(node) {
        try {
          var strokes = node.strokes;
          if (strokes && strokes.length > 0) {
            for (var i = strokes.length - 1; i >= 0; i--) {
              if (strokes[i].type === 'SOLID' && strokes[i].visible !== false) {
                return { r: strokes[i].color.r, g: strokes[i].color.g, b: strokes[i].color.b };
              }
            }
          }
        } catch (e) { /* slot sublayer */ }
        return null;
      }

      // Check if a node has any non-color visual differentiation (icon children, text change, border)
      function lintHasNonColorIndicator(node) {
        try {
          if (!node.children) return false;
          for (var i = 0; i < node.children.length; i++) {
            var child = node.children[i];
            try {
              // Icons are typically vectors, instances, or small frames with vector children
              if (child.type === 'VECTOR' || child.type === 'BOOLEAN_OPERATION') return true;
              if (child.type === 'INSTANCE') return true;
              // Check for visible strokes (borders)
              if (child.strokes && child.strokes.length > 0) {
                for (var si = 0; si < child.strokes.length; si++) {
                  if (child.strokes[si].visible !== false) return true;
                }
              }
            } catch (e) { /* skip */ }
          }
        } catch (e) { /* no children */ }
        return false;
      }

      // Heading level detection from text style name or font size
      var headingStyleRegex = /\bh(\d)\b|heading[\s-]*(\d)/i;
      function lintGetHeadingLevel(node) {
        // Try text style name first
        try {
          if (node.textStyleId && typeof node.textStyleId === 'string') {
            // We can't resolve style name in plugin sandbox directly from ID alone,
            // but we check the node name and font size as fallback
          }
        } catch (e) { /* skip */ }
        // Check node name for heading patterns
        try {
          var match = headingStyleRegex.exec(node.name);
          if (match) return parseInt(match[1] || match[2], 10);
        } catch (e) { /* skip */ }
        // Infer from font size (common convention)
        try {
          var fs = node.fontSize;
          if (typeof fs === 'number') {
            if (fs >= 40) return 1;
            if (fs >= 32) return 2;
            if (fs >= 24) return 3;
            if (fs >= 20) return 4;
            if (fs >= 18) return 5;
          }
        } catch (e) { /* mixed */ }
        return 0; // Not a heading
      }

      // Tracking for heading hierarchy check (reset per root scan)
      var headingSequence = [];

      // Tracking for reading order check — collect positioned children per parent
      // (checked after tree walk per-frame)

      // ---- Resolve active rules ----
      var requestedRules = msg.rules || ['all'];
      var activeRuleSet = {};
      for (var ri = 0; ri < requestedRules.length; ri++) {
        var ruleOrGroup = requestedRules[ri];
        if (ruleGroups[ruleOrGroup]) {
          var groupRules = ruleGroups[ruleOrGroup];
          for (var gi = 0; gi < groupRules.length; gi++) {
            activeRuleSet[groupRules[gi]] = true;
          }
        } else if (severityMap[ruleOrGroup]) {
          activeRuleSet[ruleOrGroup] = true;
        }
      }

      var maxDepth = typeof msg.maxDepth === 'number' ? msg.maxDepth : 10;
      var maxFindings = typeof msg.maxFindings === 'number' ? msg.maxFindings : 100;

      // ---- Resolve root node ----
      var rootNode;
      if (msg.nodeId) {
        rootNode = await figma.getNodeByIdAsync(msg.nodeId);
        if (!rootNode) {
          throw new Error('Node not found: ' + msg.nodeId);
        }
      } else {
        rootNode = figma.currentPage;
      }

      // ---- Collect context (styles and variables for design-system rules) ----
      var paintStyleIds = {};
      var textStyleIds = {};
      var variableIds = {};

      if (activeRuleSet['hardcoded-color'] || activeRuleSet['no-text-style']) {
        try {
          var paintStyles = await figma.getLocalPaintStylesAsync();
          for (var pi = 0; pi < paintStyles.length; pi++) {
            paintStyleIds[paintStyles[pi].id] = true;
          }
        } catch (e) { /* ignore */ }

        try {
          var textStyles = await figma.getLocalTextStylesAsync();
          for (var ti = 0; ti < textStyles.length; ti++) {
            textStyleIds[textStyles[ti].id] = true;
          }
        } catch (e) { /* ignore */ }

        try {
          var localVars = await figma.variables.getLocalVariablesAsync();
          for (var vi = 0; vi < localVars.length; vi++) {
            variableIds[localVars[vi].id] = true;
          }
        } catch (e) { /* ignore */ }
      }

      // ---- Findings storage ----
      var findings = {};
      for (var ai = 0; ai < allRuleIds.length; ai++) {
        if (activeRuleSet[allRuleIds[ai]]) {
          findings[allRuleIds[ai]] = [];
        }
      }
      var totalFindings = 0;
      var nodesScanned = 0;
      var truncated = false;

      // ---- Tree walk ----
      function walkNode(node, depth) {
        if (depth > maxDepth) return;
        if (truncated) return;

        nodesScanned++;

        var nodeType, nodeName, nodeId;
        try {
          nodeType = node.type;
          nodeName = node.name;
          nodeId = node.id;
        } catch (e) {
          return; // Slot sublayer — skip entirely
        }

        // Skip pages for most checks but still recurse into their children
        var isPage = nodeType === 'PAGE';
        var isSection = nodeType === 'SECTION';

        // ---- WCAG checks ----

        // wcag-contrast: TEXT nodes
        if (activeRuleSet['wcag-contrast'] && nodeType === 'TEXT' && !truncated) {
          try {
            var fills = node.fills;
            if (fills && fills.length > 0) {
              for (var fci = 0; fci < fills.length; fci++) {
                if (fills[fci].type === 'SOLID' && fills[fci].visible !== false) {
                  var fg = fills[fci].color;
                  var bg = lintGetEffectiveBg(node);
                  var ratio = lintContrastRatio(fg.r, fg.g, fg.b, bg.r, bg.g, bg.b);
                  var fontSize = 16;
                  var fontWeight = null;
                  try { fontSize = node.fontSize; } catch (e) { /* mixed */ }
                  try { fontWeight = node.fontWeight; } catch (e) { /* mixed */ }
                  if (typeof fontSize !== 'number') fontSize = 16;
                  var isLarge = lintIsLargeText(fontSize, fontWeight);
                  var required = isLarge ? 3.0 : 4.5;
                  var fgOpacity = (fills[fci].opacity !== undefined) ? fills[fci].opacity : 1;
                  var approximate = fgOpacity < 1 || bg.opacity < 1;
                  if (ratio < required) {
                    if (totalFindings < maxFindings) {
                      var finding = {
                        id: nodeId,
                        name: nodeName,
                        ratio: ratio.toFixed(1) + ':1',
                        required: required.toFixed(1) + ':1',
                        fg: lintRgbToHex(fg.r, fg.g, fg.b),
                        bg: lintRgbToHex(bg.r, bg.g, bg.b)
                      };
                      if (approximate) finding.approximate = true;
                      findings['wcag-contrast'].push(finding);
                      totalFindings++;
                    } else {
                      truncated = true;
                    }
                  }
                  break; // Only check the first visible solid fill
                }
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // wcag-text-size: TEXT nodes with fontSize < 12
        if (activeRuleSet['wcag-text-size'] && nodeType === 'TEXT' && !truncated) {
          try {
            var ts = node.fontSize;
            if (typeof ts === 'number' && ts < 12) {
              if (totalFindings < maxFindings) {
                findings['wcag-text-size'].push({
                  id: nodeId,
                  name: nodeName,
                  fontSize: ts
                });
                totalFindings++;
              } else {
                truncated = true;
              }
            }
          } catch (e) { /* slot sublayer or mixed */ }
        }

        // wcag-target-size: Interactive elements < 24x24
        if (activeRuleSet['wcag-target-size'] && !isPage && !isSection && !truncated) {
          try {
            if ((nodeType === 'FRAME' || nodeType === 'COMPONENT' || nodeType === 'INSTANCE' || nodeType === 'COMPONENT_SET') && interactiveNameRegex.test(nodeName)) {
              var tw = node.width;
              var th = node.height;
              if ((typeof tw === 'number' && tw < 24) || (typeof th === 'number' && th < 24)) {
                if (totalFindings < maxFindings) {
                  findings['wcag-target-size'].push({
                    id: nodeId,
                    name: nodeName,
                    width: tw,
                    height: th
                  });
                  totalFindings++;
                } else {
                  truncated = true;
                }
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // wcag-line-height: TEXT nodes where lineHeight < 1.5 * fontSize
        if (activeRuleSet['wcag-line-height'] && nodeType === 'TEXT' && !truncated) {
          try {
            var lh = node.lineHeight;
            var fs = node.fontSize;
            var effectiveLh = null;
            if (lh && typeof fs === 'number' && typeof lh === 'object' && typeof lh.value === 'number') {
              if (lh.unit === 'PIXELS') {
                effectiveLh = lh.value;
              } else if (lh.unit === 'PERCENT') {
                effectiveLh = fs * (lh.value / 100);
              }
            }
            if (effectiveLh !== null && effectiveLh < 1.5 * fs) {
              if (totalFindings < maxFindings) {
                findings['wcag-line-height'].push({
                  id: nodeId,
                  name: nodeName,
                  lineHeight: effectiveLh,
                  fontSize: fs,
                  recommended: (1.5 * fs).toFixed(1)
                });
                totalFindings++;
              } else {
                truncated = true;
              }
            }
          } catch (e) { /* slot sublayer or mixed */ }
        }

        // wcag-non-text-contrast: UI components need 3:1 against adjacent color (WCAG 1.4.11)
        if (activeRuleSet['wcag-non-text-contrast'] && !isPage && !isSection && !truncated) {
          try {
            if ((nodeType === 'FRAME' || nodeType === 'COMPONENT' || nodeType === 'INSTANCE' || nodeType === 'COMPONENT_SET') && interactiveNameRegex.test(nodeName)) {
              var uiFill = lintGetNodeFillColor(node);
              var uiStroke = lintGetNodeStrokeColor(node);
              var uiBg = lintGetEffectiveBg(node);
              // Check fill against background
              if (uiFill) {
                var uiRatio = lintContrastRatio(uiFill.r, uiFill.g, uiFill.b, uiBg.r, uiBg.g, uiBg.b);
                if (uiRatio < 3.0) {
                  if (totalFindings < maxFindings) {
                    findings['wcag-non-text-contrast'].push({
                      id: nodeId,
                      name: nodeName,
                      ratio: uiRatio.toFixed(1) + ':1',
                      required: '3.0:1',
                      component: lintRgbToHex(uiFill.r, uiFill.g, uiFill.b),
                      bg: lintRgbToHex(uiBg.r, uiBg.g, uiBg.b),
                      element: 'fill'
                    });
                    totalFindings++;
                  } else { truncated = true; }
                }
              }
              // Check stroke/border against background
              if (uiStroke && !truncated) {
                var strokeRatio = lintContrastRatio(uiStroke.r, uiStroke.g, uiStroke.b, uiBg.r, uiBg.g, uiBg.b);
                if (strokeRatio < 3.0) {
                  if (totalFindings < maxFindings) {
                    findings['wcag-non-text-contrast'].push({
                      id: nodeId,
                      name: nodeName,
                      ratio: strokeRatio.toFixed(1) + ':1',
                      required: '3.0:1',
                      component: lintRgbToHex(uiStroke.r, uiStroke.g, uiStroke.b),
                      bg: lintRgbToHex(uiBg.r, uiBg.g, uiBg.b),
                      element: 'stroke'
                    });
                    totalFindings++;
                  } else { truncated = true; }
                }
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // wcag-color-only: Component variants that differ only by color (WCAG 1.4.1)
        if (activeRuleSet['wcag-color-only'] && nodeType === 'COMPONENT_SET' && !truncated) {
          try {
            var variants = node.children;
            if (variants && variants.length >= 2) {
              // Compare each variant pair for color-only differentiation
              for (var vi = 0; vi < variants.length && !truncated; vi++) {
                var variant = variants[vi];
                try {
                  var vName = variant.name.toLowerCase();
                  // Only check state-related variants (error, warning, success, disabled)
                  if (/(error|warning|danger|success|invalid|alert)/.test(vName)) {
                    if (!lintHasNonColorIndicator(variant)) {
                      // Check if this variant's fill differs from default
                      var defaultVariant = null;
                      for (var dvi = 0; dvi < variants.length; dvi++) {
                        var dvName = variants[dvi].name.toLowerCase();
                        if (/(default|rest|idle|normal|base)/.test(dvName) || dvi === 0) {
                          defaultVariant = variants[dvi];
                          break;
                        }
                      }
                      if (defaultVariant) {
                        var varFill = lintGetNodeFillColor(variant);
                        var defFill = lintGetNodeFillColor(defaultVariant);
                        if (varFill && defFill && (varFill.r !== defFill.r || varFill.g !== defFill.g || varFill.b !== defFill.b)) {
                          if (totalFindings < maxFindings) {
                            findings['wcag-color-only'].push({
                              id: variant.id,
                              name: nodeName + ' / ' + variant.name,
                              variantColor: lintRgbToHex(varFill.r, varFill.g, varFill.b),
                              defaultColor: lintRgbToHex(defFill.r, defFill.g, defFill.b),
                              suggestion: 'Add an icon, text label, or border to differentiate this state beyond color alone'
                            });
                            totalFindings++;
                          } else { truncated = true; }
                        }
                      }
                    }
                  }
                } catch (e) { /* skip variant */ }
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // wcag-focus-indicator: Interactive components missing focus variant (WCAG 2.4.7)
        if (activeRuleSet['wcag-focus-indicator'] && nodeType === 'COMPONENT_SET' && !truncated) {
          try {
            if (interactiveNameRegex.test(nodeName)) {
              var hasFocusVariant = false;
              var focusVariantNode = null;
              var csVariants = node.children;
              if (csVariants) {
                for (var fvi = 0; fvi < csVariants.length; fvi++) {
                  var fvName = csVariants[fvi].name.toLowerCase();
                  if (/focus|focused/.test(fvName)) {
                    hasFocusVariant = true;
                    focusVariantNode = csVariants[fvi];
                    break;
                  }
                }
              }
              if (!hasFocusVariant) {
                if (totalFindings < maxFindings) {
                  findings['wcag-focus-indicator'].push({
                    id: nodeId,
                    name: nodeName,
                    issue: 'missing-variant',
                    suggestion: 'Add a focus/focused variant with a visible focus ring or outline'
                  });
                  totalFindings++;
                } else { truncated = true; }
              } else if (focusVariantNode) {
                // Check if focus variant has a visible stroke/border (focus ring)
                var focusStroke = lintGetNodeStrokeColor(focusVariantNode);
                var hasFocusEffect = false;
                try {
                  var effects = focusVariantNode.effects;
                  if (effects) {
                    for (var ei = 0; ei < effects.length; ei++) {
                      if (effects[ei].visible !== false && (effects[ei].type === 'DROP_SHADOW' || effects[ei].type === 'INNER_SHADOW')) {
                        hasFocusEffect = true;
                        break;
                      }
                    }
                  }
                } catch (e) { /* skip */ }
                if (!focusStroke && !hasFocusEffect) {
                  if (totalFindings < maxFindings) {
                    findings['wcag-focus-indicator'].push({
                      id: focusVariantNode.id,
                      name: nodeName + ' / ' + focusVariantNode.name,
                      issue: 'no-visible-indicator',
                      suggestion: 'Focus variant exists but has no visible border, outline, or shadow for the focus indicator'
                    });
                    totalFindings++;
                  } else { truncated = true; }
                }
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // wcag-letter-spacing: Negative letter spacing (WCAG 1.4.12)
        if (activeRuleSet['wcag-letter-spacing'] && nodeType === 'TEXT' && !truncated) {
          try {
            var ls = node.letterSpacing;
            if (ls && typeof ls === 'object' && typeof ls.value === 'number') {
              if (ls.unit === 'PIXELS' && ls.value < 0) {
                if (totalFindings < maxFindings) {
                  findings['wcag-letter-spacing'].push({
                    id: nodeId,
                    name: nodeName,
                    letterSpacing: ls.value + 'px'
                  });
                  totalFindings++;
                } else { truncated = true; }
              } else if (ls.unit === 'PERCENT' && ls.value < 0) {
                if (totalFindings < maxFindings) {
                  findings['wcag-letter-spacing'].push({
                    id: nodeId,
                    name: nodeName,
                    letterSpacing: ls.value + '%'
                  });
                  totalFindings++;
                } else { truncated = true; }
              }
            }
          } catch (e) { /* slot sublayer or mixed */ }
        }

        // wcag-paragraph-spacing: Paragraph spacing < 2x font size (WCAG 1.4.12)
        if (activeRuleSet['wcag-paragraph-spacing'] && nodeType === 'TEXT' && !truncated) {
          try {
            var ps = node.paragraphSpacing;
            var psFs = node.fontSize;
            if (typeof ps === 'number' && typeof psFs === 'number' && ps > 0) {
              var requiredPs = 2 * psFs;
              if (ps < requiredPs) {
                if (totalFindings < maxFindings) {
                  findings['wcag-paragraph-spacing'].push({
                    id: nodeId,
                    name: nodeName,
                    paragraphSpacing: ps,
                    fontSize: psFs,
                    recommended: requiredPs
                  });
                  totalFindings++;
                } else { truncated = true; }
              }
            }
          } catch (e) { /* slot sublayer or mixed */ }
        }

        // wcag-image-alt: Image fills without description (WCAG 1.1.1)
        if (activeRuleSet['wcag-image-alt'] && !isPage && !isSection && !truncated) {
          try {
            var hasImageFill = false;
            var imgFills = node.fills;
            if (imgFills && imgFills.length > 0) {
              for (var ifi = 0; ifi < imgFills.length; ifi++) {
                if (imgFills[ifi].type === 'IMAGE' && imgFills[ifi].visible !== false) {
                  hasImageFill = true;
                  break;
                }
              }
            }
            if (hasImageFill) {
              var hasDescription = false;
              try {
                if (node.description && node.description.trim().length > 0) {
                  hasDescription = true;
                }
              } catch (e) { /* skip */ }
              // Also check if node name indicates decorative
              var isDecorative = false;
              try {
                var lowerName = nodeName.toLowerCase();
                if (lowerName === 'decorative' || lowerName === 'decoration' || lowerName.indexOf('decorative') !== -1) {
                  isDecorative = true;
                }
              } catch (e) { /* skip */ }
              if (!hasDescription && !isDecorative) {
                if (totalFindings < maxFindings) {
                  findings['wcag-image-alt'].push({
                    id: nodeId,
                    name: nodeName,
                    suggestion: 'Add a description in the node\'s description field, or name it "decorative" if purely presentational'
                  });
                  totalFindings++;
                } else { truncated = true; }
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // wcag-heading-hierarchy: Track heading levels for hierarchy check (WCAG 1.3.1)
        if (activeRuleSet['wcag-heading-hierarchy'] && nodeType === 'TEXT' && !truncated) {
          try {
            var hlevel = lintGetHeadingLevel(node);
            if (hlevel > 0) {
              headingSequence.push({ level: hlevel, id: nodeId, name: nodeName });
            }
          } catch (e) { /* skip */ }
        }

        // wcag-reflow: Fixed-position frames without auto-layout (WCAG 1.4.10)
        if (activeRuleSet['wcag-reflow'] && !isPage && !isSection && !truncated) {
          try {
            if ((nodeType === 'FRAME' || nodeType === 'COMPONENT') && node.children && node.children.length >= 3) {
              var rlLayoutMode = 'NONE';
              try { rlLayoutMode = node.layoutMode; } catch (e) { /* skip */ }
              if (!rlLayoutMode || rlLayoutMode === 'NONE') {
                // Check if children use absolute positioning (different x/y values)
                var hasAbsoluteChildren = false;
                var childXs = [];
                var childYs = [];
                for (var rci = 0; rci < Math.min(node.children.length, 10); rci++) {
                  try {
                    childXs.push(node.children[rci].x);
                    childYs.push(node.children[rci].y);
                  } catch (e) { /* skip */ }
                }
                if (childXs.length >= 3) {
                  // If children are spread across both axes, it's likely absolute positioning
                  var uniqueXs = [];
                  var uniqueYs = [];
                  for (var uxi = 0; uxi < childXs.length; uxi++) {
                    if (uniqueXs.indexOf(childXs[uxi]) === -1) uniqueXs.push(childXs[uxi]);
                    if (uniqueYs.indexOf(childYs[uxi]) === -1) uniqueYs.push(childYs[uxi]);
                  }
                  hasAbsoluteChildren = uniqueXs.length > 2 && uniqueYs.length > 2;
                }
                if (hasAbsoluteChildren) {
                  if (totalFindings < maxFindings) {
                    findings['wcag-reflow'].push({
                      id: nodeId,
                      name: nodeName,
                      childCount: node.children.length,
                      suggestion: 'Convert to auto-layout so content can reflow at different viewport sizes'
                    });
                    totalFindings++;
                  } else { truncated = true; }
                }
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // wcag-reading-order: Check if visual order matches layer order (WCAG 1.3.2)
        if (activeRuleSet['wcag-reading-order'] && !isPage && !isSection && !truncated) {
          try {
            if ((nodeType === 'FRAME' || nodeType === 'COMPONENT' || nodeType === 'INSTANCE') && node.children && node.children.length >= 2) {
              var roLayoutMode = 'NONE';
              try { roLayoutMode = node.layoutMode; } catch (e) { /* skip */ }
              // Only check non-auto-layout frames (auto-layout enforces order)
              if (!roLayoutMode || roLayoutMode === 'NONE') {
                // Collect children y positions (for vertical reading order)
                var childPositions = [];
                for (var roi = 0; roi < node.children.length; roi++) {
                  try {
                    childPositions.push({
                      index: roi,
                      y: node.children[roi].y,
                      x: node.children[roi].x,
                      name: node.children[roi].name
                    });
                  } catch (e) { /* skip */ }
                }
                if (childPositions.length >= 2) {
                  // Sort by visual position (top-to-bottom, left-to-right)
                  var visualOrder = childPositions.slice().sort(function(a, b) {
                    if (Math.abs(a.y - b.y) > 10) return a.y - b.y; // Different row
                    return a.x - b.x; // Same row, left to right
                  });
                  // Check if layer order matches visual order
                  var orderMismatches = 0;
                  for (var omi = 0; omi < visualOrder.length; omi++) {
                    if (visualOrder[omi].index !== omi) {
                      orderMismatches++;
                    }
                  }
                  // Flag if more than 30% of elements are out of order
                  if (orderMismatches > childPositions.length * 0.3 && orderMismatches >= 2) {
                    if (totalFindings < maxFindings) {
                      findings['wcag-reading-order'].push({
                        id: nodeId,
                        name: nodeName,
                        childCount: childPositions.length,
                        mismatches: orderMismatches,
                        suggestion: 'Reorder layers to match visual top-to-bottom, left-to-right reading order'
                      });
                      totalFindings++;
                    } else { truncated = true; }
                  }
                }
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // wcag-disabled-no-context: Disabled variant without tooltip/helper text (Isabella's pattern)
        if (activeRuleSet['wcag-disabled-no-context'] && nodeType === 'COMPONENT_SET' && !truncated) {
          try {
            var csChildren = node.children;
            if (csChildren) {
              for (var dci = 0; dci < csChildren.length && !truncated; dci++) {
                var dcVariant = csChildren[dci];
                try {
                  var dcName = dcVariant.name || '';
                  if (/(disabled|inactive)/i.test(dcName)) {
                    // Check if disabled variant has tooltip, helper text, or descriptive child
                    var hasContextChild = false;
                    try {
                      if (dcVariant.children) {
                        for (var dcci = 0; dcci < dcVariant.children.length; dcci++) {
                          var dcChild = dcVariant.children[dcci];
                          try {
                            var dcChildName = (dcChild.name || '').toLowerCase();
                            // Look for tooltip, helper text, hint, description, or error message children
                            if (/tooltip|helper|hint|description|message|caption|note|info|why|reason/i.test(dcChildName)) {
                              hasContextChild = true;
                              break;
                            }
                            // Recurse one level for nested tooltip/helper
                            if (dcChild.children) {
                              for (var dcgci = 0; dcgci < dcChild.children.length; dcgci++) {
                                var dcGrandchild = dcChild.children[dcgci];
                                try {
                                  if (/tooltip|helper|hint|description|message/i.test(dcGrandchild.name || '')) {
                                    hasContextChild = true;
                                    break;
                                  }
                                } catch (e) { /* skip */ }
                              }
                              if (hasContextChild) break;
                            }
                          } catch (e) { /* skip */ }
                        }
                      }
                    } catch (e) { /* skip */ }
                    // Also check component description for disabled guidance
                    var hasDisabledAnnotation = false;
                    try {
                      var csDesc = (node.description || '').toLowerCase();
                      if (/disabled.*tooltip|disabled.*helper|disabled.*hint|aria-disabled|why.*disabled|disabled.*reason/i.test(csDesc)) {
                        hasDisabledAnnotation = true;
                      }
                    } catch (e) { /* skip */ }
                    if (!hasContextChild && !hasDisabledAnnotation) {
                      if (totalFindings < maxFindings) {
                        findings['wcag-disabled-no-context'].push({
                          id: dcVariant.id,
                          name: nodeName + ' / ' + dcVariant.name,
                          suggestion: 'Disabled elements should remain focusable (use aria-disabled, not HTML disabled). Add a tooltip or helper text explaining why the element is disabled so screen reader users understand the context.'
                        });
                        totalFindings++;
                      } else { truncated = true; }
                    }
                  }
                } catch (e) { /* skip variant */ }
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // ---- Design System checks ----

        // token-misuse: Variable name prefix doesn't match usage context
        if (activeRuleSet['token-misuse'] && !isPage && !isSection && !truncated) {
          try {
            var tmFills = node.fills;
            if (tmFills && tmFills.length > 0) {
              for (var tmi = 0; tmi < tmFills.length; tmi++) {
                var tmFill = tmFills[tmi];
                if (tmFill.type === 'SOLID' && tmFill.visible !== false) {
                  try {
                    if (tmFill.boundVariables && tmFill.boundVariables.color) {
                      var tmVarId = tmFill.boundVariables.color.id;
                      // Resolve variable name
                      try {
                        var tmVar = figma.variables.getVariableById(tmVarId);
                        if (tmVar) {
                          var tmVarName = tmVar.name.toLowerCase();
                          var isBgToken = /^(bg|background|surface|fill)[\/-]/.test(tmVarName);
                          var isTextNode = nodeType === 'TEXT';
                          // Flag: bg/surface token used as text fill
                          if (isTextNode && isBgToken) {
                            if (totalFindings < maxFindings) {
                              findings['token-misuse'].push({
                                id: nodeId,
                                name: nodeName,
                                variable: tmVar.name,
                                usage: 'text fill',
                                expectedPrefix: 'text/*, fg/*, foreground/*',
                                suggestion: 'This text node uses a background/surface token ("' + tmVar.name + '") as its fill color. This is likely a misbound token — use a text/foreground token instead.'
                              });
                              totalFindings++;
                            } else { truncated = true; }
                          }
                          // Flag: text/foreground token used as frame/shape background
                          var isTextToken = /^(text|fg|foreground|font)[\/-]/.test(tmVarName);
                          var isContainerNode = nodeType === 'FRAME' || nodeType === 'COMPONENT' || nodeType === 'INSTANCE' || nodeType === 'RECTANGLE';
                          if (isContainerNode && isTextToken && !truncated) {
                            if (totalFindings < maxFindings) {
                              findings['token-misuse'].push({
                                id: nodeId,
                                name: nodeName,
                                variable: tmVar.name,
                                usage: 'background fill',
                                expectedPrefix: 'bg/*, background/*, surface/*',
                                suggestion: 'This container uses a text/foreground token ("' + tmVar.name + '") as its background fill. This is likely a misbound token — use a background/surface token instead.'
                              });
                              totalFindings++;
                            } else { truncated = true; }
                          }
                        }
                      } catch (e) { /* can't resolve variable */ }
                    }
                  } catch (e) { /* no bound vars */ }
                }
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // hardcoded-color: Solid fills without variable binding or style
        if (activeRuleSet['hardcoded-color'] && !isPage && !isSection && !truncated) {
          try {
            var hcFills = node.fills;
            if (hcFills && hcFills.length > 0) {
              var hasFillStyle = false;
              try {
                hasFillStyle = node.fillStyleId && node.fillStyleId !== '';
              } catch (e) { /* mixed fill styles */ }

              if (!hasFillStyle) {
                for (var hci = 0; hci < hcFills.length; hci++) {
                  var hcFill = hcFills[hci];
                  if (hcFill.type === 'SOLID' && hcFill.visible !== false) {
                    var hasBoundVar = false;
                    try {
                      if (hcFill.boundVariables && hcFill.boundVariables.color) {
                        hasBoundVar = true;
                      }
                    } catch (e) { /* no bound vars */ }

                    if (!hasBoundVar) {
                      if (totalFindings < maxFindings) {
                        findings['hardcoded-color'].push({
                          id: nodeId,
                          name: nodeName,
                          color: lintRgbToHex(hcFill.color.r, hcFill.color.g, hcFill.color.b)
                        });
                        totalFindings++;
                      } else {
                        truncated = true;
                      }
                      break; // One finding per node
                    }
                  }
                }
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // no-text-style: TEXT nodes without textStyleId
        if (activeRuleSet['no-text-style'] && nodeType === 'TEXT' && !truncated) {
          try {
            var hasTextStyle = node.textStyleId && node.textStyleId !== '';
            if (!hasTextStyle) {
              if (totalFindings < maxFindings) {
                findings['no-text-style'].push({
                  id: nodeId,
                  name: nodeName
                });
                totalFindings++;
              } else {
                truncated = true;
              }
            }
          } catch (e) { /* slot sublayer or mixed */ }
        }

        // default-name: Nodes with default Figma names
        if (activeRuleSet['default-name'] && !isPage && !truncated) {
          try {
            if (defaultNameRegex.test(nodeName)) {
              if (totalFindings < maxFindings) {
                findings['default-name'].push({
                  id: nodeId,
                  name: nodeName,
                  type: nodeType
                });
                totalFindings++;
              } else {
                truncated = true;
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // detached-component: Frames with "/" in name but not component/instance
        if (activeRuleSet['detached-component'] && nodeType === 'FRAME' && !truncated) {
          try {
            if (nodeName.indexOf('/') !== -1) {
              if (totalFindings < maxFindings) {
                findings['detached-component'].push({
                  id: nodeId,
                  name: nodeName
                });
                totalFindings++;
              } else {
                truncated = true;
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // ---- Layout checks ----

        // no-autolayout: Frames with 2+ children and no auto-layout
        if (activeRuleSet['no-autolayout'] && !isPage && !isSection && !truncated) {
          try {
            if (nodeType === 'FRAME' || nodeType === 'COMPONENT' || nodeType === 'COMPONENT_SET') {
              var childCount = 0;
              try { childCount = node.children ? node.children.length : 0; } catch (e) { /* skip */ }
              if (childCount >= 2) {
                var layoutMode = 'NONE';
                try { layoutMode = node.layoutMode; } catch (e) { /* skip */ }
                if (!layoutMode || layoutMode === 'NONE') {
                  if (totalFindings < maxFindings) {
                    findings['no-autolayout'].push({
                      id: nodeId,
                      name: nodeName,
                      childCount: childCount
                    });
                    totalFindings++;
                  } else {
                    truncated = true;
                  }
                }
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // empty-container: Frames with zero children
        if (activeRuleSet['empty-container'] && !isPage && !isSection && !truncated) {
          try {
            if (nodeType === 'FRAME') {
              var ec = 0;
              try { ec = node.children ? node.children.length : 0; } catch (e) { /* skip */ }
              if (ec === 0) {
                if (totalFindings < maxFindings) {
                  findings['empty-container'].push({
                    id: nodeId,
                    name: nodeName
                  });
                  totalFindings++;
                } else {
                  truncated = true;
                }
              }
            }
          } catch (e) { /* slot sublayer */ }
        }

        // ---- Recurse into children ----
        try {
          if (node.children) {
            for (var ci = 0; ci < node.children.length; ci++) {
              if (truncated) break;
              walkNode(node.children[ci], depth + 1);
            }
          }
        } catch (e) { /* no children or slot sublayer */ }
      }

      // ---- Execute walk ----
      walkNode(rootNode, 0);

      // ---- Post-walk: heading hierarchy validation ----
      if (activeRuleSet['wcag-heading-hierarchy'] && headingSequence.length >= 2 && !truncated) {
        var prevLevel = 0;
        for (var hi = 0; hi < headingSequence.length; hi++) {
          var h = headingSequence[hi];
          if (prevLevel > 0 && h.level > prevLevel + 1) {
            // Skipped a level (e.g., H1 → H3)
            if (totalFindings < maxFindings) {
              findings['wcag-heading-hierarchy'].push({
                id: h.id,
                name: h.name,
                level: h.level,
                previousLevel: prevLevel,
                suggestion: 'Expected H' + (prevLevel + 1) + ' but found H' + h.level + '. Do not skip heading levels.'
              });
              totalFindings++;
            } else {
              truncated = true;
              break;
            }
          }
          prevLevel = h.level;
        }
      }

      // ---- Build response ----
      var categories = [];
      var summaryObj = { critical: 0, warning: 0, info: 0, total: 0 };

      for (var rk = 0; rk < allRuleIds.length; rk++) {
        var ruleId = allRuleIds[rk];
        if (!findings[ruleId] || findings[ruleId].length === 0) continue;
        var sev = severityMap[ruleId];
        categories.push({
          rule: ruleId,
          severity: sev,
          wcagLevel: wcagLevelMap[ruleId] || null,
          count: findings[ruleId].length,
          description: ruleDescriptions[ruleId],
          nodes: findings[ruleId]
        });
        summaryObj[sev] = (summaryObj[sev] || 0) + findings[ruleId].length;
        summaryObj.total += findings[ruleId].length;
      }

      var responseData = {
        rootNodeId: rootNode.id,
        rootNodeName: rootNode.name,
        nodesScanned: nodesScanned,
        categories: categories,
        summary: summaryObj
      };

      if (truncated) {
        responseData.warning = 'Showing first ' + maxFindings + ' findings...';
      }

      console.log('🌉 [Desktop Bridge] Lint complete: ' + summaryObj.total + ' findings across ' + nodesScanned + ' nodes');

      figma.ui.postMessage({
        type: 'LINT_DESIGN_RESULT',
        requestId: msg.requestId,
        success: true,
        data: responseData
      });

    } catch (error) {
      var errorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Lint design error:', errorMsg);
      figma.ui.postMessage({
        type: 'LINT_DESIGN_RESULT',
        requestId: msg.requestId,
        success: false,
        error: errorMsg
      });
    }
  }

  // ============================================================================
  // AUDIT_COMPONENT_ACCESSIBILITY — Deep accessibility audit for a component set
  // ============================================================================
  else if (msg.type === 'AUDIT_COMPONENT_ACCESSIBILITY') {
    try {
      console.log('🌉 [Desktop Bridge] Running component accessibility audit...');

      // ---- Color science helpers (shared with lint) ----
      function auditLinearize(c) {
        return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
      }
      function auditLuminance(r, g, b) {
        return 0.2126 * auditLinearize(r) + 0.7152 * auditLinearize(g) + 0.0722 * auditLinearize(b);
      }
      function auditContrastRatio(r1, g1, b1, r2, g2, b2) {
        var l1 = auditLuminance(r1, g1, b1);
        var l2 = auditLuminance(r2, g2, b2);
        return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
      }
      function auditRgbToHex(r, g, b) {
        var rr = Math.round(r * 255).toString(16);
        var gg = Math.round(g * 255).toString(16);
        var bb = Math.round(b * 255).toString(16);
        if (rr.length === 1) rr = '0' + rr;
        if (gg.length === 1) gg = '0' + gg;
        if (bb.length === 1) bb = '0' + bb;
        return '#' + rr.toUpperCase() + gg.toUpperCase() + bb.toUpperCase();
      }

      // ---- Color-blind simulation matrices ----
      // Brettel/Vienot transformation matrices for dichromatic vision
      var colorBlindMatrices = {
        protanopia: [
          [0.152286, 1.052583, -0.204868],
          [0.114503, 0.786281, 0.099216],
          [-0.003882, -0.048116, 1.051998]
        ],
        deuteranopia: [
          [0.367322, 0.860646, -0.227968],
          [0.280085, 0.672501, 0.047413],
          [-0.011820, 0.042940, 0.968881]
        ],
        tritanopia: [
          [1.255528, -0.076749, -0.178779],
          [-0.078411, 0.930809, 0.147602],
          [0.004733, 0.691367, 0.303900]
        ]
      };

      function simulateColorBlind(r, g, b, matrix) {
        var nr = Math.max(0, Math.min(1, matrix[0][0] * r + matrix[0][1] * g + matrix[0][2] * b));
        var ng = Math.max(0, Math.min(1, matrix[1][0] * r + matrix[1][1] * g + matrix[1][2] * b));
        var nb = Math.max(0, Math.min(1, matrix[2][0] * r + matrix[2][1] * g + matrix[2][2] * b));
        return { r: nr, g: ng, b: nb };
      }

      // ---- Node inspection helpers ----
      function auditGetFillColor(node) {
        try {
          var fills = node.fills;
          if (fills && fills.length > 0) {
            for (var i = fills.length - 1; i >= 0; i--) {
              if (fills[i].type === 'SOLID' && fills[i].visible !== false) {
                return { r: fills[i].color.r, g: fills[i].color.g, b: fills[i].color.b };
              }
            }
          }
        } catch (e) { /* skip */ }
        return null;
      }

      function auditGetStrokeColor(node) {
        try {
          var strokes = node.strokes;
          if (strokes && strokes.length > 0) {
            for (var i = strokes.length - 1; i >= 0; i--) {
              if (strokes[i].type === 'SOLID' && strokes[i].visible !== false) {
                return { r: strokes[i].color.r, g: strokes[i].color.g, b: strokes[i].color.b };
              }
            }
          }
        } catch (e) { /* skip */ }
        return null;
      }

      function auditGetEffectiveBg(node) {
        var current = node.parent;
        while (current) {
          try {
            if (current.fills && current.fills.length > 0) {
              for (var fi = current.fills.length - 1; fi >= 0; fi--) {
                var fill = current.fills[fi];
                if (fill.type === 'SOLID' && fill.visible !== false) {
                  return { r: fill.color.r, g: fill.color.g, b: fill.color.b };
                }
              }
            }
          } catch (e) { /* skip */ }
          current = current.parent;
        }
        return { r: 1, g: 1, b: 1 };
      }

      function auditHasChildOfType(node, types) {
        try {
          if (!node.children) return false;
          for (var i = 0; i < node.children.length; i++) {
            var child = node.children[i];
            try {
              for (var t = 0; t < types.length; t++) {
                if (child.type === types[t]) return true;
              }
              // Recurse one level deeper for nested icons
              if (child.children) {
                for (var j = 0; j < child.children.length; j++) {
                  try {
                    for (var t2 = 0; t2 < types.length; t2++) {
                      if (child.children[j].type === types[t2]) return true;
                    }
                  } catch (e) { /* skip */ }
                }
              }
            } catch (e) { /* skip */ }
          }
        } catch (e) { /* skip */ }
        return false;
      }

      // Collect all text fill colors and background colors from a variant tree
      function auditCollectColorPairs(node, pairs, depth) {
        if (depth > 5) return;
        try {
          if (node.type === 'TEXT') {
            var fills = node.fills;
            if (fills && fills.length > 0) {
              for (var i = 0; i < fills.length; i++) {
                if (fills[i].type === 'SOLID' && fills[i].visible !== false) {
                  var bg = auditGetEffectiveBg(node);
                  pairs.push({
                    fg: { r: fills[i].color.r, g: fills[i].color.g, b: fills[i].color.b },
                    bg: bg,
                    nodeName: node.name,
                    nodeId: node.id
                  });
                  break;
                }
              }
            }
          }
          if (node.children) {
            for (var ci = 0; ci < node.children.length; ci++) {
              auditCollectColorPairs(node.children[ci], pairs, depth + 1);
            }
          }
        } catch (e) { /* skip */ }
      }

      // ---- Resolve target node ----
      var targetNode;
      if (msg.nodeId) {
        targetNode = await figma.getNodeByIdAsync(msg.nodeId);
        if (!targetNode) {
          throw new Error('Node not found: ' + msg.nodeId);
        }
      } else {
        // Try current selection
        var sel = figma.currentPage.selection;
        if (sel && sel.length === 1) {
          targetNode = sel[0];
        } else {
          throw new Error('No nodeId provided and no single node selected. Select a component set or provide a nodeId.');
        }
      }

      // If user selected a COMPONENT or INSTANCE, walk up to the COMPONENT_SET
      var componentSet = targetNode;
      if (targetNode.type === 'COMPONENT' && targetNode.parent && targetNode.parent.type === 'COMPONENT_SET') {
        componentSet = targetNode.parent;
      } else if (targetNode.type === 'INSTANCE') {
        try {
          var mainComponent = await targetNode.getMainComponentAsync();
          if (mainComponent && mainComponent.parent && mainComponent.parent.type === 'COMPONENT_SET') {
            componentSet = mainComponent.parent;
          } else if (mainComponent && mainComponent.type === 'COMPONENT') {
            componentSet = mainComponent;
          }
        } catch (e) { /* use target as-is */ }
      }

      var isComponentSet = componentSet.type === 'COMPONENT_SET';
      var isComponent = componentSet.type === 'COMPONENT';
      if (!isComponentSet && !isComponent) {
        throw new Error('Node "' + componentSet.name + '" is type ' + componentSet.type + '. Expected COMPONENT_SET or COMPONENT.');
      }

      // ---- Analyze variants ----
      var variants = isComponentSet ? componentSet.children : [componentSet];
      var variantCount = variants.length;

      // ---- Classify component as interactive vs presentational ----
      var interactiveNames = /^(button|link|input|checkbox|radio|switch|toggle|tab|select|slider|dropdown|menu-item|search|combobox|listbox)/i;
      var presentationalNames = /^(alert|badge|card|avatar|divider|skeleton|tooltip|tag|chip|banner|callout|notification|toast|icon|image|separator|progress|spinner|loader|breadcrumb|label|heading|paragraph|caption|stat|meter|indicator)/i;

      // Parse variant axes from variant names (e.g., "type=success, style=fill" → {type: [...], style: [...]})
      var variantAxes = {};
      var hasStateAxis = false;
      for (var vai = 0; vai < variants.length; vai++) {
        var vParts = variants[vai].name.split(',');
        for (var vpi = 0; vpi < vParts.length; vpi++) {
          var kv = vParts[vpi].trim().split('=');
          if (kv.length === 2) {
            var axisName = kv[0].trim().toLowerCase();
            var axisValue = kv[1].trim().toLowerCase();
            if (!variantAxes[axisName]) variantAxes[axisName] = [];
            if (variantAxes[axisName].indexOf(axisValue) === -1) {
              variantAxes[axisName].push(axisValue);
            }
            // Check if this axis contains interaction state values
            if (axisName === 'state' && /(hover|focus|pressed|disabled|active)/i.test(axisValue)) {
              hasStateAxis = true;
            }
          }
        }
      }

      var componentName = componentSet.name || '';
      var isInteractive = interactiveNames.test(componentName) || hasStateAxis;
      var isPresentational = !isInteractive && (presentationalNames.test(componentName) || !hasStateAxis);
      // If ambiguous, check if any variant mentions interaction states
      if (!isInteractive && !isPresentational) {
        for (var ami = 0; ami < variants.length; ami++) {
          if (/(hover|focus|pressed|disabled)/i.test(variants[ami].name)) {
            isInteractive = true;
            break;
          }
        }
        if (!isInteractive) isPresentational = true;
      }

      // 1. Coverage analysis — adapts to component classification
      var stateKeywords = {
        'default': /(default|rest|idle|normal|base)/i,
        'hover': /(hover|hovered)/i,
        'focus': /(focus|focused)/i,
        'disabled': /(disabled|inactive)/i,
        'error': /(error|invalid|danger)/i,
        'active': /(active|pressed|selected)/i,
        'loading': /(loading|spinner)/i
      };

      var coveredCount = 0;
      var totalStates = 0;
      var missingStates = [];
      var statesCovered = {};
      var statesFound = {};
      var coverageLabel = '';
      var variantAxisCoverage = null;

      if (isInteractive) {
        // Interactive components: check for interaction states
        coverageLabel = 'interactive-states';
        for (var sk in stateKeywords) {
          statesCovered[sk] = false;
          statesFound[sk] = null;
        }
        for (var vi = 0; vi < variants.length; vi++) {
          var vName = variants[vi].name;
          for (var sk2 in stateKeywords) {
            if (stateKeywords[sk2].test(vName)) {
              statesCovered[sk2] = true;
              if (!statesFound[sk2]) statesFound[sk2] = vName;
            }
          }
        }
        if (variantCount === 1 && !statesCovered['default']) {
          statesCovered['default'] = true;
          statesFound['default'] = variants[0].name;
        }
        for (var sk3 in statesCovered) {
          totalStates++;
          if (statesCovered[sk3]) {
            coveredCount++;
          } else {
            missingStates.push(sk3);
          }
        }
      } else {
        // Presentational components: check variant axis completeness
        coverageLabel = 'variant-axes';
        // Calculate expected combinations vs actual
        var axisNames = [];
        var axisCounts = [];
        var expectedCombinations = 1;
        for (var axName in variantAxes) {
          axisNames.push(axName);
          axisCounts.push(variantAxes[axName].length);
          expectedCombinations *= variantAxes[axName].length;
        }
        // Score: actual variants / expected combinations (capped at 100%)
        var axisCoverageRatio = expectedCombinations > 0 ? Math.min(1, variantCount / expectedCombinations) : 1;
        coveredCount = variantCount;
        totalStates = expectedCombinations;
        variantAxisCoverage = {
          axes: variantAxes,
          axisCount: axisNames.length,
          expectedCombinations: expectedCombinations,
          actualVariants: variantCount,
          completeness: Math.round(axisCoverageRatio * 100) + '%'
        };
        // For presentational, no "missing states" — instead note if combinations are incomplete
        if (variantCount < expectedCombinations) {
          missingStates.push(variantCount + '/' + expectedCombinations + ' axis combinations present');
        }
      }

      // 2. Focus indicator quality
      var focusAnalysis = { hasVariant: false, hasVisibleIndicator: false, contrastRatio: null, details: '' };
      if (statesCovered['focus'] && isComponentSet) {
        focusAnalysis.hasVariant = true;
        for (var fvi = 0; fvi < variants.length; fvi++) {
          if (/(focus|focused)/i.test(variants[fvi].name)) {
            var focusNode = variants[fvi];
            // Check for stroke (focus ring)
            var fStroke = auditGetStrokeColor(focusNode);
            if (fStroke) {
              var fBg = auditGetEffectiveBg(focusNode);
              var fRatio = auditContrastRatio(fStroke.r, fStroke.g, fStroke.b, fBg.r, fBg.g, fBg.b);
              focusAnalysis.hasVisibleIndicator = true;
              focusAnalysis.contrastRatio = parseFloat(fRatio.toFixed(1));
              focusAnalysis.details = 'Focus ring (stroke) with ' + fRatio.toFixed(1) + ':1 contrast';
              break;
            }
            // Check for shadow effect
            try {
              var effects = focusNode.effects;
              if (effects) {
                for (var ei = 0; ei < effects.length; ei++) {
                  if (effects[ei].visible !== false && (effects[ei].type === 'DROP_SHADOW' || effects[ei].type === 'INNER_SHADOW')) {
                    focusAnalysis.hasVisibleIndicator = true;
                    focusAnalysis.details = 'Focus indicator via ' + effects[ei].type.toLowerCase().replace('_', ' ');
                    break;
                  }
                }
              }
            } catch (e) { /* skip */ }
            if (!focusAnalysis.hasVisibleIndicator) {
              focusAnalysis.details = 'Focus variant exists but no visible stroke or shadow detected';
            }
            break;
          }
        }
      } else if (!statesCovered['focus']) {
        focusAnalysis.details = 'No focus/focused variant found';
      }

      // 3. Non-color differentiation for status states
      var colorDifferentiation = { issues: [], checked: 0 };
      if (isComponentSet) {
        var statusStates = ['error', 'disabled', 'active'];
        var defaultVariant = null;
        for (var dvi = 0; dvi < variants.length; dvi++) {
          if (/(default|rest|idle|normal|base)/i.test(variants[dvi].name) || dvi === 0) {
            defaultVariant = variants[dvi];
            break;
          }
        }
        for (var ssi = 0; ssi < statusStates.length; ssi++) {
          var stateName = statusStates[ssi];
          if (statesCovered[stateName]) {
            colorDifferentiation.checked++;
            for (var svi = 0; svi < variants.length; svi++) {
              if (stateKeywords[stateName].test(variants[svi].name)) {
                var stateVariant = variants[svi];
                var stateFill = auditGetFillColor(stateVariant);
                var defaultFill = defaultVariant ? auditGetFillColor(defaultVariant) : null;
                var hasIcon = auditHasChildOfType(stateVariant, ['VECTOR', 'BOOLEAN_OPERATION', 'INSTANCE']);
                var hasStroke = auditGetStrokeColor(stateVariant) !== null;
                var colorDiffers = stateFill && defaultFill && (stateFill.r !== defaultFill.r || stateFill.g !== defaultFill.g || stateFill.b !== defaultFill.b);
                if (colorDiffers && !hasIcon && !hasStroke) {
                  colorDifferentiation.issues.push({
                    variant: stateVariant.name,
                    state: stateName,
                    variantColor: stateFill ? auditRgbToHex(stateFill.r, stateFill.g, stateFill.b) : null,
                    defaultColor: defaultFill ? auditRgbToHex(defaultFill.r, defaultFill.g, defaultFill.b) : null,
                    suggestion: 'Add icon, border, or text indicator beyond color'
                  });
                }
                break;
              }
            }
          }
        }
      }

      // 4. Target size analysis
      // WCAG 2.5.8 applies to interactive targets — presentational components
      // (badges, avatars, progress bars) are not tap targets by definition.
      // Skip target size checking for presentational components to avoid false positives.
      var targetSizeAnalysis = { minWidth: Infinity, minHeight: Infinity, variants: [], issues: [] };
      var minTarget = msg.targetSize || 24; // Default WCAG 2.5.8 minimum
      for (var tvi = 0; tvi < variants.length; tvi++) {
        try {
          var tw = variants[tvi].width;
          var th = variants[tvi].height;
          if (typeof tw === 'number' && typeof th === 'number') {
            targetSizeAnalysis.variants.push({ name: variants[tvi].name, width: tw, height: th });
            if (tw < targetSizeAnalysis.minWidth) targetSizeAnalysis.minWidth = tw;
            if (th < targetSizeAnalysis.minHeight) targetSizeAnalysis.minHeight = th;
            // Only flag target size issues for interactive components
            if (isInteractive && (tw < minTarget || th < minTarget)) {
              targetSizeAnalysis.issues.push({
                variant: variants[tvi].name,
                width: tw,
                height: th,
                required: minTarget + 'x' + minTarget
              });
            }
          }
        } catch (e) { /* skip */ }
      }
      if (targetSizeAnalysis.minWidth === Infinity) targetSizeAnalysis.minWidth = null;
      if (targetSizeAnalysis.minHeight === Infinity) targetSizeAnalysis.minHeight = null;

      // 5. Annotation completeness
      var annotations = { hasDescription: false, description: '', hasA11yNotes: false, a11yNotes: '' };
      try {
        var desc = componentSet.description || '';
        if (desc.trim().length > 0) {
          annotations.hasDescription = true;
          annotations.description = desc.substring(0, 200);
        }
        if (/aria|accessibility|a11y|screen.?reader|keyboard|role|tab.?order/i.test(desc)) {
          annotations.hasA11yNotes = true;
          // Extract just the a11y-relevant section
          var lines = desc.split('\n');
          var a11yLines = [];
          for (var ali = 0; ali < lines.length; ali++) {
            if (/aria|accessibility|a11y|screen.?reader|keyboard|role|tab.?order/i.test(lines[ali])) {
              a11yLines.push(lines[ali].trim());
            }
          }
          annotations.a11yNotes = a11yLines.join('; ').substring(0, 300);
        }
      } catch (e) { /* skip */ }

      // 6. Color-blind simulation
      var colorBlindAnalysis = { simulations: [], issues: [] };
      // Collect all text/bg color pairs from the default variant (or first variant)
      var sampleVariant = defaultVariant || variants[0];
      var colorPairs = [];
      auditCollectColorPairs(sampleVariant, colorPairs, 0);

      // Also check component fill against its background
      var compFill = auditGetFillColor(sampleVariant);
      var compBg = auditGetEffectiveBg(sampleVariant);
      if (compFill) {
        colorPairs.push({ fg: compFill, bg: compBg, nodeName: sampleVariant.name + ' (fill)', nodeId: sampleVariant.id });
      }

      var cbTypes = ['protanopia', 'deuteranopia', 'tritanopia'];
      for (var cbi = 0; cbi < cbTypes.length; cbi++) {
        var cbType = cbTypes[cbi];
        var matrix = colorBlindMatrices[cbType];
        var failingPairs = [];
        for (var cpi = 0; cpi < Math.min(colorPairs.length, 20); cpi++) {
          var pair = colorPairs[cpi];
          var simFg = simulateColorBlind(pair.fg.r, pair.fg.g, pair.fg.b, matrix);
          var simBg = simulateColorBlind(pair.bg.r, pair.bg.g, pair.bg.b, matrix);
          var originalRatio = auditContrastRatio(pair.fg.r, pair.fg.g, pair.fg.b, pair.bg.r, pair.bg.g, pair.bg.b);
          var simRatio = auditContrastRatio(simFg.r, simFg.g, simFg.b, simBg.r, simBg.g, simBg.b);
          // Flag if simulated contrast drops below 4.5:1 (AA) when original was passing
          if (originalRatio >= 4.5 && simRatio < 4.5) {
            failingPairs.push({
              nodeName: pair.nodeName,
              originalRatio: parseFloat(originalRatio.toFixed(1)),
              simulatedRatio: parseFloat(simRatio.toFixed(1)),
              originalFg: auditRgbToHex(pair.fg.r, pair.fg.g, pair.fg.b),
              simulatedFg: auditRgbToHex(simFg.r, simFg.g, simFg.b)
            });
          }
          // Also flag if contrast drops significantly (>30% reduction) even if still passing
          if (originalRatio >= 4.5 && simRatio >= 4.5 && (simRatio / originalRatio) < 0.7) {
            failingPairs.push({
              nodeName: pair.nodeName,
              originalRatio: parseFloat(originalRatio.toFixed(1)),
              simulatedRatio: parseFloat(simRatio.toFixed(1)),
              originalFg: auditRgbToHex(pair.fg.r, pair.fg.g, pair.fg.b),
              simulatedFg: auditRgbToHex(simFg.r, simFg.g, simFg.b),
              note: 'Significant contrast reduction (>30%)'
            });
          }
        }
        colorBlindAnalysis.simulations.push({
          type: cbType,
          pairsChecked: Math.min(colorPairs.length, 20),
          issues: failingPairs.length,
          details: failingPairs
        });
        if (failingPairs.length > 0) {
          colorBlindAnalysis.issues.push(cbType + ': ' + failingPairs.length + ' color pair(s) lose sufficient contrast');
        }
      }

      // ---- Compute overall score ----
      var scores = {};
      // Coverage: percentage of states (interactive) or axis combinations (presentational)
      scores.variantCoverage = totalStates > 0 ? Math.round((coveredCount / totalStates) * 100) : 100;
      // Focus indicator: 0 (missing), 50 (exists but no indicator), 100 (good indicator)
      // For presentational: N/A → score 100 (don't penalize)
      scores.focusIndicator = isPresentational ? 100 : (!focusAnalysis.hasVariant ? 0 : (!focusAnalysis.hasVisibleIndicator ? 50 : 100));
      // Color differentiation: 100 if no issues, decremented per issue
      scores.colorDifferentiation = colorDifferentiation.checked === 0 ? 100 : Math.max(0, Math.round(((colorDifferentiation.checked - colorDifferentiation.issues.length) / colorDifferentiation.checked) * 100));
      // Target size: N/A for presentational (not tap targets), scored for interactive
      scores.targetSize = isPresentational ? 100 : (targetSizeAnalysis.issues.length === 0 ? 100 : Math.max(0, Math.round(((targetSizeAnalysis.variants.length - targetSizeAnalysis.issues.length) / Math.max(1, targetSizeAnalysis.variants.length)) * 100)));
      // Annotations: 0 (nothing), 50 (description only), 100 (has a11y notes)
      scores.annotations = annotations.hasA11yNotes ? 100 : (annotations.hasDescription ? 50 : 0);
      // Color blind: percentage of simulations with no issues
      var cbPassCount = 0;
      for (var cbsi = 0; cbsi < colorBlindAnalysis.simulations.length; cbsi++) {
        if (colorBlindAnalysis.simulations[cbsi].issues === 0) cbPassCount++;
      }
      scores.colorBlindSafety = colorBlindAnalysis.simulations.length > 0 ? Math.round((cbPassCount / colorBlindAnalysis.simulations.length) * 100) : 100;

      // Overall weighted score — weights differ by component classification
      var overall;
      if (isInteractive) {
        // Interactive: focus and states matter most
        overall = Math.round(
          scores.variantCoverage * 0.20 +
          scores.focusIndicator * 0.20 +
          scores.colorDifferentiation * 0.15 +
          scores.targetSize * 0.15 +
          scores.annotations * 0.10 +
          scores.colorBlindSafety * 0.20
        );
      } else {
        // Presentational: variant completeness and color safety matter most, focus is N/A
        overall = Math.round(
          scores.variantCoverage * 0.25 +
          scores.colorDifferentiation * 0.25 +
          scores.annotations * 0.15 +
          scores.colorBlindSafety * 0.25 +
          scores.targetSize * 0.10
        );
      }

      // ---- Build response ----
      var coverageSection;
      if (isInteractive) {
        coverageSection = {
          mode: 'interactive-states',
          found: statesFound,
          missing: missingStates,
          coverage: coveredCount + '/' + totalStates
        };
      } else {
        coverageSection = {
          mode: 'variant-axes',
          axes: variantAxisCoverage,
          coverage: coveredCount + '/' + totalStates
        };
      }

      var auditResult = {
        component: {
          id: componentSet.id,
          name: componentSet.name,
          type: componentSet.type,
          variantCount: variantCount,
          classification: isInteractive ? 'interactive' : 'presentational'
        },
        overallScore: overall,
        scores: scores,
        variantCoverage: coverageSection,
        focusIndicator: isInteractive ? focusAnalysis : { notApplicable: true, details: 'Focus indicators are not expected for presentational components' },
        colorDifferentiation: colorDifferentiation,
        targetSize: isInteractive ? {
          minimum: minTarget + 'x' + minTarget,
          smallest: targetSizeAnalysis.minWidth + 'x' + targetSizeAnalysis.minHeight,
          issues: targetSizeAnalysis.issues
        } : { notApplicable: true, details: 'Target size checks apply to interactive components (WCAG 2.5.8 is about tap targets)', smallest: targetSizeAnalysis.minWidth + 'x' + targetSizeAnalysis.minHeight },
        annotations: annotations,
        colorBlindSimulation: colorBlindAnalysis,
        recommendations: []
      };

      // Generate recommendations — classification-aware
      if (isInteractive) {
        if (!focusAnalysis.hasVariant) {
          auditResult.recommendations.push({ priority: 'high', area: 'focus', message: 'Add a focus/focused variant with a visible focus ring (WCAG 2.4.7)' });
        } else if (!focusAnalysis.hasVisibleIndicator) {
          auditResult.recommendations.push({ priority: 'medium', area: 'focus', message: 'Focus variant exists but lacks visible indicator — add a border or shadow' });
        }
      }
      if (colorDifferentiation.issues.length > 0) {
        auditResult.recommendations.push({ priority: 'high', area: 'color', message: 'Add non-color indicators (icons, borders, text) to ' + colorDifferentiation.issues.length + ' state variant(s) (WCAG 1.4.1)' });
      }
      if (targetSizeAnalysis.issues.length > 0) {
        auditResult.recommendations.push({ priority: 'high', area: 'target-size', message: targetSizeAnalysis.issues.length + ' variant(s) below ' + minTarget + 'x' + minTarget + 'px minimum target size (WCAG 2.5.8)' });
      }
      if (!annotations.hasDescription) {
        auditResult.recommendations.push({ priority: 'medium', area: 'documentation', message: 'Add a component description with usage guidelines' });
      }
      if (!annotations.hasA11yNotes) {
        var a11yHint = isInteractive
          ? 'Add accessibility notes (ARIA role, keyboard interactions, screen reader behavior)'
          : 'Add accessibility notes (ARIA role, live region behavior, semantic usage)';
        auditResult.recommendations.push({ priority: 'medium', area: 'documentation', message: a11yHint });
      }
      if (colorBlindAnalysis.issues.length > 0) {
        auditResult.recommendations.push({ priority: 'medium', area: 'color-blind', message: colorBlindAnalysis.issues.join('; ') });
      }
      if (isInteractive) {
        for (var msi = 0; msi < missingStates.length; msi++) {
          var ms = missingStates[msi];
          if (ms === 'focus' || ms === 'disabled') {
            auditResult.recommendations.push({ priority: 'medium', area: 'states', message: 'Consider adding a "' + ms + '" variant for complete interactive state coverage' });
          }
        }
      } else if (variantAxisCoverage && variantCount < variantAxisCoverage.expectedCombinations) {
        auditResult.recommendations.push({ priority: 'low', area: 'coverage', message: variantCount + ' of ' + variantAxisCoverage.expectedCombinations + ' axis combinations present — consider adding missing variants for completeness' });
      }

      console.log('🌉 [Desktop Bridge] Component audit complete: score ' + overall + '/100 for "' + componentSet.name + '"');

      figma.ui.postMessage({
        type: 'AUDIT_COMPONENT_ACCESSIBILITY_RESULT',
        requestId: msg.requestId,
        success: true,
        data: auditResult
      });

    } catch (error) {
      var auditErrorMsg = error && error.message ? error.message : String(error);
      console.error('🌉 [Desktop Bridge] Component accessibility audit error:', auditErrorMsg);
      figma.ui.postMessage({
        type: 'AUDIT_COMPONENT_ACCESSIBILITY_RESULT',
        requestId: msg.requestId,
        success: false,
        error: auditErrorMsg
      });
    }
  }

  // ============================================================================
  // FIGJAM TOOLS — Only functional when editorType === 'figjam'
  // ============================================================================

  // CREATE_STICKY - Create a sticky note
  else if (msg.type === 'CREATE_STICKY') {
    try {
      if (__editorType !== 'figjam') {
        throw new Error('CREATE_STICKY is only available in FigJam files');
      }
      console.log('🌉 [Desktop Bridge] Creating sticky note');

      var sticky = figma.createSticky();
      await figma.loadFontAsync(sticky.text.fontName);
      sticky.text.characters = msg.text || '';

      if (typeof msg.x === 'number') sticky.x = msg.x;
      if (typeof msg.y === 'number') sticky.y = msg.y;

      // Set sticky color if provided
      if (msg.color) {
        var stickyColor = __stickyColors[msg.color.toUpperCase()];
        if (stickyColor) {
          sticky.fills = [{ type: 'SOLID', color: stickyColor }];
        }
      }

      figma.ui.postMessage({
        type: 'CREATE_STICKY_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { id: sticky.id, type: sticky.type, name: sticky.name, x: sticky.x, y: sticky.y }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Create sticky error:', error);
      figma.ui.postMessage({
        type: 'CREATE_STICKY_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // CREATE_STICKIES - Batch create sticky notes
  else if (msg.type === 'CREATE_STICKIES') {
    try {
      if (__editorType !== 'figjam') {
        throw new Error('CREATE_STICKIES is only available in FigJam files');
      }
      console.log('🌉 [Desktop Bridge] Batch creating sticky notes:', msg.stickies.length);

      var created = [];
      var failed = [];

      // Load font once for all stickies (they all share the same default font)
      var stickyFontLoaded = false;

      for (var si = 0; si < msg.stickies.length; si++) {
        try {
          var spec = msg.stickies[si];
          var sticky = figma.createSticky();
          if (!stickyFontLoaded) {
            await figma.loadFontAsync(sticky.text.fontName);
            stickyFontLoaded = true;
          }
          sticky.text.characters = spec.text || '';

          if (typeof spec.x === 'number') sticky.x = spec.x;
          if (typeof spec.y === 'number') sticky.y = spec.y;

          if (spec.color) {
            var sc = __stickyColors[spec.color.toUpperCase()];
            if (sc) {
              sticky.fills = [{ type: 'SOLID', color: sc }];
            }
          }

          created.push({ id: sticky.id, type: sticky.type, name: sticky.name, x: sticky.x, y: sticky.y });
        } catch (e) {
          failed.push({ index: si, error: e.message || String(e) });
        }
      }

      figma.ui.postMessage({
        type: 'CREATE_STICKIES_RESULT',
        requestId: msg.requestId,
        success: failed.length === 0,
        data: { created: created.length, failed: failed.length, results: created, errors: failed }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Batch create stickies error:', error);
      figma.ui.postMessage({
        type: 'CREATE_STICKIES_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // CREATE_CONNECTOR - Connect two nodes with a connector
  else if (msg.type === 'CREATE_CONNECTOR') {
    try {
      if (__editorType !== 'figjam') {
        throw new Error('CREATE_CONNECTOR is only available in FigJam files');
      }
      console.log('🌉 [Desktop Bridge] Creating connector');

      var connector = figma.createConnector();

      // Set start and end endpoints
      var startNode = await figma.getNodeByIdAsync(msg.startNodeId);
      var endNode = await figma.getNodeByIdAsync(msg.endNodeId);

      if (!startNode) throw new Error('Start node not found: ' + msg.startNodeId);
      if (!endNode) throw new Error('End node not found: ' + msg.endNodeId);

      connector.connectorStart = {
        endpointNodeId: msg.startNodeId,
        magnet: msg.startMagnet || 'AUTO'
      };
      connector.connectorEnd = {
        endpointNodeId: msg.endNodeId,
        magnet: msg.endMagnet || 'AUTO'
      };

      // Set label text if provided
      if (msg.label) {
        try {
          await figma.loadFontAsync(connector.text.fontName);
        } catch (e) {
          // Connector default font may not be loadable — fall back to Inter
          await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
          connector.text.fontName = { family: 'Inter', style: 'Medium' };
        }
        connector.text.characters = msg.label;
      }

      figma.ui.postMessage({
        type: 'CREATE_CONNECTOR_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { id: connector.id, type: connector.type, name: connector.name }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Create connector error:', error);
      figma.ui.postMessage({
        type: 'CREATE_CONNECTOR_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // CREATE_SECTION - Create a section on FigJam board
  else if (msg.type === 'CREATE_SECTION') {
    try {
      if (__editorType !== 'figjam') {
        throw new Error('CREATE_SECTION is only available in FigJam files');
      }
      console.log('🌉 [Desktop Bridge] Creating section');

      var section = figma.createSection();
      if (msg.name) section.name = msg.name;
      if (typeof msg.x === 'number') section.x = msg.x;
      if (typeof msg.y === 'number') section.y = msg.y;
      if (typeof msg.width === 'number' && typeof msg.height === 'number') {
        section.resizeWithoutConstraints(msg.width, msg.height);
      }
      if (msg.fillColor) {
        var scHex = msg.fillColor.replace('#', '');
        var scR = parseInt(scHex.substring(0, 2), 16) / 255;
        var scG = parseInt(scHex.substring(2, 4), 16) / 255;
        var scB = parseInt(scHex.substring(4, 6), 16) / 255;
        section.fills = [{ type: 'SOLID', color: { r: scR, g: scG, b: scB } }];
      }

      figma.ui.postMessage({
        type: 'CREATE_SECTION_RESULT',
        requestId: msg.requestId,
        success: true,
        data: {
          id: section.id,
          type: section.type,
          name: section.name,
          x: section.x,
          y: section.y,
          width: section.width,
          height: section.height
        }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Create section error:', error);
      figma.ui.postMessage({
        type: 'CREATE_SECTION_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // CREATE_SHAPE_WITH_TEXT - Create a labeled shape
  else if (msg.type === 'CREATE_SHAPE_WITH_TEXT') {
    try {
      if (__editorType !== 'figjam') {
        throw new Error('CREATE_SHAPE_WITH_TEXT is only available in FigJam files');
      }
      console.log('🌉 [Desktop Bridge] Creating shape with text');

      var shape = figma.createShapeWithText();

      // Set shape type if provided
      if (msg.shapeType) {
        shape.shapeType = msg.shapeType;
      }

      // Set position
      if (typeof msg.x === 'number') shape.x = msg.x;
      if (typeof msg.y === 'number') shape.y = msg.y;

      // Resize (before text so text reflows to fit)
      if (typeof msg.width === 'number' && typeof msg.height === 'number') {
        shape.resize(msg.width, msg.height);
      } else if (typeof msg.width === 'number') {
        shape.resize(msg.width, shape.height);
      } else if (typeof msg.height === 'number') {
        shape.resize(shape.width, msg.height);
      }

      // Fill color
      if (msg.fillColor) {
        var fHex = msg.fillColor.replace('#', '');
        var fR = parseInt(fHex.substring(0, 2), 16) / 255;
        var fG = parseInt(fHex.substring(2, 4), 16) / 255;
        var fB = parseInt(fHex.substring(4, 6), 16) / 255;
        shape.fills = [{ type: 'SOLID', color: { r: fR, g: fG, b: fB } }];
      }

      // Stroke color
      if (msg.strokeColor) {
        var sHex = msg.strokeColor.replace('#', '');
        var sR = parseInt(sHex.substring(0, 2), 16) / 255;
        var sG = parseInt(sHex.substring(2, 4), 16) / 255;
        var sB = parseInt(sHex.substring(4, 6), 16) / 255;
        shape.strokes = [{ type: 'SOLID', color: { r: sR, g: sG, b: sB } }];
        shape.strokeWeight = 1;
      }

      // Dash pattern
      if (msg.strokeDashPattern) {
        var parts = msg.strokeDashPattern.split(',').map(function(p) { return parseFloat(p.trim()); });
        shape.dashPattern = parts;
      }

      // Set text (after resize so text reflows to fit new size)
      if (msg.text) {
        try {
          await figma.loadFontAsync(shape.text.fontName);
        } catch (e) {
          await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
          shape.text.fontName = { family: 'Inter', style: 'Medium' };
        }
        shape.text.characters = msg.text;
        if (typeof msg.fontSize === 'number') {
          shape.text.fontSize = msg.fontSize;
        }
      }

      figma.ui.postMessage({
        type: 'CREATE_SHAPE_WITH_TEXT_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { id: shape.id, type: shape.type, name: shape.name, x: shape.x, y: shape.y, width: shape.width, height: shape.height }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Create shape with text error:', error);
      figma.ui.postMessage({
        type: 'CREATE_SHAPE_WITH_TEXT_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // CREATE_TABLE - Create a table with data
  else if (msg.type === 'CREATE_TABLE') {
    try {
      if (__editorType !== 'figjam') {
        throw new Error('CREATE_TABLE is only available in FigJam files');
      }
      console.log('🌉 [Desktop Bridge] Creating table:', msg.rows, 'x', msg.columns);

      var table = figma.createTable(msg.rows, msg.columns);

      if (typeof msg.x === 'number') table.x = msg.x;
      if (typeof msg.y === 'number') table.y = msg.y;

      // Populate cells if data provided
      if (msg.data && Array.isArray(msg.data)) {
        for (var row = 0; row < msg.data.length && row < msg.rows; row++) {
          for (var col = 0; col < msg.data[row].length && col < msg.columns; col++) {
            var cell = table.cellAt(row, col);
            if (cell && msg.data[row][col] != null) {
              await figma.loadFontAsync(cell.text.fontName);
              cell.text.characters = String(msg.data[row][col]);
            }
          }
        }
      }

      figma.ui.postMessage({
        type: 'CREATE_TABLE_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { id: table.id, type: table.type, name: table.name, rows: msg.rows, columns: msg.columns }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Create table error:', error);
      figma.ui.postMessage({
        type: 'CREATE_TABLE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // CREATE_CODE_BLOCK - Create a code block
  else if (msg.type === 'CREATE_CODE_BLOCK') {
    try {
      if (__editorType !== 'figjam') {
        throw new Error('CREATE_CODE_BLOCK is only available in FigJam files');
      }
      console.log('🌉 [Desktop Bridge] Creating code block');

      var codeBlock = figma.createCodeBlock();

      // Code blocks require Source Code Pro font, fall back to Inter if unavailable
      try {
        await figma.loadFontAsync({ family: 'Source Code Pro', style: 'Medium' });
      } catch (e) {
        await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
      }

      if (msg.code) {
        codeBlock.code = msg.code;
      }
      if (msg.language) {
        codeBlock.codeLanguage = msg.language;
      }

      if (typeof msg.x === 'number') codeBlock.x = msg.x;
      if (typeof msg.y === 'number') codeBlock.y = msg.y;

      figma.ui.postMessage({
        type: 'CREATE_CODE_BLOCK_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { id: codeBlock.id, type: codeBlock.type, name: codeBlock.name, x: codeBlock.x, y: codeBlock.y }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Create code block error:', error);
      figma.ui.postMessage({
        type: 'CREATE_CODE_BLOCK_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // GET_BOARD_CONTENTS - Read all FigJam nodes from the current page
  else if (msg.type === 'GET_BOARD_CONTENTS') {
    try {
      if (__editorType !== 'figjam') {
        throw new Error('GET_BOARD_CONTENTS is only available in FigJam files');
      }
      console.log('🌉 [Desktop Bridge] Reading board contents');

      var maxNodes = msg.maxNodes || 500;
      var filterTypes = msg.nodeTypes || null;

      // FigJam node types we care about
      var figjamTypes = ['STICKY', 'SHAPE_WITH_TEXT', 'CONNECTOR', 'TABLE', 'CODE_BLOCK', 'SECTION', 'FRAME', 'TEXT'];

      var allNodes = figma.currentPage.children;
      var results = [];
      var truncated = false;

      for (var ni = 0; ni < allNodes.length && results.length < maxNodes; ni++) {
        var node = allNodes[ni];

        // Skip if filtering and this type isn't in the filter list
        if (filterTypes && filterTypes.indexOf(node.type) === -1) continue;
        // Skip if not a FigJam-relevant type
        if (!filterTypes && figjamTypes.indexOf(node.type) === -1) continue;

        var entry = {
          id: node.id,
          type: node.type,
          name: node.name,
          x: node.x,
          y: node.y,
          width: node.width,
          height: node.height
        };

        // Extract text content based on node type
        if (node.type === 'STICKY') {
          entry.text = node.text ? node.text.characters : '';
          if (node.fills && node.fills.length > 0 && node.fills[0].color) {
            entry.color = node.fills[0].color;
          }
        } else if (node.type === 'SHAPE_WITH_TEXT') {
          entry.text = node.text ? node.text.characters : '';
          entry.shapeType = node.shapeType || 'ROUNDED_RECTANGLE';
        } else if (node.type === 'CONNECTOR') {
          entry.connectorStart = node.connectorStart || null;
          entry.connectorEnd = node.connectorEnd || null;
          entry.text = node.text ? node.text.characters : '';
        } else if (node.type === 'CODE_BLOCK') {
          entry.code = node.code || '';
          entry.codeLanguage = node.codeLanguage || '';
        } else if (node.type === 'TABLE') {
          entry.numRows = node.numRows;
          entry.numColumns = node.numColumns;
          // Read first 10 rows of cell data to avoid huge payloads
          var cellData = [];
          var maxCellRows = Math.min(node.numRows, 10);
          for (var row = 0; row < maxCellRows; row++) {
            var rowData = [];
            for (var col = 0; col < node.numColumns; col++) {
              try {
                var cell = node.cellAt(row, col);
                rowData.push(cell && cell.text ? cell.text.characters : '');
              } catch (e) {
                rowData.push('');
              }
            }
            cellData.push(rowData);
          }
          entry.cellData = cellData;
          if (node.numRows > 10) entry.cellDataTruncated = true;
        } else if (node.type === 'SECTION') {
          entry.childCount = node.children ? node.children.length : 0;
        } else if (node.type === 'TEXT') {
          entry.text = node.characters || '';
        }

        results.push(entry);
      }

      if (results.length >= maxNodes) truncated = true;

      figma.ui.postMessage({
        type: 'GET_BOARD_CONTENTS_RESULT',
        requestId: msg.requestId,
        success: true,
        data: {
          nodes: results,
          totalFound: results.length,
          truncated: truncated,
          page: figma.currentPage.name
        }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Get board contents error:', error);
      figma.ui.postMessage({
        type: 'GET_BOARD_CONTENTS_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // GET_CONNECTIONS - Read the connection graph from the board
  else if (msg.type === 'GET_CONNECTIONS') {
    try {
      if (__editorType !== 'figjam') {
        throw new Error('GET_CONNECTIONS is only available in FigJam files');
      }
      console.log('🌉 [Desktop Bridge] Reading connection graph');

      var connectors = figma.currentPage.findAll(function(n) { return n.type === 'CONNECTOR'; });
      var edges = [];
      var nodeMap = {};

      for (var ci = 0; ci < connectors.length; ci++) {
        var conn = connectors[ci];
        var startId = conn.connectorStart ? conn.connectorStart.endpointNodeId : null;
        var endId = conn.connectorEnd ? conn.connectorEnd.endpointNodeId : null;
        var label = conn.text ? conn.text.characters : '';

        edges.push({
          connectorId: conn.id,
          startNodeId: startId,
          endNodeId: endId,
          label: label
        });

        // Build a lookup of connected nodes with their names/types
        if (startId && !nodeMap[startId]) {
          var startNode = await figma.getNodeByIdAsync(startId);
          if (startNode) {
            nodeMap[startId] = {
              id: startId,
              type: startNode.type,
              name: startNode.name,
              text: startNode.text ? startNode.text.characters : (startNode.characters || '')
            };
          }
        }
        if (endId && !nodeMap[endId]) {
          var endNode = await figma.getNodeByIdAsync(endId);
          if (endNode) {
            nodeMap[endId] = {
              id: endId,
              type: endNode.type,
              name: endNode.name,
              text: endNode.text ? endNode.text.characters : (endNode.characters || '')
            };
          }
        }
      }

      figma.ui.postMessage({
        type: 'GET_CONNECTIONS_RESULT',
        requestId: msg.requestId,
        success: true,
        data: {
          edges: edges,
          connectedNodes: nodeMap,
          totalConnectors: connectors.length,
          totalConnectedNodes: Object.keys(nodeMap).length
        }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Get connections error:', error);
      figma.ui.postMessage({
        type: 'GET_CONNECTIONS_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }
  // ==========================================================================
  // SLIDES TOOLS — Figma Slides command handlers
  // ==========================================================================

  // LIST_SLIDES - List all slides in the presentation
  else if (msg.type === 'LIST_SLIDES') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('LIST_SLIDES is only available in Slides files');
      }
      console.log('🌉 [Desktop Bridge] Listing slides');

      var grid = figma.getSlideGrid();
      var slides = [];
      for (var rowIdx = 0; rowIdx < grid.length; rowIdx++) {
        var row = grid[rowIdx];
        // SlideGrid rows are array-like (iterable with numeric indices), not objects with .children
        for (var colIdx = 0; colIdx < row.length; colIdx++) {
          var slide = row[colIdx];
          slides.push({
            id: slide.id,
            name: slide.name,
            row: rowIdx,
            col: colIdx,
            isSkippedSlide: slide.isSkippedSlide,
            childCount: slide.children ? slide.children.length : 0
          });
        }
      }

      figma.ui.postMessage({
        type: 'LIST_SLIDES_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { slides: slides, totalSlides: slides.length, totalRows: grid.length }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] List slides error:', error);
      figma.ui.postMessage({
        type: 'LIST_SLIDES_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // GET_SLIDE_CONTENT - Get node tree of a slide
  else if (msg.type === 'GET_SLIDE_CONTENT') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('GET_SLIDE_CONTENT is only available in Slides files');
      }
      var slideNode = await figma.getNodeByIdAsync(msg.slideId);
      if (!slideNode || slideNode.type !== 'SLIDE') {
        throw new Error('Node ' + msg.slideId + ' is not a SLIDE');
      }
      console.log('🌉 [Desktop Bridge] Getting slide content:', slideNode.name);

      function serializeNode(n) {
        var result = { id: n.id, type: n.type, name: n.name, x: n.x, y: n.y, width: n.width, height: n.height };
        if (n.type === 'TEXT') {
          result.characters = n.characters;
          result.fontSize = n.fontSize;
        }
        if (n.children && n.children.length > 0) {
          result.children = [];
          for (var i = 0; i < n.children.length; i++) {
            result.children.push(serializeNode(n.children[i]));
          }
        }
        return result;
      }

      figma.ui.postMessage({
        type: 'GET_SLIDE_CONTENT_RESULT',
        requestId: msg.requestId,
        success: true,
        data: serializeNode(slideNode)
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Get slide content error:', error);
      figma.ui.postMessage({
        type: 'GET_SLIDE_CONTENT_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // CREATE_SLIDE - Create a new slide
  else if (msg.type === 'CREATE_SLIDE') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('CREATE_SLIDE is only available in Slides files');
      }
      console.log('🌉 [Desktop Bridge] Creating slide');

      var newSlide;
      if (typeof msg.row === 'number' && typeof msg.col === 'number') {
        newSlide = figma.createSlide({ row: msg.row, col: msg.col });
      } else {
        newSlide = figma.createSlide();
      }

      figma.ui.postMessage({
        type: 'CREATE_SLIDE_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { id: newSlide.id, name: newSlide.name }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Create slide error:', error);
      figma.ui.postMessage({
        type: 'CREATE_SLIDE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // DELETE_SLIDE - Delete a slide
  else if (msg.type === 'DELETE_SLIDE') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('DELETE_SLIDE is only available in Slides files');
      }
      var delSlide = await figma.getNodeByIdAsync(msg.slideId);
      if (!delSlide || delSlide.type !== 'SLIDE') {
        throw new Error('Node ' + msg.slideId + ' is not a SLIDE');
      }
      console.log('🌉 [Desktop Bridge] Deleting slide:', delSlide.name);

      var delName = delSlide.name;
      delSlide.remove();

      figma.ui.postMessage({
        type: 'DELETE_SLIDE_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { deleted: msg.slideId, name: delName }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Delete slide error:', error);
      figma.ui.postMessage({
        type: 'DELETE_SLIDE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // DUPLICATE_SLIDE - Clone a slide
  else if (msg.type === 'DUPLICATE_SLIDE') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('DUPLICATE_SLIDE is only available in Slides files');
      }
      var srcSlide = await figma.getNodeByIdAsync(msg.slideId);
      if (!srcSlide || srcSlide.type !== 'SLIDE') {
        throw new Error('Node ' + msg.slideId + ' is not a SLIDE');
      }
      console.log('🌉 [Desktop Bridge] Duplicating slide:', srcSlide.name);

      var clone = srcSlide.clone();

      figma.ui.postMessage({
        type: 'DUPLICATE_SLIDE_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { originalId: msg.slideId, newId: clone.id, name: clone.name }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Duplicate slide error:', error);
      figma.ui.postMessage({
        type: 'DUPLICATE_SLIDE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // GET_SLIDE_GRID - Get 2D grid layout
  else if (msg.type === 'GET_SLIDE_GRID') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('GET_SLIDE_GRID is only available in Slides files');
      }
      console.log('🌉 [Desktop Bridge] Getting slide grid');

      var slideGrid = figma.getSlideGrid();
      var gridData = [];
      for (var ri = 0; ri < slideGrid.length; ri++) {
        var gridRow = slideGrid[ri];
        var rowSlides = [];
        // SlideGrid rows are array-like (iterable with numeric indices)
        for (var ci = 0; ci < gridRow.length; ci++) {
          var gs = gridRow[ci];
          rowSlides.push({ id: gs.id, name: gs.name, col: ci, isSkippedSlide: gs.isSkippedSlide });
        }
        gridData.push({ rowIndex: ri, slides: rowSlides });
      }

      figma.ui.postMessage({
        type: 'GET_SLIDE_GRID_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { grid: gridData, totalRows: gridData.length }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Get slide grid error:', error);
      figma.ui.postMessage({
        type: 'GET_SLIDE_GRID_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // REORDER_SLIDES - Reorder slides via 2D grid of slide IDs
  else if (msg.type === 'REORDER_SLIDES') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('REORDER_SLIDES is only available in Slides files');
      }
      console.log('🌉 [Desktop Bridge] Reordering slides');

      var newGrid = msg.grid; // 2D array of slide IDs

      // Build a lookup: slideId → SlideNode from the current grid
      var currentGrid = figma.getSlideGrid();
      var slideMap = {};
      for (var gri = 0; gri < currentGrid.length; gri++) {
        var grow = currentGrid[gri];
        for (var gci = 0; gci < grow.length; gci++) {
          slideMap[grow[gci].id] = grow[gci];
        }
      }

      // Build new grid as arrays of SlideNode references
      var reorderedRows = [];
      for (var rri = 0; rri < newGrid.length; rri++) {
        var rowIds = newGrid[rri];
        var rowSlides = [];
        for (var cci = 0; cci < rowIds.length; cci++) {
          var slideRef = slideMap[rowIds[cci]];
          if (!slideRef) throw new Error('Slide not found in current grid: ' + rowIds[cci]);
          rowSlides.push(slideRef);
        }
        reorderedRows.push(rowSlides);
      }

      figma.setSlideGrid(reorderedRows);

      figma.ui.postMessage({
        type: 'REORDER_SLIDES_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { success: true, rows: reorderedRows.length }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Reorder slides error:', error);
      figma.ui.postMessage({
        type: 'REORDER_SLIDES_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // SET_SLIDE_TRANSITION - Set transition on a slide
  else if (msg.type === 'SET_SLIDE_TRANSITION') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('SET_SLIDE_TRANSITION is only available in Slides files');
      }
      var transSlide = await figma.getNodeByIdAsync(msg.slideId);
      if (!transSlide || transSlide.type !== 'SLIDE') {
        throw new Error('Node ' + msg.slideId + ' is not a SLIDE');
      }
      console.log('🌉 [Desktop Bridge] Setting slide transition:', transSlide.name);

      var transConfig = {
        style: msg.style,
        duration: msg.duration,
        curve: msg.curve,
        timing: { type: 'ON_CLICK' }
      };
      if (msg.timing) {
        transConfig.timing = msg.timing;
      }
      transSlide.setSlideTransition(transConfig);

      figma.ui.postMessage({
        type: 'SET_SLIDE_TRANSITION_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { id: transSlide.id, transition: transSlide.getSlideTransition() }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Set slide transition error:', error);
      figma.ui.postMessage({
        type: 'SET_SLIDE_TRANSITION_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // GET_SLIDE_TRANSITION - Read transition from a slide
  else if (msg.type === 'GET_SLIDE_TRANSITION') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('GET_SLIDE_TRANSITION is only available in Slides files');
      }
      var readTransSlide = await figma.getNodeByIdAsync(msg.slideId);
      if (!readTransSlide || readTransSlide.type !== 'SLIDE') {
        throw new Error('Node ' + msg.slideId + ' is not a SLIDE');
      }
      console.log('🌉 [Desktop Bridge] Getting slide transition:', readTransSlide.name);

      figma.ui.postMessage({
        type: 'GET_SLIDE_TRANSITION_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { id: readTransSlide.id, transition: readTransSlide.getSlideTransition() }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Get slide transition error:', error);
      figma.ui.postMessage({
        type: 'GET_SLIDE_TRANSITION_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // SET_SLIDES_VIEW_MODE - Toggle between grid and single-slide view
  else if (msg.type === 'SET_SLIDES_VIEW_MODE') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('SET_SLIDES_VIEW_MODE is only available in Slides files');
      }
      console.log('🌉 [Desktop Bridge] Setting slides view mode:', msg.mode);

      figma.viewport.slidesView = msg.mode;

      figma.ui.postMessage({
        type: 'SET_SLIDES_VIEW_MODE_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { mode: figma.viewport.slidesView }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Set slides view mode error:', error);
      figma.ui.postMessage({
        type: 'SET_SLIDES_VIEW_MODE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // GET_FOCUSED_SLIDE - Get currently focused slide
  else if (msg.type === 'GET_FOCUSED_SLIDE') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('GET_FOCUSED_SLIDE is only available in Slides files');
      }
      console.log('🌉 [Desktop Bridge] Getting focused slide');

      var focused = figma.currentPage.focusedSlide;
      var focusData = focused ? { id: focused.id, name: focused.name } : { focused: null };

      figma.ui.postMessage({
        type: 'GET_FOCUSED_SLIDE_RESULT',
        requestId: msg.requestId,
        success: true,
        data: focusData
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Get focused slide error:', error);
      figma.ui.postMessage({
        type: 'GET_FOCUSED_SLIDE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // FOCUS_SLIDE - Navigate to a specific slide
  else if (msg.type === 'FOCUS_SLIDE') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('FOCUS_SLIDE is only available in Slides files');
      }
      var focusTarget = await figma.getNodeByIdAsync(msg.slideId);
      if (!focusTarget || focusTarget.type !== 'SLIDE') {
        throw new Error('Node ' + msg.slideId + ' is not a SLIDE');
      }
      console.log('🌉 [Desktop Bridge] Focusing slide:', focusTarget.name);

      figma.viewport.slidesView = 'single-slide';
      figma.currentPage.focusedSlide = focusTarget;

      figma.ui.postMessage({
        type: 'FOCUS_SLIDE_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { focused: focusTarget.id, name: focusTarget.name }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Focus slide error:', error);
      figma.ui.postMessage({
        type: 'FOCUS_SLIDE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // SKIP_SLIDE - Toggle skip on a slide
  else if (msg.type === 'SKIP_SLIDE') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('SKIP_SLIDE is only available in Slides files');
      }
      var skipSlide = await figma.getNodeByIdAsync(msg.slideId);
      if (!skipSlide || skipSlide.type !== 'SLIDE') {
        throw new Error('Node ' + msg.slideId + ' is not a SLIDE');
      }
      console.log('🌉 [Desktop Bridge] Toggling slide skip:', skipSlide.name, '→', msg.skip);

      skipSlide.isSkippedSlide = !!msg.skip;

      figma.ui.postMessage({
        type: 'SKIP_SLIDE_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { id: skipSlide.id, isSkippedSlide: skipSlide.isSkippedSlide }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Skip slide error:', error);
      figma.ui.postMessage({
        type: 'SKIP_SLIDE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // GET_TEXT_STYLES - Get all local text styles
  else if (msg.type === 'GET_TEXT_STYLES') {
    try {
      var textStyles = await figma.getLocalTextStylesAsync();
      var styles = [];
      for (var si = 0; si < textStyles.length; si++) {
        var style = textStyles[si];
        styles.push({
          id: style.id,
          name: style.name,
          description: style.description || '',
          fontSize: style.fontSize,
          fontFamily: style.fontName ? style.fontName.family : 'unknown',
          fontStyle: style.fontName ? style.fontName.style : 'unknown',
          letterSpacing: style.letterSpacing,
          lineHeight: style.lineHeight,
          textCase: style.textCase,
          textDecoration: style.textDecoration
        });
      }
      console.log('🌉 [Desktop Bridge] Got ' + styles.length + ' text styles');

      figma.ui.postMessage({
        type: 'GET_TEXT_STYLES_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { styles: styles, count: styles.length }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Get text styles error:', error);
      figma.ui.postMessage({
        type: 'GET_TEXT_STYLES_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // SET_SLIDE_BACKGROUND - Set slide background color
  else if (msg.type === 'SET_SLIDE_BACKGROUND') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('SET_SLIDE_BACKGROUND is only available in Slides files');
      }
      var bgSlide = await figma.getNodeByIdAsync(msg.slideId);
      if (!bgSlide || bgSlide.type !== 'SLIDE') {
        throw new Error('Node ' + msg.slideId + ' is not a SLIDE');
      }
      console.log('🌉 [Desktop Bridge] Setting slide background:', bgSlide.name);

      // Parse hex color
      var bgHex = (msg.color || '#CCCCCC').replace('#', '');
      var bgR = parseInt(bgHex.substring(0, 2), 16) / 255;
      var bgG = parseInt(bgHex.substring(2, 4), 16) / 255;
      var bgB = parseInt(bgHex.substring(4, 6), 16) / 255;
      var bgColor = { r: bgR, g: bgG, b: bgB };

      // Check if a background rectangle already exists
      var existingBg = null;
      for (var ci = 0; ci < bgSlide.children.length; ci++) {
        var child = bgSlide.children[ci];
        if (child.type === 'RECTANGLE' && child.name === 'Background' && child.width === 1920 && child.height === 1080) {
          existingBg = child;
          break;
        }
      }

      if (existingBg) {
        // Update existing background
        existingBg.fills = [{ type: 'SOLID', color: bgColor }];
      } else {
        // Create new background rectangle
        var bgRect = figma.createRectangle();
        bgRect.name = 'Background';
        bgRect.resize(1920, 1080);
        bgRect.x = 0;
        bgRect.y = 0;
        bgRect.fills = [{ type: 'SOLID', color: bgColor }];
        bgSlide.appendChild(bgRect);
        bgSlide.insertChild(0, bgRect);
      }

      figma.ui.postMessage({
        type: 'SET_SLIDE_BACKGROUND_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { slideId: bgSlide.id, color: msg.color, updated: !!existingBg }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Set slide background error:', error);
      figma.ui.postMessage({
        type: 'SET_SLIDE_BACKGROUND_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // ADD_TEXT_TO_SLIDE - Add a text node to a slide
  else if (msg.type === 'ADD_TEXT_TO_SLIDE') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('ADD_TEXT_TO_SLIDE is only available in Slides files');
      }
      var textSlide = await figma.getNodeByIdAsync(msg.slideId);
      if (!textSlide || textSlide.type !== 'SLIDE') {
        throw new Error('Node ' + msg.slideId + ' is not a SLIDE');
      }
      console.log('🌉 [Desktop Bridge] Adding text to slide:', textSlide.name);

      var textNode = figma.createText();
      var fontFamily = msg.fontFamily || 'Inter';
      var fontStyle = msg.fontStyle || 'Regular';
      await figma.loadFontAsync({ family: fontFamily, style: fontStyle });
      textNode.fontName = { family: fontFamily, style: fontStyle };
      textNode.characters = msg.text || '';
      textNode.fontSize = msg.fontSize || 24;
      textNode.x = typeof msg.x === 'number' ? msg.x : 100;
      textNode.y = typeof msg.y === 'number' ? msg.y : 100;
      if (msg.color) {
        var hex = msg.color.replace('#', '');
        var r = parseInt(hex.substring(0, 2), 16) / 255;
        var g = parseInt(hex.substring(2, 4), 16) / 255;
        var b = parseInt(hex.substring(4, 6), 16) / 255;
        textNode.fills = [{ type: 'SOLID', color: { r: r, g: g, b: b } }];
      }
      if (msg.textAlign) {
        textNode.textAlignHorizontal = msg.textAlign;
      }
      if (msg.width) {
        textNode.resize(msg.width, textNode.height);
        textNode.textAutoResize = 'HEIGHT';
      }
      if (typeof msg.lineHeight === 'number') {
        textNode.lineHeight = { value: msg.lineHeight, unit: 'PIXELS' };
      }
      if (typeof msg.letterSpacing === 'number') {
        textNode.letterSpacing = { value: msg.letterSpacing, unit: 'PIXELS' };
      }
      if (msg.textCase) {
        textNode.textCase = msg.textCase;
      }
      textSlide.appendChild(textNode);

      figma.ui.postMessage({
        type: 'ADD_TEXT_TO_SLIDE_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { id: textNode.id, text: textNode.characters }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Add text to slide error:', error);
      figma.ui.postMessage({
        type: 'ADD_TEXT_TO_SLIDE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }

  // ADD_SHAPE_TO_SLIDE - Add a shape to a slide
  else if (msg.type === 'ADD_SHAPE_TO_SLIDE') {
    try {
      if (__editorType !== 'slides') {
        throw new Error('ADD_SHAPE_TO_SLIDE is only available in Slides files');
      }
      var shapeSlide = await figma.getNodeByIdAsync(msg.slideId);
      if (!shapeSlide || shapeSlide.type !== 'SLIDE') {
        throw new Error('Node ' + msg.slideId + ' is not a SLIDE');
      }
      console.log('🌉 [Desktop Bridge] Adding shape to slide:', shapeSlide.name);

      var shape;
      if (msg.shapeType === 'ELLIPSE') {
        shape = figma.createEllipse();
      } else {
        shape = figma.createRectangle();
      }
      shape.x = typeof msg.x === 'number' ? msg.x : 100;
      shape.y = typeof msg.y === 'number' ? msg.y : 100;
      shape.resize(typeof msg.width === 'number' ? msg.width : 200, typeof msg.height === 'number' ? msg.height : 200);

      if (msg.color && typeof msg.color === 'string') {
        var hex = msg.color.replace('#', '');
        if (/^[0-9a-fA-F]{6}$/.test(hex)) {
          shape.fills = [{
            type: 'SOLID',
            color: {
              r: parseInt(hex.substring(0, 2), 16) / 255,
              g: parseInt(hex.substring(2, 4), 16) / 255,
              b: parseInt(hex.substring(4, 6), 16) / 255
            }
          }];
        }
      }
      shapeSlide.appendChild(shape);

      figma.ui.postMessage({
        type: 'ADD_SHAPE_TO_SLIDE_RESULT',
        requestId: msg.requestId,
        success: true,
        data: { id: shape.id, type: shape.type }
      });

    } catch (error) {
      console.error('🌉 [Desktop Bridge] Add shape to slide error:', error);
      figma.ui.postMessage({
        type: 'ADD_SHAPE_TO_SLIDE_RESULT',
        requestId: msg.requestId,
        success: false,
        error: error.message || String(error)
      });
    }
  }
};

// ============================================================================
// DOCUMENT CHANGE LISTENER - Forward change events for cache invalidation
// Fires when variables, styles, or nodes change (by any means — user edits, API, etc.)
// Requires figma.loadAllPagesAsync() in dynamic-page mode before registering.
// ============================================================================
figma.loadAllPagesAsync().then(function() {
  figma.on('documentchange', function(event) {
    var hasStyleChanges = false;
    var hasNodeChanges = false;
    var changedNodeIds = [];

    // v1.25.0: also collect metadata-only changes (descriptions, annotations).
    // These are properties that the REST API DOESN'T expose in version snapshots,
    // so figma_diff_versions can't see them through REST alone. Forwarding them
    // via the Plugin API + WebSocket is the only way to make them diff-visible.
    var metadataChanges = [];

    for (var i = 0; i < event.documentChanges.length; i++) {
      var change = event.documentChanges[i];
      if (change.type === 'STYLE_CREATE' || change.type === 'STYLE_DELETE' || change.type === 'STYLE_PROPERTY_CHANGE') {
        hasStyleChanges = true;
      } else if (change.type === 'CREATE' || change.type === 'DELETE' || change.type === 'PROPERTY_CHANGE') {
        hasNodeChanges = true;
        if (change.id && changedNodeIds.length < 50) {
          changedNodeIds.push(change.id);
        }
      }

      // Metadata change detection. PROPERTY_CHANGE includes a `properties` array
      // listing which fields changed; we only snapshot the new value for the
      // two fields Figma's REST never returns: `description` and `annotations`.
      if (change.type === 'PROPERTY_CHANGE' && change.node && Array.isArray(change.properties)) {
        for (var p = 0; p < change.properties.length; p++) {
          var prop = change.properties[p];
          if (prop === 'description' || prop === 'descriptionMarkdown' || prop === 'annotations') {
            try {
              var newValue = change.node[prop];
              // Serialize annotations to plain JSON-safe form
              if (prop === 'annotations' && Array.isArray(newValue)) {
                newValue = newValue.map(function(a) {
                  return {
                    label: a.label || null,
                    labelMarkdown: a.labelMarkdown || null,
                    categoryId: a.categoryId || null,
                    properties: a.properties || []
                  };
                });
              }
              metadataChanges.push({
                node_id: change.node.id,
                node_name: change.node.name || null,
                node_type: change.node.type || null,
                field: prop === 'descriptionMarkdown' ? 'description' : prop,
                new_value: newValue,
                timestamp: Date.now()
              });
            } catch (e) {
              // Some property reads can throw on detached / deleted nodes
            }
          }
        }
      }
    }

    if (metadataChanges.length > 0) {
      figma.ui.postMessage({
        type: 'METADATA_CHANGE',
        data: { changes: metadataChanges }
      });
    }

    if (hasStyleChanges || hasNodeChanges) {
      figma.ui.postMessage({
        type: 'DOCUMENT_CHANGE',
        data: {
          hasStyleChanges: hasStyleChanges,
          hasNodeChanges: hasNodeChanges,
          changedNodeIds: changedNodeIds,
          changeCount: event.documentChanges.length,
          timestamp: Date.now()
        }
      });
    }
  });
  // Selection change listener — tracks what the user has selected in Figma
  figma.on('selectionchange', function() {
    var selection = figma.currentPage.selection;
    var selectedNodes = [];
    for (var i = 0; i < Math.min(selection.length, 50); i++) {
      try {
        var node = selection[i];
        selectedNodes.push({
          id: node.id,
          name: node.name,
          type: node.type,
          width: node.width,
          height: node.height
        });
      } catch (e) {
        // Slot sublayers and table cells may not be fully resolvable —
        // accessing .name throws "does not exist" for these node types.
        // Skip silently rather than crashing the plugin.
      }
    }
    figma.ui.postMessage({
      type: 'SELECTION_CHANGE',
      data: {
        nodes: selectedNodes,
        count: selection.length,
        page: figma.currentPage.name,
        timestamp: Date.now()
      }
    });
  });

  // Page change listener — tracks which page the user is viewing
  figma.on('currentpagechange', function() {
    figma.ui.postMessage({
      type: 'PAGE_CHANGE',
      data: {
        pageId: figma.currentPage.id,
        pageName: figma.currentPage.name,
        timestamp: Date.now()
      }
    });
  });

  console.log('🌉 [Desktop Bridge] Document change, selection, and page listeners registered');
}).catch(function(err) {
  console.warn('🌉 [Desktop Bridge] Could not register event listeners:', err);
});

console.log('🌉 [Desktop Bridge] Ready to handle component requests');
console.log('🌉 [Desktop Bridge] Plugin will stay open until manually closed');

// Plugin stays open - no auto-close
// UI iframe remains accessible so the in-iframe WebSocket bridge client can keep relaying state to the MCP server
