/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import { renderFolderedSelectorList } from "./folderedSelectorList";
import { createReorderableListController } from "./reorderableListController";
import {
  menuItem,
  menuLabel,
  menuSeparator,
  openContextMenuAtPoint,
} from "./contextMenuSchema";

export function createSidebarController(scope) {
  const documentScope = scope.documentScope || scope;
  const renderScope = scope.renderScope || scope;
  const historyScope = scope.historyScope || scope;
  const uiScope = scope.uiScope || scope;

  const layerListReorderController = createReorderableListController({
    container: scope.layerListEl,
    itemSelector: ".layer-row-wrap[data-layer-number]",
    handleSelector: ".layer-drag-handle",
    getItemId: (item) => item.getAttribute("data-layer-number"),
    canDragItem: (itemId) => (Number(itemId) || 0) > 0,
    canDropOnItem: (itemId) => (Number(itemId) || 0) > 0,
    onMove: (sourceId, targetId, position) => {
      const result = scope.moveLayerToDepth(sourceId, targetId, position);
      if (!result) {
        renderLayerList();
        return;
      }
      scope.syncCachedWorldRoomLayerMetadata?.({ layerNumberMap: result.layerNumberMap });
      renderLayerList();
      scope.renderNpcList();
      scope.draw();
      scope.registerHistory("Layer depth changed", "layer:" + result.sourceLayer, "layer:" + (result.layerNumberMap[String(result.sourceLayer)] ?? result.sourceLayer), [
        "Moved layer " + result.sourceLayer + " " + result.position + " layer " + result.targetLayer + ".",
        "Depth order: " + result.previousOrder.join(", ") + " -> " + result.nextOrder.join(", "),
      ]);
      setStatus("Moved layer " + result.sourceLayer + " to a new draw depth.", false);
    },
  });
  const heightLayerListReorderController = createReorderableListController({
    container: scope.heightLayerListEl,
    itemSelector: ".layer-row-wrap[data-height-layer-id]",
    handleSelector: ".layer-drag-handle",
    getItemId: (item) => item.getAttribute("data-height-layer-id"),
    canDragItem: (itemId) => !!String(itemId || "").trim(),
    canDropOnItem: (itemId) => !!String(itemId || "").trim(),
    onMove: (sourceId, targetId, position) => {
      const result = scope.moveHeightLayerToDepth(sourceId, targetId, position);
      if (!result) {
        renderHeightLayerList();
        return;
      }
      scope.syncCachedWorldHeightLayerMetadata?.();
      renderHeightLayerList();
      scope.draw();
      scope.registerHistory("Height order changed", result.sourceId, result.targetId, [
        "Moved height layer " + result.sourceId + " " + result.position + " " + result.targetId + ".",
        "Height order: " + result.previousOrder.join(", ") + " -> " + result.nextOrder.join(", "),
      ]);
      setStatus("Moved height layer to a new stack position.", false);
    },
  });
  const tilePaletteSearchState = {
    active: false,
    query: "",
  };
  const entityPaletteSearchState = {
    active: false,
    query: "",
  };
  const ENTITY_TYPE_META = {
    friendly: { label: "Friendly", hint: "NPCs, vendors, townsfolk" },
    hostile: { label: "Hostile", hint: "Aggro actors, threats" },
    prop: { label: "Props", hint: "Things, obstacles, scenery" },
  };

  function normalizeEntityType(value, fallback = "friendly") {
    const normalizedValue = String(value || "").trim().toLowerCase();
    if (normalizedValue === "friendly" || normalizedValue === "friend" || normalizedValue === "friendo" || normalizedValue === "npc") {
      return "friendly";
    }
    if (normalizedValue === "hostile" || normalizedValue === "enemy" || normalizedValue === "aggro" || normalizedValue === "monster") {
      return "hostile";
    }
    if (normalizedValue === "prop" || normalizedValue === "props" || normalizedValue === "thing" || normalizedValue === "things" || normalizedValue === "object") {
      return "prop";
    }
    return fallback;
  }

  function getEntityTypeLabel(value) {
    const normalized = normalizeEntityType(value);
    return ENTITY_TYPE_META[normalized]?.label || ENTITY_TYPE_META.friendly.label;
  }

  function setStatus(message, isError, options = {}) {
    const normalizedMessage = String(message || "");
    uiScope.saveStatusEl.textContent = message;
    uiScope.saveStatusEl.title = normalizedMessage ? `${normalizedMessage}\nRight-click to open status log.` : "Right-click to open status log.";
    uiScope.saveStatusEl.setAttribute("aria-label", normalizedMessage || "Status");
    uiScope.saveStatusEl.style.color = isError ? "var(--editor-status-error)" : "var(--editor-status-ok)";
    if (options?.skipLogEntry !== true) {
      scope.appendEditorLogEntry?.(isError ? "Error" : "Information", normalizedMessage);
    }
  }

  function getPanelLabel(panelKey) {
    const labels = {
      tiles: "Graphics",
      graphicsSprites: "Graphics",
      graphicsOther: "Graphics",
      instanceTemplates: "Entity Catalog",
      instances: "Placed Entities",
      monsters: "Monsters",
      triggers: "Triggers",
      paths: "Paths",
      transitions: "Transitions",
    };
    return labels[String(panelKey || "").trim()] || "Selectors";
  }

  function refreshEntityTypeTabs() {
    const activeType = normalizeEntityType(scope.activeEntityCategory, "friendly");
    const buttonMap = [
      ["friendly", scope.entityTypeFriendlyBtn],
      ["hostile", scope.entityTypeHostileBtn],
      ["prop", scope.entityTypePropBtn],
    ];
    buttonMap.forEach(([type, button]) => {
      if (!button) {
        return;
      }
      const isActive = activeType === type;
      button.classList.toggle("active", isActive);
      button.setAttribute("aria-pressed", isActive ? "true" : "false");
      const meta = ENTITY_TYPE_META[type] || ENTITY_TYPE_META.friendly;
      button.title = meta.hint;
    });
  }

  function setActiveEntityCategory(nextType, options = {}) {
    const normalizedType = normalizeEntityType(nextType, "friendly");
    const previousType = normalizeEntityType(scope.activeEntityCategory, "friendly");
    const changed = normalizedType !== previousType;
    scope.activeEntityCategory = normalizedType;
    refreshEntityTypeTabs();
    renderInstancePalette();
    scope.renderNpcList();
    scope.draw();
    if (options.focusPanel !== false) {
      scope.setSidebarTab("instances");
    }
    if (changed && options.status !== false) {
      setStatus("Showing " + getEntityTypeLabel(normalizedType).toLowerCase() + " entities.", false);
    }
    return changed;
  }

  function refreshInstanceSectionState() {
    if (scope.instanceTemplateSectionBody) {
      scope.instanceTemplateSectionBody.classList.toggle("hidden", scope.templateSectionCollapsed === true);
    }
    if (scope.placedInstanceSectionBody) {
      scope.placedInstanceSectionBody.classList.toggle("hidden", scope.placedSectionCollapsed === true);
    }
    if (scope.toggleTemplateSectionBtn) {
      scope.toggleTemplateSectionBtn.innerHTML =
        '<span class="selector-section-chevron">' + (scope.templateSectionCollapsed ? "&#9656;" : "&#9662;") + "</span>" +
        "<span>Catalog</span>";
      scope.toggleTemplateSectionBtn.setAttribute("aria-expanded", scope.templateSectionCollapsed ? "false" : "true");
    }
    if (scope.togglePlacedSectionBtn) {
      scope.togglePlacedSectionBtn.innerHTML =
        '<span class="selector-section-chevron">' + (scope.placedSectionCollapsed ? "&#9656;" : "&#9662;") + "</span>" +
        "<span>Placed Entities</span>";
      scope.togglePlacedSectionBtn.setAttribute("aria-expanded", scope.placedSectionCollapsed ? "false" : "true");
    }
  }

  function refreshLayerSectionState() {
    if (scope.drawLayerSectionBody) {
      scope.drawLayerSectionBody.classList.toggle("hidden", scope.drawLayerSectionCollapsed === true);
    }
    if (scope.heightLayerSectionBody) {
      scope.heightLayerSectionBody.classList.toggle("hidden", scope.heightLayerSectionCollapsed === true);
    }
    if (scope.toggleDrawLayerSectionBtn) {
      scope.toggleDrawLayerSectionBtn.innerHTML =
        '<span class="selector-section-chevron">' + (scope.drawLayerSectionCollapsed ? "&#9656;" : "&#9662;") + "</span>" +
        "<span>Draw Layers</span>";
      scope.toggleDrawLayerSectionBtn.setAttribute("aria-expanded", scope.drawLayerSectionCollapsed ? "false" : "true");
    }
    if (scope.toggleHeightLayerSectionBtn) {
      scope.toggleHeightLayerSectionBtn.innerHTML =
        '<span class="selector-section-chevron">' + (scope.heightLayerSectionCollapsed ? "&#9656;" : "&#9662;") + "</span>" +
        "<span>Height Layers</span>";
      scope.toggleHeightLayerSectionBtn.setAttribute("aria-expanded", scope.heightLayerSectionCollapsed ? "false" : "true");
    }
  }

  function toggleDrawLayerSection() {
    scope.drawLayerSectionCollapsed = !scope.drawLayerSectionCollapsed;
    refreshLayerSectionState();
  }

  function toggleHeightLayerSection() {
    scope.heightLayerSectionCollapsed = !scope.heightLayerSectionCollapsed;
    refreshLayerSectionState();
  }

  function toggleTemplateSection() {
    scope.templateSectionCollapsed = !scope.templateSectionCollapsed;
    refreshInstanceSectionState();
  }

  function togglePlacedSection() {
    scope.placedSectionCollapsed = !scope.placedSectionCollapsed;
    refreshInstanceSectionState();
  }

  function collapsePanelFolders(panelKey, itemIds) {
    const layout = scope.getPanelLayout(panelKey, itemIds);
    const folderEntries = Object.entries(layout.folders || {});
    if (!folderEntries.length) {
      return false;
    }
    let changed = false;
    const nextFolders = {};
    folderEntries.forEach(([folderId, folder]) => {
      const nextFolder = {
        ...folder,
        collapsed: true,
      };
      if (folder?.collapsed !== true) {
        changed = true;
      }
      nextFolders[folderId] = nextFolder;
    });
    if (!changed) {
      return false;
    }
    scope.setPanelLayout(panelKey, {
      ...layout,
      folders: nextFolders,
    }, itemIds);
    return true;
  }

  function collapseActiveSidebarTabSections() {
    let changed = false;
    const activeTab = String(scope.activeSidebarTab || "");
    if (activeTab === "tiles") {
      const activeGraphicsTab = getActiveGraphicsTab();
      if (activeGraphicsTab === "tiles") {
        changed = collapsePanelFolders("tiles", scope.getPaintableTileIds()) || changed;
      } else {
        const panelKey = getGraphicsFolderPanelKey(activeGraphicsTab);
        const itemIds = getSpriteGraphicsEntries(activeGraphicsTab).map((entry) => String(entry.id || "").trim()).filter(Boolean);
        changed = collapsePanelFolders(panelKey, itemIds) || changed;
      }
      if (changed) {
        renderPaintPalette();
      }
    } else if (activeTab === "layers") {
      if (!scope.drawLayerSectionCollapsed) {
        scope.drawLayerSectionCollapsed = true;
        changed = true;
      }
      if (!scope.heightLayerSectionCollapsed) {
        scope.heightLayerSectionCollapsed = true;
        changed = true;
      }
      if (changed) {
        refreshLayerSectionState();
      }
    } else if (activeTab === "instances") {
      const templateIds = scope.getNpcCatalogRecords().map((entry) => String(entry.id || "").trim()).filter(Boolean);
      const instanceIds = scope.npcOverlays.map((npc) => String(npc.id || "").trim()).filter(Boolean);
      if (!scope.templateSectionCollapsed) {
        scope.templateSectionCollapsed = true;
        changed = true;
      }
      if (!scope.placedSectionCollapsed) {
        scope.placedSectionCollapsed = true;
        changed = true;
      }
      changed = collapsePanelFolders("instanceTemplates", templateIds) || changed;
      changed = collapsePanelFolders("instances", instanceIds) || changed;
      changed = collapsePanelFolders("monsters", []) || changed;
      if (scope.selectedNpcId) {
        scope.selectedNpcId = "";
        changed = true;
      }
      if (changed) {
        refreshInstanceSectionState();
        renderInstancePalette();
        scope.renderNpcList();
        renderMonsterList();
        scope.draw();
      }
    } else if (activeTab === "triggers") {
      changed = collapsePanelFolders("triggers", []) || changed;
      if (changed) {
        renderTriggerList();
      }
    } else if (activeTab === "paths") {
      changed = collapsePanelFolders("paths", []) || changed;
      if (changed) {
        renderPathList();
      }
    } else if (activeTab === "transitions") {
      changed = collapsePanelFolders("transitions", []) || changed;
      if (changed) {
        renderTransitionList();
      }
    }
    if (changed) {
      setStatus("Collapsed open sections in " + (activeTab === "instances" ? "Entities" : getPanelLabel(activeTab)) + ".", false);
    }
    return changed;
  }

  function togglePanelFolderAndRefresh(panelKey, itemIds, folderId, refreshFn) {
    if (scope.togglePanelFolder(panelKey, itemIds, folderId, getPanelLabel(panelKey))) {
      refreshFn();
    }
  }

  function renamePanelFolderAndRefresh(panelKey, itemIds, folderId, refreshFn) {
    if (scope.renamePanelFolder(panelKey, itemIds, folderId, getPanelLabel(panelKey))) {
      refreshFn();
    }
  }

  function deletePanelFolderAndRefresh(panelKey, itemIds, folderId, refreshFn) {
    if (scope.deletePanelFolder(panelKey, itemIds, folderId, getPanelLabel(panelKey))) {
      refreshFn();
    }
  }

  function movePanelNodeAndRefresh(panelKey, itemIds, dragging, dropTarget, refreshFn) {
    if (scope.movePanelNode(panelKey, itemIds, getPanelLabel(panelKey), dragging, dropTarget)) {
      refreshFn();
    }
  }

  function setSidebarTab(tab) {
    scope.activeSidebarTab = ["information", "layers", "tiles", "instances", "history", "triggers", "paths", "transitions"].includes(tab) ? tab : "information";
    if (scope.activeSidebarTab !== "instances" && scope.activeInstanceBrushId) {
      scope.activeInstanceBrushId = "";
      renderInstancePalette();
    }
    scope.syncToolPanels();
    if (typeof scope.persistPopupSessionLayout === "function") {
      scope.persistPopupSessionLayout();
    }
  }

  function refreshInformationPanel() {
    if (uiScope.mapIdLockedEl) {
      uiScope.mapIdLockedEl.value = scope.mapId;
    }
    if (uiScope.mapNameInputEl) {
      uiScope.mapNameInputEl.value = String(documentScope.mapName || scope.mapId || "");
    }
    if (uiScope.mapWidthInputEl) {
      uiScope.mapWidthInputEl.value = String(documentScope.width);
    }
    if (uiScope.mapHeightInputEl) {
      uiScope.mapHeightInputEl.value = String(documentScope.height);
    }
    if (uiScope.mapBackgroundColorInputEl) {
      uiScope.mapBackgroundColorInputEl.value = documentScope.normalizeMapBackgroundColor(documentScope.backgroundColor);
    }
    documentScope.mapInfoDraft = {
      width: documentScope.width,
      height: documentScope.height,
      name: String(documentScope.mapName || scope.mapId || ""),
      backgroundColor: documentScope.normalizeMapBackgroundColor(documentScope.backgroundColor),
    };
    refreshBackgroundModeButton();
    uiScope.refreshEngineOverrideSummary?.();
    refreshInformationDraftState();
  }

  function refreshBackgroundModeButton() {
    if (!scope.backgroundModeBtn || !scope.backgroundModePreviewEl || !scope.backgroundModeTitleEl || !scope.backgroundModeMetaEl) {
      return;
    }
    const backgroundTileId = String(scope.backgroundTileId || "").trim();
    const tileEntry = backgroundTileId ? scope.getTileEntryById(backgroundTileId) : null;
    const effectiveMode = scope.backgroundCellMode === "tile" && !backgroundTileId
      ? "inherit"
      : scope.backgroundCellMode;
    scope.backgroundModePreviewEl.className = "background-mode-preview";
    scope.backgroundModePreviewEl.innerHTML = "";
    scope.backgroundModePreviewEl.style.background = "var(--editor-preview-bg)";
    if (effectiveMode === "tile" && tileEntry) {
      if (tileEntry.dataUrl) {
        const img = document.createElement("img");
        img.alt = "Background tile";
        img.src = tileEntry.dataUrl;
        scope.backgroundModePreviewEl.appendChild(img);
      } else {
        scope.backgroundModePreviewEl.style.background = String(tileEntry.color || "var(--editor-preview-bg)");
        scope.backgroundModePreviewEl.textContent = String(tileEntry.symbol || ".").charAt(0) || ".";
      }
      scope.backgroundModeTitleEl.textContent = String(tileEntry.name || backgroundTileId || "Background Tile");
      scope.backgroundModeMetaEl.textContent = "Click canvas to stamp explicit background tile.";
    } else if (effectiveMode === "hole") {
      scope.backgroundModePreviewEl.classList.add("background-mode-preview-hole");
      scope.backgroundModeTitleEl.textContent = "Transparent Hole";
      scope.backgroundModeMetaEl.textContent = "Click canvas to punch holes in the background.";
    } else {
      scope.backgroundModePreviewEl.classList.add("background-mode-preview-inherit");
      scope.backgroundModeTitleEl.textContent = "Inherit Background";
      scope.backgroundModeMetaEl.textContent = backgroundTileId
        ? "Click canvas to restore cells to the map background tile."
        : "Fill Background to choose a map-wide background tile first.";
    }
  }

  function cycleBackgroundCellMode() {
    const sequence = scope.backgroundCellModeOrder || ["tile", "hole", "inherit"];
    const current = String(scope.backgroundCellMode || "inherit");
    const currentIndex = Math.max(0, sequence.indexOf(current));
    let nextMode = sequence[(currentIndex + 1) % sequence.length] || "inherit";
    if (nextMode === "tile" && !scope.backgroundTileId) {
      nextMode = "hole";
    }
    scope.backgroundCellMode = nextMode;
    refreshBackgroundModeButton();
    const label = nextMode === "tile"
      ? (scope.backgroundTileId ? "background tile" : "inherit")
      : (nextMode === "hole" ? "transparent hole" : "inherit");
    setStatus("Background brush set to " + label + ".", false);
  }

  function refreshInformationDraftState() {
    const dimensionsLocked = scope.mapWidthInputEl?.readOnly === true || scope.isWorldModeActive?.() === true;
    const widthDirty = !dimensionsLocked && Number(scope.mapInfoDraft.width) !== Number(scope.width);
    const heightDirty = !dimensionsLocked && Number(scope.mapInfoDraft.height) !== Number(scope.height);
    if (scope.mapWidthControlsEl) {
      scope.mapWidthControlsEl.classList.toggle("visible", widthDirty);
    }
    if (scope.mapHeightControlsEl) {
      scope.mapHeightControlsEl.classList.toggle("visible", heightDirty);
    }
    if (scope.mapWidthValueEl) {
      scope.mapWidthValueEl.classList.toggle("dirty", widthDirty);
    }
    if (scope.mapHeightValueEl) {
      scope.mapHeightValueEl.classList.toggle("dirty", heightDirty);
    }
  }

  function cancelDimensionEdit(field) {
    if (field === "width") {
      scope.mapInfoDraft = { ...scope.mapInfoDraft, width: scope.width };
      if (scope.mapWidthInputEl) {
        scope.mapWidthInputEl.value = String(scope.width);
      }
    } else {
      scope.mapInfoDraft = { ...scope.mapInfoDraft, height: scope.height };
      if (scope.mapHeightInputEl) {
        scope.mapHeightInputEl.value = String(scope.height);
      }
    }
    refreshInformationDraftState();
    setStatus("Discarded " + field + " change.", false);
  }

  function handleDimensionKeydown(field, event) {
    if (!event) {
      return;
    }
    if (event.key === "Enter") {
      event.preventDefault();
      if (field === "width") {
        applyInformationEdits({ width: true, height: false });
      } else {
        applyInformationEdits({ width: false, height: true });
      }
      return;
    }
    if (event.key === "Escape") {
      event.preventDefault();
      cancelDimensionEdit(field);
    }
  }

  function cancelInformationEdits() {
    documentScope.mapName = String(documentScope.mapInfoDraft?.name || scope.mapId || "");
    documentScope.backgroundColor = documentScope.normalizeMapBackgroundColor(documentScope.mapInfoDraft?.backgroundColor || documentScope.backgroundColor);
    documentScope.mapInfoDraft = {
      width: documentScope.width,
      height: documentScope.height,
      name: String(documentScope.mapName || scope.mapId || ""),
      backgroundColor: documentScope.normalizeMapBackgroundColor(documentScope.backgroundColor),
    };
    refreshInformationPanel();
    renderScope.draw();
    setStatus("Discarded information changes.", false);
  }

  function applyInformationEdits(options) {
    const dimensionsLocked = scope.mapWidthInputEl?.readOnly === true || scope.isWorldModeActive?.() === true;
    const applyWidth = !dimensionsLocked && (options ? options.width === true : true);
    const applyHeight = !dimensionsLocked && (options ? options.height === true : true);
    const requestedWidth = Math.max(1, Math.min(512, Number(uiScope.mapWidthInputEl ? uiScope.mapWidthInputEl.value : documentScope.width) || documentScope.width));
    const requestedHeight = Math.max(1, Math.min(512, Number(uiScope.mapHeightInputEl ? uiScope.mapHeightInputEl.value : documentScope.height) || documentScope.height));
    const nextWidth = applyWidth ? requestedWidth : documentScope.width;
    const nextHeight = applyHeight ? requestedHeight : documentScope.height;
    const nextName = String(uiScope.mapNameInputEl ? uiScope.mapNameInputEl.value : documentScope.mapName).trim() || scope.mapId;
    const nextBackgroundColor = documentScope.normalizeMapBackgroundColor(uiScope.mapBackgroundColorInputEl ? uiScope.mapBackgroundColorInputEl.value : documentScope.backgroundColor);
    const oldWidth = documentScope.width;
    const oldHeight = documentScope.height;
    const oldName = String(documentScope.mapInfoDraft?.name || documentScope.mapName || scope.mapId || "").trim() || scope.mapId;
    const oldBackgroundColor = documentScope.normalizeMapBackgroundColor(documentScope.mapInfoDraft?.backgroundColor || documentScope.backgroundColor);
    if (nextWidth === documentScope.width && nextHeight === documentScope.height && nextName === oldName && nextBackgroundColor === oldBackgroundColor) {
      setStatus("No information changes to apply.", false);
      return;
    }

    documentScope.applyMapInformationEditsToDocument({
      width: nextWidth,
      height: nextHeight,
      name: nextName,
      backgroundColor: nextBackgroundColor,
    });
    scope.rebuildVisibleWorldChunksFromDocument?.();
    uiScope.renderLayerList();
    uiScope.renderNpcList();
    renderScope.draw();
    historyScope.registerHistory("Map information changed", oldWidth + "x" + oldHeight, documentScope.width + "x" + documentScope.height, [
      "Map name: " + documentScope.mapName,
      "Size: " + oldWidth + "x" + oldHeight + " -> " + documentScope.width + "x" + documentScope.height,
      "Background color: " + oldBackgroundColor + " -> " + documentScope.backgroundColor,
      "NPCs outside the new bounds were removed.",
    ]);
    const statusParts = [];
    if (applyWidth && nextWidth !== oldWidth) {
      statusParts.push("width");
    }
    if (applyHeight && nextHeight !== oldHeight) {
      statusParts.push("height");
    }
    if (statusParts.length > 0) {
      setStatus("Applied " + statusParts.join(" + ") + " change" + (statusParts.length > 1 ? "s" : "") + ".", false);
    } else {
      setStatus("Applied map information changes.", false);
    }
    refreshInformationPanel();
  }

  function hasUnsavedChanges() {
    const currentHistoryId = historyScope.historyEntries[historyScope.historyIndex] ? historyScope.historyEntries[historyScope.historyIndex].id : 0;
    return currentHistoryId !== historyScope.lastSavedHistoryId;
  }

  function selectInstanceTemplateForPlacement(templateEntry) {
    if (!templateEntry || !templateEntry.id) {
      return;
    }
    scope.activeInstanceBrushId = String(templateEntry.id || "").trim();
    scope.selectedNpcId = "";
    scope.setSidebarTab("instances");
    renderInstancePalette();
    scope.draw();
    setStatus("Catalog entity selected for placement: " + (templateEntry.name || templateEntry.id) + ".", false);
  }

  function clearInstanceTemplatePlacement() {
    if (!scope.activeInstanceBrushId) {
      return;
    }
    scope.activeInstanceBrushId = "";
    renderInstancePalette();
    scope.draw();
    setStatus("Placement entity cleared.", false);
  }

  function focusPlacedInstanceForTemplate(templateEntry) {
    const placedNpc = scope.findPlacedNpcByTemplateId(templateEntry && templateEntry.id);
    if (!placedNpc) {
      setStatus("No placed entity found for catalog entry " + (templateEntry?.name || templateEntry?.id || "entry") + ".", true);
      return false;
    }
    scope.selectNpc(placedNpc);
    return true;
  }

  function setAllLayerVisibility(isVisible) {
    scope.roomLayers.forEach((layer) => {
      scope.setLayerVisibility(layer.layer, isVisible);
    });
  }

  function activateLayerForEditing(layerNumber) {
    const normalizedLayer = Number(layerNumber) || 0;
    scope.setEditingTargetKind("room");
    scope.viewingAllLayers = false;
    scope.activeLayer = normalizedLayer;
    scope.setLayerVisibility(normalizedLayer, true);
    renderLayerList();
    scope.renderNpcList();
    scope.draw();
    setStatus("Editing on " + scope.getLayerDisplayName(normalizedLayer) + ".", false);
  }

  function getSortedHeightLayers() {
    return (Array.isArray(scope.heightLayers) ? scope.heightLayers.slice() : []);
  }

  function selectHeightLayerForEditing(heightLayerId) {
    const entry = scope.getHeightLayerById(heightLayerId);
    if (!entry) {
      return false;
    }
    scope.setEditingTargetKind("height", entry.id);
    scope.selectedTile = null;
    renderLayerList();
    scope.draw();
    setStatus("Editing height layer " + scope.getHeightLayerDisplayName(entry) + " at Z" + entry.z + ".", false);
    return true;
  }

  function createNextHeightLayerId() {
    const existingIds = new Set((Array.isArray(scope.heightLayers) ? scope.heightLayers : []).map((entry) => String(entry?.id || "").trim()).filter(Boolean));
    let sequence = 1;
    let nextId = "";
    while (!nextId || existingIds.has(nextId)) {
      nextId = "height_" + String(sequence);
      sequence += 1;
    }
    return nextId;
  }

  function addHeightLayer() {
    const nextId = createNextHeightLayerId();
    const targetZ = (Array.isArray(scope.heightLayers) ? scope.heightLayers.length : 0) + 1;
    const nextEntry = {
      id: nextId,
      name: "Height Z" + targetZ,
      z: targetZ,
      x: 0,
      y: 0,
      rows: [],
    };
    scope.heightLayers = scope.cloneHeightLayers([...(Array.isArray(scope.heightLayers) ? scope.heightLayers : []), nextEntry]);
    scope.syncCachedWorldHeightLayerMetadata?.();
    scope.rebuildVisibleWorldChunksFromDocument?.();
    scope.setEditingTargetKind("height", nextId);
    renderLayerList();
    scope.draw();
    scope.registerHistory("Height layer added", "height:" + Math.max(0, scope.heightLayers.length - 1), "height:" + scope.heightLayers.length, [
      "Height layer: " + scope.getHeightLayerDisplayName(nextEntry),
      "Z Index: " + targetZ,
    ]);
    setStatus("Added height layer " + scope.getHeightLayerDisplayName(nextEntry) + ".", false);
    return true;
  }

  function duplicateHeightLayer(heightLayerId) {
    const sourceEntry = scope.getHeightLayerById(heightLayerId);
    if (!sourceEntry) {
      return false;
    }
    const currentLayers = Array.isArray(scope.heightLayers) ? scope.heightLayers : [];
    const sourceIndex = currentLayers.findIndex((entry) => String(entry?.id || "").trim() === String(heightLayerId || "").trim());
    if (sourceIndex < 0) {
      return false;
    }
    const nextId = createNextHeightLayerId();
    const nextZ = sourceIndex + 2;
    const previousLabel = scope.getHeightLayerDisplayName(sourceEntry);
    const duplicateName = typeof sourceEntry.name === "string" && sourceEntry.name.trim()
      ? (sourceEntry.name.trim() + " Copy")
      : ("Height Z" + nextZ);
    const nextEntry = {
      id: nextId,
      name: duplicateName,
      z: nextZ,
      x: Math.max(0, Number(sourceEntry.x) || 0),
      y: Math.max(0, Number(sourceEntry.y) || 0),
      rows: Array.isArray(sourceEntry.rows) ? sourceEntry.rows.map((row) => String(row || "")) : [],
    };
    const nextLayers = currentLayers.slice();
    nextLayers.splice(sourceIndex + 1, 0, nextEntry);
    scope.heightLayers = scope.cloneHeightLayers(nextLayers);
    scope.syncCachedWorldHeightLayerMetadata?.();
    scope.rebuildVisibleWorldChunksFromDocument?.();
    scope.setEditingTargetKind("height", nextId);
    renderLayerList();
    scope.draw();
    scope.registerHistory("Height layer duplicated", previousLabel, scope.getHeightLayerDisplayName(nextEntry), [
      "Source height layer: " + previousLabel,
      "Duplicate height layer: " + scope.getHeightLayerDisplayName(nextEntry),
      "Assigned Z Index: " + nextZ,
    ]);
    setStatus("Duplicated height layer " + previousLabel + " to " + scope.getHeightLayerDisplayName(nextEntry) + ".", false);
    return true;
  }

  function renameHeightLayer(heightLayerId) {
    const entry = scope.getHeightLayerById(heightLayerId);
    if (!entry) {
      return false;
    }
    const previousLabel = scope.getHeightLayerDisplayName(entry);
    const nextRaw = window.prompt("Height layer name", previousLabel);
    if (nextRaw === null) {
      return false;
    }
    const nextName = String(nextRaw || "").trim();
    entry.name = nextName || undefined;
    const nextLabel = scope.getHeightLayerDisplayName(entry);
    if (previousLabel === nextLabel) {
      return false;
    }
    scope.heightLayers = scope.cloneHeightLayers(scope.heightLayers);
    scope.syncCachedWorldHeightLayerMetadata?.();
    scope.rebuildVisibleWorldChunksFromDocument?.();
    renderLayerList();
    scope.draw();
    scope.registerHistory("Height layer renamed", previousLabel, nextLabel, [
      "Height layer: " + String(entry.id || ""),
      "Name: " + previousLabel + " -> " + nextLabel,
    ]);
    setStatus("Renamed height layer to " + nextLabel + ".", false);
    return true;
  }

  function deleteHeightLayer(heightLayerId) {
    const entry = scope.getHeightLayerById(heightLayerId);
    if (!entry) {
      return false;
    }
    const previousLabel = scope.getHeightLayerDisplayName(entry);
    const nextLayers = (Array.isArray(scope.heightLayers) ? scope.heightLayers : [])
      .filter((candidate) => String(candidate?.id || "").trim() !== String(heightLayerId || "").trim());
    scope.heightLayers = scope.cloneHeightLayers(nextLayers);
    scope.syncCachedWorldHeightLayerMetadata?.();
    scope.rebuildVisibleWorldChunksFromDocument?.();
    if (String(scope.activeHeightLayerId || "").trim() === String(heightLayerId || "").trim()) {
      const fallbackId = String(nextLayers[0]?.id || "").trim();
      if (fallbackId) {
        scope.setEditingTargetKind("height", fallbackId);
      } else {
        scope.setEditingTargetKind("room");
      }
    }
    renderLayerList();
    scope.draw();
    scope.registerHistory("Height layer removed", previousLabel, "height:" + scope.heightLayers.length, [
      "Removed height layer: " + previousLabel,
      "Remaining height layers: " + scope.heightLayers.length,
    ]);
    setStatus("Removed height layer " + previousLabel + ".", false);
    return true;
  }

  function isolateLayer(layerNumber) {
    const normalizedLayer = Number(layerNumber) || 0;
    scope.viewingAllLayers = false;
    scope.activeLayer = normalizedLayer;
    scope.roomLayers.forEach((layer) => {
      scope.setLayerVisibility(layer.layer, Number(layer.layer) === normalizedLayer);
    });
    renderLayerList();
    scope.renderNpcList();
    scope.draw();
    setStatus("Showing only " + scope.getLayerDisplayName(normalizedLayer) + ".", false);
  }

  function revealAllLayers() {
    setAllLayerVisibility(true);
    renderLayerList();
    scope.renderNpcList();
    scope.draw();
    setStatus("All layers are visible.", false);
  }

  function moveLayerToExtreme(layerNumber, direction) {
    const normalizedLayer = Number(layerNumber) || 0;
    const nonBaseLayers = scope.roomLayers
      .map((layer) => Number(layer.layer) || 0)
      .filter((entry) => entry > 0)
      .sort((left, right) => left - right);
    if (normalizedLayer <= 0 || nonBaseLayers.length <= 1) {
      return false;
    }
    if (direction === "top") {
      const highestOtherLayer = nonBaseLayers.filter((entry) => entry !== normalizedLayer).slice(-1)[0];
      if (!highestOtherLayer) {
        return false;
      }
      const result = scope.moveLayerToDepth(normalizedLayer, highestOtherLayer, "after");
      if (!result) {
        return false;
      }
      renderLayerList();
      scope.renderNpcList();
      scope.draw();
      scope.registerHistory("Layer depth changed", "layer:" + result.sourceLayer, "layer:" + (result.layerNumberMap[String(result.sourceLayer)] ?? result.sourceLayer), [
        "Moved layer " + result.sourceLayer + " to the top draw depth.",
        "Depth order: " + result.previousOrder.join(", ") + " -> " + result.nextOrder.join(", "),
      ]);
      setStatus("Moved layer " + normalizedLayer + " to the top draw depth.", false);
      return true;
    }
    const lowestOtherLayer = nonBaseLayers.find((entry) => entry !== normalizedLayer);
    if (!lowestOtherLayer) {
      return false;
    }
    const result = scope.moveLayerToDepth(normalizedLayer, lowestOtherLayer, "before");
    if (!result) {
      return false;
    }
    renderLayerList();
    scope.renderNpcList();
    scope.draw();
    scope.registerHistory("Layer depth changed", "layer:" + result.sourceLayer, "layer:" + (result.layerNumberMap[String(result.sourceLayer)] ?? result.sourceLayer), [
      "Moved layer " + result.sourceLayer + " to the bottom non-base draw depth.",
      "Depth order: " + result.previousOrder.join(", ") + " -> " + result.nextOrder.join(", "),
    ]);
    setStatus("Moved layer " + normalizedLayer + " toward the base draw depth.", false);
    return true;
  }

  function deleteLayerByNumber(layerNumber) {
    const normalizedLayer = Number(layerNumber) || 0;
    if (normalizedLayer === 0) {
      return;
    }
    const beforeCount = scope.roomLayers.length;
    scope.roomLayers = scope.roomLayers.filter((entry) => Number(entry.layer) !== normalizedLayer);
    scope.npcOverlays.forEach((npc) => {
      if ((Number(npc.layer) || 0) === normalizedLayer) {
        npc.layer = 0;
        if (npc.record && typeof npc.record === "object" && !Array.isArray(npc.record)) {
          npc.record.layer = 0;
        }
      }
    });
    scope.syncCachedWorldRoomLayerMetadata?.({
      removedLayerNumbers: [normalizedLayer],
      fallbackLayerNumber: 0,
    });
    scope.ensureBaseLayer();
    scope.rebuildVisibleWorldChunksFromDocument?.();
    if (!scope.viewingAllLayers && Number(scope.activeLayer) === normalizedLayer) {
      scope.activeLayer = scope.getEditableLayerNumber();
      scope.setLayerVisibility(scope.activeLayer, true);
    }
    renderLayerList();
    scope.renderNpcList();
    scope.draw();
    scope.registerHistory("Layer removed", "layer:" + normalizedLayer, "layers:" + scope.roomLayers.length, [
      "Removed layer: " + scope.getLayerDisplayName(normalizedLayer),
      "Layer count: " + beforeCount + " -> " + scope.roomLayers.length,
      "Moved NPCs from removed layer to layer 0.",
    ]);
    setStatus("Removed " + scope.getLayerDisplayName(normalizedLayer) + ".", false);
  }

  function renameLayerByNumber(layerNumber) {
    const normalizedLayer = Number(layerNumber) || 0;
    const layer = scope.getLayerByNumber(normalizedLayer);
    if (!layer) {
      return false;
    }
    const previousLabel = scope.getLayerDisplayName(layer);
    const nextRaw = window.prompt("Layer name", previousLabel);
    if (nextRaw === null) {
      return false;
    }
    const nextName = String(nextRaw || "").trim();
    const nextCustomName = nextName || undefined;
    const nextLabel = nextCustomName || scope.getLayerDefaultName(normalizedLayer);
    if ((layer.name || undefined) === nextCustomName || previousLabel === nextLabel) {
      return false;
    }
    layer.name = nextCustomName;
    scope.syncCachedWorldRoomLayerMetadata?.();
    renderLayerList();
    scope.draw();
    scope.registerHistory("Layer renamed", previousLabel, nextLabel, [
      "Layer: " + normalizedLayer,
      "Name: " + previousLabel + " -> " + nextLabel,
    ]);
    setStatus("Renamed layer to " + nextLabel + ".", false);
    return true;
  }

  function selectPaintTile(tileId) {
    const normalizedTileId = String(tileId || "").trim();
    if (!normalizedTileId) {
      return;
    }
    scope.activeBrushTileId = normalizedTileId;
    scope.activeGraphicsRecordId = normalizedTileId;
    scope.activeGraphicsTab = "tiles";
    scope.selectedNpcId = "";
    scope.setSidebarTab("tiles");
    renderPaintPalette();
    refreshInformationPanel();
    scope.draw();
    setStatus("Selected tile " + scope.describeBrushTileId(normalizedTileId) + ".", false);
  }

  function getActiveGraphicsTab() {
    const normalized = String(scope.activeGraphicsTab || "").trim().toLowerCase();
    if (normalized === "sprites" || normalized === "other") {
      return normalized;
    }
    return "tiles";
  }

  function refreshGraphicsCategoryTabs() {
    const activeTab = getActiveGraphicsTab();
    [
      ["tiles", scope.graphicsTilesBtn],
      ["sprites", scope.graphicsSpritesBtn],
      ["other", scope.graphicsOtherBtn],
    ].forEach(([tabId, button]) => {
      if (!button) {
        return;
      }
      const isActive = activeTab === tabId;
      button.classList.toggle("active", isActive);
      button.setAttribute("aria-pressed", isActive ? "true" : "false");
    });
  }

  function getGraphicsFolderPanelKey(graphicsTab) {
    if (graphicsTab === "sprites") {
      return "graphicsSprites";
    }
    if (graphicsTab === "other") {
      return "graphicsOther";
    }
    return "tiles";
  }

  function getSpriteGraphicsEntries(graphicsTab) {
    const normalizedTab = graphicsTab === "other" ? "other" : "sprites";
    const payload = scope.ensureDocumentContentPayload?.("sprites", { schemaVersion: 1, sprites: [] }) || { schemaVersion: 1, sprites: [] };
    const records = payload && Array.isArray(payload.sprites) ? payload.sprites : [];
    return records
      .filter((entry) => entry && typeof entry === "object" && !Array.isArray(entry))
      .filter((entry) => {
        const role = String(entry.graphicRole || "sprite").trim().toLowerCase() === "other" ? "other" : "sprites";
        return role === normalizedTab;
      })
      .map((entry) => {
        const id = String(entry.id || "").trim();
        const sprite = scope.spriteCatalog[id] || null;
        return {
          id,
          name: String(entry.name || id || "Graphic").trim() || "Graphic",
          role: normalizedTab,
          tags: Array.isArray(entry.tags) ? entry.tags.map((tag) => String(tag || "").trim()).filter(Boolean) : [],
          dataUrl: sprite ? sprite.dataUrl : null,
          record: entry,
        };
      })
      .filter((entry) => entry.id)
      .sort((left, right) => String(left.name || left.id).localeCompare(String(right.name || right.id), undefined, { sensitivity: "base" }));
  }

  function selectGraphicsRecord(graphicsTab, recordId, options = {}) {
    const normalizedId = String(recordId || "").trim();
    if (!normalizedId) {
      return false;
    }
    scope.activeGraphicsTab = graphicsTab === "other" ? "other" : (graphicsTab === "sprites" ? "sprites" : "tiles");
    scope.activeGraphicsRecordId = normalizedId;
    if (scope.activeGraphicsTab === "tiles") {
      scope.activeBrushTileId = normalizedId;
      scope.selectedNpcId = "";
      refreshInformationPanel();
      scope.draw();
      if (options.status !== false) {
        setStatus("Selected tile " + scope.describeBrushTileId(normalizedId) + ".", false);
      }
    } else if (options.status !== false) {
      setStatus("Selected graphic " + normalizedId + ".", false);
    }
    renderPaintPalette();
    return true;
  }

  function normalizeTileSearchQuery(value) {
    return String(value || "").replace(/\s+/g, " ").trim();
  }

  function normalizeEntitySearchQuery(value) {
    return String(value || "").replace(/\s+/g, " ").trim();
  }

  function refreshTileSearchButtonState() {
    if (!scope.tileSearchModeBtn) {
      return;
    }
    const isActive = tilePaletteSearchState.active === true;
    scope.tileSearchModeBtn.classList.toggle("active", isActive);
    scope.tileSearchModeBtn.setAttribute("aria-pressed", isActive ? "true" : "false");
    scope.tileSearchModeBtn.title = isActive ? "Close graphics search" : "Search graphics";
    scope.tileSearchModeBtn.setAttribute("aria-label", scope.tileSearchModeBtn.title);
  }

  function bindTileSearchToolbarButton() {
    if (!scope.tileSearchModeBtn || scope.tileSearchModeBtn.dataset.bound === "true") {
      refreshTileSearchButtonState();
      return;
    }
    scope.tileSearchModeBtn.dataset.bound = "true";
    scope.tileSearchModeBtn.addEventListener("click", () => {
      tilePaletteSearchState.active = !tilePaletteSearchState.active;
      if (!tilePaletteSearchState.active) {
        tilePaletteSearchState.query = "";
      }
      renderPaintPalette();
      if (tilePaletteSearchState.active) {
        setStatus("Graphics search enabled. Search by name, id, symbol, or tags.", false);
      } else {
        setStatus("Graphics search closed.", false);
      }
    });
    refreshTileSearchButtonState();
  }

  function refreshEntitySearchButtonState() {
    if (!scope.entitySearchModeBtn) {
      return;
    }
    const isActive = entityPaletteSearchState.active === true;
    scope.entitySearchModeBtn.classList.toggle("active", isActive);
    scope.entitySearchModeBtn.setAttribute("aria-pressed", isActive ? "true" : "false");
    scope.entitySearchModeBtn.title = isActive ? "Close entity search" : "Search entities";
    scope.entitySearchModeBtn.setAttribute("aria-label", scope.entitySearchModeBtn.title);
  }

  function bindEntitySearchToolbarButton() {
    if (!scope.entitySearchModeBtn || scope.entitySearchModeBtn.dataset.bound === "true") {
      refreshEntitySearchButtonState();
      return;
    }
    scope.entitySearchModeBtn.dataset.bound = "true";
    scope.entitySearchModeBtn.addEventListener("click", () => {
      entityPaletteSearchState.active = !entityPaletteSearchState.active;
      if (!entityPaletteSearchState.active) {
        entityPaletteSearchState.query = "";
      }
      renderInstancePalette();
      scope.renderNpcList();
      if (entityPaletteSearchState.active) {
        setStatus("Entity search enabled. Search catalog and placed entities by name, id, type, folder, sprite, position, or tag.", false);
      } else {
        setStatus("Entity search closed.", false);
      }
    });
    refreshEntitySearchButtonState();
  }

  function findTileFolderEntry(tileId, layout) {
    const normalizedTileId = String(tileId || "").trim();
    if (!normalizedTileId || !layout?.folders) {
      return null;
    }
    const folderEntry = Object.entries(layout.folders).find(([, folder]) => Array.isArray(folder?.itemOrder) && folder.itemOrder.includes(normalizedTileId));
    if (!folderEntry) {
      return null;
    }
    return {
      id: String(folderEntry[0] || "").trim(),
      folder: folderEntry[1],
    };
  }

  function findEntityFolderEntry(panelKey, itemId, layout) {
    const normalizedItemId = String(itemId || "").trim();
    if (!normalizedItemId || !layout?.folders) {
      return null;
    }
    const folderEntry = Object.entries(layout.folders).find(([, folder]) => Array.isArray(folder?.itemOrder) && folder.itemOrder.includes(normalizedItemId));
    if (!folderEntry) {
      return null;
    }
    return {
      id: String(folderEntry[0] || "").trim(),
      folder: folderEntry[1],
      panelKey,
    };
  }

  function ensureEntityFolderExpanded(panelKey, itemId, itemIds) {
    const normalizedItemId = String(itemId || "").trim();
    if (!normalizedItemId) {
      return false;
    }
    const layout = scope.getPanelLayout(panelKey, itemIds);
    const folderEntry = findEntityFolderEntry(panelKey, normalizedItemId, layout);
    if (!folderEntry?.id || folderEntry.folder?.collapsed !== true) {
      return false;
    }
    scope.setPanelLayout(panelKey, {
      ...layout,
      folders: {
        ...(layout.folders || {}),
        [folderEntry.id]: {
          ...folderEntry.folder,
          collapsed: false,
        },
      },
    }, itemIds);
    return true;
  }

  function ensureTileFolderExpanded(tileId, tileIds, panelKey = "tiles") {
    const normalizedTileId = String(tileId || "").trim();
    if (!normalizedTileId) {
      return false;
    }
    const layout = scope.getPanelLayout(panelKey, tileIds);
    const folderEntry = findTileFolderEntry(normalizedTileId, layout);
    if (!folderEntry?.id || folderEntry.folder?.collapsed !== true) {
      return false;
    }
    scope.setPanelLayout(panelKey, {
      ...layout,
      folders: {
        ...(layout.folders || {}),
        [folderEntry.id]: {
          ...folderEntry.folder,
          collapsed: false,
        },
      },
    }, tileIds);
    return true;
  }

  function getTileSearchResults(tileEntries, tileIds) {
    const normalizedQuery = normalizeTileSearchQuery(tilePaletteSearchState.query).toLocaleLowerCase();
    const layout = scope.getPanelLayout("tiles", tileIds);
    return tileEntries
      .map((entry) => {
        const tileId = String(entry?.id || "").trim();
        const tile = entry?.tile || {};
        const folderEntry = findTileFolderEntry(tileId, layout);
        const folderName = String(folderEntry?.folder?.name || "").trim();
        const tileName = String(tile.name || tileId || "Tile").trim() || "Tile";
        const symbol = String(tile.symbol || ".").charAt(0) || ".";
        const tags = Array.isArray(tile.tags)
          ? tile.tags.map((tag) => String(tag || "").trim()).filter(Boolean)
          : [];
        const searchParts = [
          tileName,
          tileId,
          symbol,
          folderName,
          ...tags,
        ];
        if (!normalizedQuery) {
          return {
            tileId,
            tile,
            tileName,
            symbol,
            tags,
            folderName,
            score: 0,
          };
        }
        let score = 0;
        const lowerName = tileName.toLocaleLowerCase();
        const lowerId = tileId.toLocaleLowerCase();
        const lowerSymbol = symbol.toLocaleLowerCase();
        const lowerFolder = folderName.toLocaleLowerCase();
        const lowerTags = tags.map((tag) => tag.toLocaleLowerCase());
        if (lowerName === normalizedQuery) {
          score += 400;
        } else if (lowerName.startsWith(normalizedQuery)) {
          score += 220;
        } else if (lowerName.includes(normalizedQuery)) {
          score += 120;
        }
        if (lowerId === normalizedQuery) {
          score += 320;
        } else if (lowerId.startsWith(normalizedQuery)) {
          score += 180;
        } else if (lowerId.includes(normalizedQuery)) {
          score += 90;
        }
        if (lowerSymbol === normalizedQuery) {
          score += 260;
        }
        if (lowerFolder) {
          if (lowerFolder === normalizedQuery) {
            score += 140;
          } else if (lowerFolder.includes(normalizedQuery)) {
            score += 55;
          }
        }
        lowerTags.forEach((tag) => {
          if (tag === normalizedQuery) {
            score += 200;
          } else if (tag.includes(normalizedQuery)) {
            score += 80;
          }
        });
        if (score <= 0 && !searchParts.some((part) => String(part || "").toLocaleLowerCase().includes(normalizedQuery))) {
          return null;
        }
        return {
          tileId,
          tile,
          tileName,
          symbol,
          tags,
          folderName,
          score,
        };
      })
      .filter(Boolean)
      .sort((left, right) => {
        if (Number(right.score) !== Number(left.score)) {
          return Number(right.score) - Number(left.score);
        }
        return String(left.tileName || left.tileId).localeCompare(String(right.tileName || right.tileId), undefined, { sensitivity: "base" });
      });
  }

  function selectTileFromSearchResult(tileId, tileIds) {
    const normalizedTileId = String(tileId || "").trim();
    if (!normalizedTileId) {
      return false;
    }
    ensureTileFolderExpanded(normalizedTileId, tileIds);
    tilePaletteSearchState.active = false;
    tilePaletteSearchState.query = "";
    selectPaintTile(normalizedTileId);
    return true;
  }

  function getSpriteGraphicSearchResults(graphicsTab, entries) {
    const normalizedQuery = normalizeTileSearchQuery(tilePaletteSearchState.query).toLocaleLowerCase();
    const panelKey = getGraphicsFolderPanelKey(graphicsTab);
    const itemIds = entries.map((entry) => String(entry.id || "").trim()).filter(Boolean);
    const layout = scope.getPanelLayout(panelKey, itemIds);
    return entries
      .map((entry) => {
        const folderEntry = findTileFolderEntry(entry.id, layout);
        const folderName = String(folderEntry?.folder?.name || "").trim();
        const tags = Array.isArray(entry.tags) ? entry.tags : [];
        const searchParts = [
          entry.name,
          entry.id,
          folderName,
          ...tags,
        ];
        if (!normalizedQuery) {
          return {
            ...entry,
            folderName,
            score: 0,
          };
        }
        let score = 0;
        const lowerName = String(entry.name || "").toLocaleLowerCase();
        const lowerId = String(entry.id || "").toLocaleLowerCase();
        const lowerFolder = folderName.toLocaleLowerCase();
        if (lowerName === normalizedQuery) {
          score += 400;
        } else if (lowerName.startsWith(normalizedQuery)) {
          score += 220;
        } else if (lowerName.includes(normalizedQuery)) {
          score += 120;
        }
        if (lowerId === normalizedQuery) {
          score += 320;
        } else if (lowerId.startsWith(normalizedQuery)) {
          score += 180;
        } else if (lowerId.includes(normalizedQuery)) {
          score += 90;
        }
        if (lowerFolder) {
          if (lowerFolder === normalizedQuery) {
            score += 140;
          } else if (lowerFolder.includes(normalizedQuery)) {
            score += 55;
          }
        }
        tags.forEach((tag) => {
          const lowered = String(tag || "").toLocaleLowerCase();
          if (lowered === normalizedQuery) {
            score += 200;
          } else if (lowered.includes(normalizedQuery)) {
            score += 80;
          }
        });
        if (score <= 0 && !searchParts.some((part) => String(part || "").toLocaleLowerCase().includes(normalizedQuery))) {
          return null;
        }
        return {
          ...entry,
          folderName,
          score,
        };
      })
      .filter(Boolean)
      .sort((left, right) => {
        if (Number(right.score) !== Number(left.score)) {
          return Number(right.score) - Number(left.score);
        }
        return String(left.name || left.id).localeCompare(String(right.name || right.id), undefined, { sensitivity: "base" });
      });
  }

  function selectSpriteGraphicFromSearchResult(graphicsTab, result) {
    if (!result?.id) {
      return false;
    }
    const panelKey = getGraphicsFolderPanelKey(graphicsTab);
    const itemIds = getSpriteGraphicsEntries(graphicsTab).map((entry) => String(entry.id || "").trim()).filter(Boolean);
    ensureTileFolderExpanded(result.id, itemIds, panelKey);
    tilePaletteSearchState.active = false;
    tilePaletteSearchState.query = "";
    return selectGraphicsRecord(graphicsTab, result.id, { status: false });
  }

  function getEntitySearchResults() {
    const normalizedQuery = normalizeEntitySearchQuery(entityPaletteSearchState.query).toLocaleLowerCase();
    const catalogEntries = scope.getNpcCatalogRecords();
    const catalogIds = catalogEntries.map((entry) => String(entry?.id || "").trim()).filter(Boolean);
    const catalogLayout = scope.getPanelLayout("instanceTemplates", catalogIds);
    const placedEntries = scope.npcOverlays
      .slice()
      .sort((left, right) => String(left?.name || left?.id || "").localeCompare(String(right?.name || right?.id || ""), undefined, { sensitivity: "base" }));
    const placedIds = placedEntries.map((entry) => String(entry?.id || "").trim()).filter(Boolean);
    const placedLayout = scope.getPanelLayout("instances", placedIds);
    const allResults = [];
    const scoreSearchRecord = (parts, labels) => {
      if (!normalizedQuery) {
        return 0;
      }
      let score = 0;
      labels.forEach(({ value, exact = 200, starts = 120, contains = 60 }) => {
        const lowered = String(value || "").toLocaleLowerCase();
        if (!lowered) {
          return;
        }
        if (lowered === normalizedQuery) {
          score += exact;
        } else if (lowered.startsWith(normalizedQuery)) {
          score += starts;
        } else if (lowered.includes(normalizedQuery)) {
          score += contains;
        }
      });
      if (score <= 0 && !parts.some((part) => String(part || "").toLocaleLowerCase().includes(normalizedQuery))) {
        return -1;
      }
      return score;
    };

    catalogEntries.forEach((entry) => {
      const entityId = String(entry?.id || "").trim();
      const entityName = String(entry?.name || entityId || "Entity").trim() || "Entity";
      const entityType = normalizeEntityType(entry?.entityType, "friendly");
      const folderEntry = findEntityFolderEntry("instanceTemplates", entityId, catalogLayout);
      const folderName = String(folderEntry?.folder?.name || "").trim();
      const spriteId = String(entry?.spriteId || "").trim();
      const title = String(entry?.title || "").trim();
      const tags = Array.isArray(entry?.tags)
        ? entry.tags.map((tag) => String(tag || "").trim()).filter(Boolean)
        : [];
      const score = scoreSearchRecord(
        [entityName, entityId, getEntityTypeLabel(entityType), folderName, spriteId, title, ...tags, "catalog"],
        [
          { value: entityName, exact: 420, starts: 220, contains: 120 },
          { value: entityId, exact: 320, starts: 180, contains: 90 },
          { value: getEntityTypeLabel(entityType), exact: 240, starts: 120, contains: 70 },
          { value: folderName, exact: 150, starts: 90, contains: 50 },
          { value: spriteId, exact: 160, starts: 90, contains: 50 },
          { value: title, exact: 140, starts: 80, contains: 40 },
          ...tags.map((tag) => ({ value: tag, exact: 200, starts: 110, contains: 70 })),
          { value: "catalog", exact: 80, starts: 40, contains: 20 },
        ],
      );
      if (score < 0) {
        return;
      }
      allResults.push({
        kind: "catalog",
        id: entityId,
        entityType,
        entityName,
        folderName,
        spriteId,
        title,
        tags,
        dataUrl: entry?.dataUrl || null,
        entry,
        score,
      });
    });

    placedEntries.forEach((npc) => {
      const entityId = String(npc?.id || "").trim();
      const entityName = String(npc?.name || entityId || "Entity").trim() || "Entity";
      const entityType = scope.getNpcEntityType?.(npc) || "friendly";
      const folderEntry = findEntityFolderEntry("instances", entityId, placedLayout);
      const folderName = String(folderEntry?.folder?.name || "").trim();
      const spriteId = String(npc?.spriteId || "").trim();
      const faction = String(npc?.record?.faction || "").trim();
      const dialogueId = String(npc?.record?.dialogueId || "").trim();
      const tags = Array.isArray(npc?.record?.tags)
        ? npc.record.tags.map((tag) => String(tag || "").trim()).filter(Boolean)
        : [];
      const position = Number.isFinite(Number(npc?.x)) && Number.isFinite(Number(npc?.y))
        ? "(" + Math.floor(Number(npc.x) || 0) + "," + Math.floor(Number(npc.y) || 0) + ")"
        : "unplaced";
      const layer = String(Number(npc?.layer) || 0);
      const score = scoreSearchRecord(
        [entityName, entityId, getEntityTypeLabel(entityType), folderName, spriteId, faction, dialogueId, position, layer, ...tags, "placed"],
        [
          { value: entityName, exact: 420, starts: 220, contains: 120 },
          { value: entityId, exact: 320, starts: 180, contains: 90 },
          { value: getEntityTypeLabel(entityType), exact: 240, starts: 120, contains: 70 },
          { value: folderName, exact: 150, starts: 90, contains: 50 },
          { value: spriteId, exact: 160, starts: 90, contains: 50 },
          { value: faction, exact: 150, starts: 80, contains: 40 },
          { value: dialogueId, exact: 140, starts: 80, contains: 40 },
          { value: position, exact: 160, starts: 80, contains: 50 },
          { value: layer, exact: 70, starts: 40, contains: 20 },
          ...tags.map((tag) => ({ value: tag, exact: 200, starts: 110, contains: 70 })),
          { value: "placed", exact: 80, starts: 40, contains: 20 },
        ],
      );
      if (score < 0) {
        return;
      }
      allResults.push({
        kind: "placed",
        id: entityId,
        entityType,
        entityName,
        folderName,
        spriteId,
        faction,
        dialogueId,
        layer,
        position,
        tags,
        dataUrl: npc?.dataUrl || null,
        npc,
        score,
      });
    });

    return allResults.sort((left, right) => {
      if (Number(right.score) !== Number(left.score)) {
        return Number(right.score) - Number(left.score);
      }
      if (String(left.kind) !== String(right.kind)) {
        return String(left.kind).localeCompare(String(right.kind));
      }
      return String(left.entityName || left.id).localeCompare(String(right.entityName || right.id), undefined, { sensitivity: "base" });
    });
  }

  function selectEntityFromSearchResult(result) {
    if (!result || typeof result !== "object") {
      return false;
    }
    if (result.kind === "catalog" && result.entry) {
      const templateIds = scope.getNpcCatalogRecords().map((entry) => String(entry?.id || "").trim()).filter(Boolean);
      ensureEntityFolderExpanded("instanceTemplates", result.id, templateIds);
      entityPaletteSearchState.active = false;
      entityPaletteSearchState.query = "";
      setActiveEntityCategory(result.entityType, { focusPanel: true, status: false });
      selectInstanceTemplateForPlacement(result.entry);
      return true;
    }
    if (result.kind === "placed" && result.npc) {
      const placedIds = scope.npcOverlays.map((entry) => String(entry?.id || "").trim()).filter(Boolean);
      ensureEntityFolderExpanded("instances", result.id, placedIds);
      entityPaletteSearchState.active = false;
      entityPaletteSearchState.query = "";
      setActiveEntityCategory(result.entityType, { focusPanel: true, status: false });
      scope.selectNpc(result.npc);
      return true;
    }
    return false;
  }

  function renderEntitySearchMode() {
    if (!scope.entitySearchModeHost || !scope.entityCatalogSection || !scope.placedEntitiesSection) {
      return false;
    }
    bindEntitySearchToolbarButton();
    refreshEntitySearchButtonState();
    const isActive = entityPaletteSearchState.active === true;
    scope.entityCatalogSection.classList.toggle("hidden", isActive);
    scope.placedEntitiesSection.classList.toggle("hidden", isActive);
    scope.entitySearchModeHost.classList.toggle("hidden", !isActive);
    if (!isActive) {
      scope.entitySearchModeHost.innerHTML = "";
      return false;
    }
    scope.entitySearchModeHost.innerHTML = "";
    const searchWrapEl = document.createElement("div");
    searchWrapEl.className = "tile-search-mode";
    const searchInputEl = document.createElement("input");
    searchInputEl.type = "text";
    searchInputEl.className = "tile-search-field";
    searchInputEl.placeholder = "Search catalog and placed entities";
    searchInputEl.value = entityPaletteSearchState.query;
    searchInputEl.spellcheck = false;
    searchInputEl.addEventListener("input", () => {
      entityPaletteSearchState.query = searchInputEl.value;
      renderEntitySearchMode();
    });
    searchInputEl.addEventListener("keydown", (event) => {
      if (event.key === "Escape") {
        event.preventDefault();
        entityPaletteSearchState.active = false;
        entityPaletteSearchState.query = "";
        renderInstancePalette();
        scope.renderNpcList();
        return;
      }
      if (event.key === "Enter") {
        const firstResult = getEntitySearchResults()[0];
        if (firstResult) {
          event.preventDefault();
          selectEntityFromSearchResult(firstResult);
        }
      }
    });
    const searchMetaEl = document.createElement("div");
    searchMetaEl.className = "tile-search-meta";
    const searchResultsEl = document.createElement("div");
    searchResultsEl.className = "tile-search-results";
    const results = getEntitySearchResults();
    searchMetaEl.textContent = results.length === 1
      ? "1 matching entity"
      : `${results.length} matching entities`;
    if (results.length <= 0) {
      const emptyEl = document.createElement("div");
      emptyEl.className = "tile-search-empty";
      emptyEl.textContent = "No entities matched that search. Try name, id, type, folder, sprite, faction, dialogue, position, or tag.";
      searchResultsEl.appendChild(emptyEl);
    } else {
      results.forEach((result) => {
        const btn = document.createElement("button");
        btn.type = "button";
        const isActive = result.kind === "catalog"
          ? String(scope.activeInstanceBrushId || "").trim() === String(result.id || "").trim()
          : String(scope.selectedNpcId || "").trim() === String(result.id || "").trim();
        btn.className = "history-row npc-row tile-search-result-row" + (isActive ? " active" : "");
        const header = document.createElement("div");
        header.className = "npc-row-header";
        const thumb = result.dataUrl
          ? '<img class="npc-thumb" alt="entity sprite" src="' + scope.runtimeEscapeHtml(result.dataUrl) + '">'
          : '<span class="npc-thumb-fallback">NPC</span>';
        const metaParts = [
          result.kind === "catalog" ? "catalog" : "placed",
          '<span class="entity-type-badge">' + scope.runtimeEscapeHtml(getEntityTypeLabel(result.entityType)) + "</span>",
          scope.runtimeEscapeHtml(String(result.id || "")),
        ];
        if (result.folderName) {
          metaParts.push("folder: " + scope.runtimeEscapeHtml(result.folderName));
        }
        if (result.kind === "catalog") {
          if (result.spriteId) {
            metaParts.push("sprite: " + scope.runtimeEscapeHtml(result.spriteId));
          }
          if (result.title) {
            metaParts.push("title: " + scope.runtimeEscapeHtml(result.title));
          }
          if (Array.isArray(result.tags) && result.tags.length > 0) {
            metaParts.push("tags: " + scope.runtimeEscapeHtml(result.tags.join(", ")));
          }
        } else {
          metaParts.push("layer: " + scope.runtimeEscapeHtml(String(result.layer || "0")));
          metaParts.push("pos: " + scope.runtimeEscapeHtml(String(result.position || "unplaced")));
          if (result.faction) {
            metaParts.push("faction: " + scope.runtimeEscapeHtml(result.faction));
          }
          if (result.dialogueId) {
            metaParts.push("dialogue: " + scope.runtimeEscapeHtml(result.dialogueId));
          }
          if (Array.isArray(result.tags) && result.tags.length > 0) {
            metaParts.push("tags: " + scope.runtimeEscapeHtml(result.tags.join(", ")));
          }
        }
        header.innerHTML =
          thumb +
          "<div><span>" + scope.runtimeEscapeHtml(String(result.entityName || result.id || "Entity")) + "</span>" +
          '<span class="history-meta">' + metaParts.join(" | ") + "</span></div>";
        header.addEventListener("click", () => {
          selectEntityFromSearchResult(result);
        });
        btn.appendChild(header);
        searchResultsEl.appendChild(btn);
      });
    }
    searchWrapEl.appendChild(searchInputEl);
    searchWrapEl.appendChild(searchMetaEl);
    searchWrapEl.appendChild(searchResultsEl);
    scope.entitySearchModeHost.appendChild(searchWrapEl);
    window.requestAnimationFrame(() => {
      if (entityPaletteSearchState.active && searchInputEl.isConnected) {
        searchInputEl.focus();
        searchInputEl.setSelectionRange(searchInputEl.value.length, searchInputEl.value.length);
      }
    });
    return true;
  }

  function openTilePaletteContextMenu(tileId, tile, event, options = {}) {
    event.preventDefault();
    event.stopPropagation();
    const normalizedTileId = String(tileId || "").trim();
    const tileEntry = tile || scope.getTileEntryById(normalizedTileId);
    const targetLayer = scope.getEditableLayerNumber();
    const editingHeightLayer = scope.isEditingHeightLayer();
    const replaceSourceTileId = getReplaceSourceTileId(normalizedTileId, targetLayer);
    const canReplace = !editingHeightLayer && !!replaceSourceTileId && replaceSourceTileId !== normalizedTileId;
    const canFill = !editingHeightLayer && scope.isBackgroundLayer(targetLayer);
    const symbol = String(tileEntry.symbol || ".").charAt(0) || ".";
    const buildItems = typeof options.buildItems === "function" ? options.buildItems : null;
    const tooltipId = String(options.tooltipId || ("tile-palette:" + normalizedTileId));
    const menuItems = [
      menuLabel(tileEntry.name || normalizedTileId || "Tile"),
      menuItem("<span>Select tile</span>", () => {
        selectPaintTile(normalizedTileId);
        scope.atTooltip.close();
      }, normalizedTileId === scope.activeBrushTileId ? "active" : ""),
      menuItem("<span>Edit Tile</span>", () => {
        if (scope.openTileArtEditorWindow?.(normalizedTileId)) {
          scope.atTooltip.close();
        }
      }),
      menuItem("<span>Duplicate Tile</span>", () => {
        void Promise.resolve(scope.duplicateGraphicRecord?.("tile", normalizedTileId)).then((duplicated) => {
          if (duplicated) {
            scope.atTooltip.close();
          }
        });
      }),
      menuItem("<span>Assign to Sprites</span>", () => {
        void Promise.resolve(scope.assignGraphicToCategory?.("tile", normalizedTileId, "sprites")).then((changed) => {
          if (changed) {
            scope.atTooltip.close();
          }
        });
      }),
      menuItem("<span>Assign to Other</span>", () => {
        void Promise.resolve(scope.assignGraphicToCategory?.("tile", normalizedTileId, "other")).then((changed) => {
          if (changed) {
            scope.atTooltip.close();
          }
        });
      }),
      menuSeparator(),
      menuItem("<span>Fill Background</span>", () => {
        if (fillEditableLayerWithTile(normalizedTileId)) {
          scope.atTooltip.close();
        }
      }, "", { disabled: !canFill }),
      menuItem("<span>Replace matching tile on current layer</span>", () => {
        if (replaceMatchingTileOnEditableLayer(normalizedTileId)) {
          scope.atTooltip.close();
        }
      }, "", { disabled: !canReplace }),
    ];
    const extraItems = buildItems?.({
        tileId: normalizedTileId,
        tile: tileEntry,
        symbol,
        targetLayer,
        canFill,
        canReplace,
      }) || [];
    menuItems.push(
      ...extraItems,
      menuSeparator(),
      menuItem("<span>Delete tile</span>", () => {
        void Promise.resolve(scope.deleteTileEverywhere?.(normalizedTileId)).then((deleted) => {
          if (deleted) {
            scope.atTooltip.close();
          }
        });
      }),
    );
    openContextMenuAtPoint(scope.atTooltip, event.clientX, event.clientY, menuItems, tooltipId);
    if (options.status !== false) {
      setStatus("Opened tile menu for " + scope.describeBrushTileId(normalizedTileId) + ".", false);
    }
  }

  function getTileIdForCell(layerNumber, tileX, tileY) {
    const normalizedLayer = Number(layerNumber) || 0;
    const layerEntry = scope.roomLayers.find((entry) => Number(entry.layer) === normalizedLayer);
    if (!layerEntry) {
      return "";
    }
    const fillChar = scope.getRowFillChar(normalizedLayer);
    const rows = scope.normalizeRows(layerEntry.rows, fillChar);
    const row = rows[tileY] || fillChar.repeat(scope.width);
    const symbol = String(row.charAt(tileX) || fillChar).charAt(0) || fillChar;
    if (normalizedLayer === 0 && symbol === " ") {
      return "";
    }
    if (symbol === fillChar) {
      if (normalizedLayer === 0) {
        const backgroundTileId = typeof scope.getBackgroundTileIdForLocalTile === "function"
          ? String(scope.getBackgroundTileIdForLocalTile(tileX, tileY) || "").trim()
          : String(scope.backgroundTileId || "").trim();
        if (backgroundTileId) {
          return backgroundTileId;
        }
      }
      return "";
    }
    const fallbackEntry = Object.values(scope.tileCatalogById).find((entry) => String(entry?.symbol || "").charAt(0) === symbol);
    return fallbackEntry ? String(fallbackEntry.id || "").trim() : "";
  }

  function getReplaceSourceTileId(targetTileId, layerNumber) {
    if (scope.selectedTile && Number(scope.selectedTile.layer) === Number(layerNumber)) {
      const selectedTileId = getTileIdForCell(layerNumber, Number(scope.selectedTile.x) || 0, Number(scope.selectedTile.y) || 0);
      if (selectedTileId) {
        return selectedTileId;
      }
    }
    const activeTileId = String(scope.activeBrushTileId || "").trim();
    if (activeTileId) {
      return activeTileId;
    }
    return "";
  }

  function fillEditableLayerWithTile(tileId) {
    const normalizedTileId = String(tileId || "").trim();
    const targetLayer = scope.getEditableLayerNumber();
    if (!scope.isBackgroundLayer(targetLayer)) {
      setStatus("Roomwide fill is only available on Background.", true);
      return false;
    }
    const existingBackgroundTileId = String(scope.backgroundTileId || "").trim();
    const layerEntry = scope.getLayerByNumber(targetLayer);
    const rows = scope.normalizeRows(layerEntry?.rows || [], ".");
    const explicitOverrideCount = rows.reduce((count, row) => (
      count + Array.from(String(row || "")).filter((symbol) => symbol !== ".").length
    ), 0);
    if (existingBackgroundTileId === normalizedTileId && explicitOverrideCount === 0) {
      setStatus("Background is already filled with " + scope.describeBrushTileId(normalizedTileId) + ".", false);
      return false;
    }
    const changedCells = existingBackgroundTileId === normalizedTileId
      ? explicitOverrideCount
      : scope.width * scope.height;
    scope.setLayerVisibility(targetLayer, true);
    scope.backgroundTileId = normalizedTileId;
    scope.roomLayers = scope.roomLayers.map((layer) => (
      Number(layer.layer) !== targetLayer
        ? layer
        : { ...layer, rows: scope.normalizeRows([], ".") }
    ));
    scope.rebuildVisibleWorldChunksFromDocument?.();
    scope.activeBrushTileId = normalizedTileId;
    renderPaintPalette();
    refreshInformationPanel();
    scope.draw();
    scope.registerHistory("Background filled", "layer:" + targetLayer, "tile:" + normalizedTileId, [
      "Layer: " + targetLayer,
      "Tile: " + scope.describeBrushTileId(normalizedTileId),
      "Cells changed: " + changedCells,
    ]);
    if (scope.backgroundCellMode !== "tile") {
      scope.backgroundCellMode = "tile";
    }
    refreshBackgroundModeButton();
    setStatus("Filled Background with " + scope.describeBrushTileId(normalizedTileId) + ".", false);
    return true;
  }

  function replaceMatchingTileOnEditableLayer(targetTileId) {
    const normalizedTargetTileId = String(targetTileId || "").trim();
    const targetLayer = scope.getEditableLayerNumber();
    const sourceTileId = getReplaceSourceTileId(normalizedTargetTileId, targetLayer);
    if (!sourceTileId) {
      setStatus("Select a tile on the canvas or choose an active tile first.", true);
      return false;
    }
    if (sourceTileId === normalizedTargetTileId) {
      setStatus("Pick a different destination tile to replace " + scope.describeBrushTileId(sourceTileId) + ".", true);
      return false;
    }
    const targetTileEntry = scope.getTileEntryById(normalizedTargetTileId);
    const targetSymbol = String(targetTileEntry.symbol || ".").charAt(0) || ".";
    const isImplicitBackgroundReplace = scope.isBackgroundLayer(targetLayer) && sourceTileId === String(scope.backgroundTileId || "").trim();
    if (isImplicitBackgroundReplace) {
      const layerEntry = scope.getLayerByNumber(targetLayer);
      const rows = scope.normalizeRows(layerEntry?.rows || [], ".");
      const explicitMatchCells = [];
      let matchedCells = 0;
      for (let y = 0; y < scope.height; y += 1) {
        for (let x = 0; x < scope.width; x += 1) {
          if (getTileIdForCell(targetLayer, x, y) !== sourceTileId) {
            continue;
          }
          matchedCells += 1;
          const rowSymbol = String((rows[y] || "").charAt(x) || ".").charAt(0) || ".";
          if (rowSymbol !== ".") {
            explicitMatchCells.push({ x, y });
          }
        }
      }
      if (matchedCells === 0) {
        setStatus("No matching " + scope.describeBrushTileId(sourceTileId) + " tiles found on layer " + targetLayer + ".", false);
        return false;
      }
      scope.backgroundTileId = normalizedTargetTileId;
      explicitMatchCells.forEach((cell) => {
        scope.setTileCharAt(targetLayer, cell.x, cell.y, targetSymbol, normalizedTargetTileId);
      });
      scope.rebuildVisibleWorldChunksFromDocument?.();
      scope.activeBrushTileId = normalizedTargetTileId;
      renderPaintPalette();
      scope.draw();
      refreshBackgroundModeButton();
      scope.registerHistory("Matching tiles replaced", sourceTileId, normalizedTargetTileId, [
        "Layer: " + targetLayer,
        "Source: " + scope.describeBrushTileId(sourceTileId),
        "Target: " + scope.describeBrushTileId(normalizedTargetTileId),
        "Cells changed: " + matchedCells,
      ]);
      setStatus("Replaced " + matchedCells + " matching tiles on layer " + targetLayer + ".", false);
      return true;
    }
    let matchedCells = 0;
    let changedCells = 0;
    scope.setLayerVisibility(targetLayer, true);
    scope.beginTileInstanceMutationBatch();
    try {
      for (let y = 0; y < scope.height; y += 1) {
        for (let x = 0; x < scope.width; x += 1) {
          if (getTileIdForCell(targetLayer, x, y) !== sourceTileId) {
            continue;
          }
          matchedCells += 1;
          if (scope.setTileCharAt(targetLayer, x, y, targetSymbol, normalizedTargetTileId) !== null) {
            changedCells += 1;
          }
        }
      }
    } finally {
      scope.endTileInstanceMutationBatch();
    }
    if (matchedCells === 0) {
      setStatus("No " + scope.describeBrushTileId(sourceTileId) + " tiles found on layer " + targetLayer + ".", true);
      return false;
    }
    if (changedCells === 0) {
      setStatus("No tile replacements were needed on layer " + targetLayer + ".", false);
      return false;
    }
    scope.activeBrushTileId = normalizedTargetTileId;
    renderPaintPalette();
    refreshInformationPanel();
    scope.draw();
    scope.registerHistory("Layer tile replaced", "tile:" + sourceTileId, "tile:" + normalizedTargetTileId, [
      "Layer: " + targetLayer,
      "From: " + scope.describeBrushTileId(sourceTileId),
      "To: " + scope.describeBrushTileId(normalizedTargetTileId),
      "Cells changed: " + changedCells,
    ]);
    setStatus("Replaced " + scope.describeBrushTileId(sourceTileId) + " with " + scope.describeBrushTileId(normalizedTargetTileId) + " on layer " + targetLayer + ".", false);
    return true;
  }

  function openSpriteGraphicsContextMenu(graphicsTab, entry, event) {
    event.preventDefault();
    event.stopPropagation();
    const normalizedTab = graphicsTab === "other" ? "other" : "sprites";
    const menuItems = [
      menuLabel(entry.name || entry.id || "Graphic"),
      menuItem("<span>Select graphic</span>", () => {
        selectGraphicsRecord(normalizedTab, entry.id);
        scope.atTooltip.close();
      }, String(scope.activeGraphicsRecordId || "").trim() === String(entry.id || "").trim() ? "active" : ""),
      menuItem("<span>" + (normalizedTab === "sprites" ? "Edit Sprite" : "Edit Graphic") + "</span>", () => {
        if (scope.openTileArtEditorWindow?.(normalizedTab === "other" ? "other" : "sprite", entry.id)) {
          scope.atTooltip.close();
        }
      }),
      menuItem("<span>" + (normalizedTab === "sprites" ? "Duplicate Sprite" : "Duplicate Graphic") + "</span>", () => {
        void Promise.resolve(scope.duplicateGraphicRecord?.(normalizedTab, entry.id)).then((duplicated) => {
          if (duplicated) {
            scope.atTooltip.close();
          }
        });
      }),
      menuSeparator(),
      menuItem("<span>Assign to Tiles</span>", () => {
        void Promise.resolve(scope.assignGraphicToCategory?.(normalizedTab, entry.id, "tiles")).then((changed) => {
          if (changed) {
            scope.atTooltip.close();
          }
        });
      }),
    ];
      if (normalizedTab === "sprites") {
        menuItems.push(menuItem("<span>Move to Other</span>", () => {
          void Promise.resolve(scope.assignGraphicToCategory?.("sprite", entry.id, "other")).then((changed) => {
            if (changed) {
              scope.atTooltip.close();
            }
          });
        }));
      } else {
        menuItems.push(menuItem("<span>Move to Sprites</span>", () => {
          void Promise.resolve(scope.assignGraphicToCategory?.("other", entry.id, "sprites")).then((changed) => {
            if (changed) {
              scope.atTooltip.close();
            }
          });
        }));
      }
    menuItems.push(
      menuSeparator(),
      menuItem("<span>Delete " + (normalizedTab === "sprites" ? "Sprite" : "Graphic") + "</span>", () => {
        void Promise.resolve(scope.deleteSpriteEverywhere?.(entry.id)).then((deleted) => {
          if (deleted) {
            scope.atTooltip.close();
          }
        });
      }),
    );
    openContextMenuAtPoint(scope.atTooltip, event.clientX, event.clientY, menuItems, "graphics-palette:" + normalizedTab + ":" + entry.id);
    setStatus("Opened graphic menu for " + (entry.name || entry.id) + ".", false);
  }

  function openHeightLayerContextMenu(entry, event) {
    const entryId = String(entry?.id || "").trim();
    if (!entryId || !event) {
      return false;
    }
    const isActive = scope.editingTargetKind === "height" && String(scope.activeHeightLayerId || "").trim() === entryId;
    event.preventDefault();
    event.stopPropagation();
    openContextMenuAtPoint(scope.atTooltip, event.clientX, event.clientY, [
      menuLabel(scope.getHeightLayerDisplayName(entry)),
      menuItem("<span>Edit this height layer</span>", () => {
        selectHeightLayerForEditing(entryId);
        scope.atTooltip.close();
      }, isActive ? "active" : ""),
      menuItem("<span>Rename height layer</span>", () => {
        if (renameHeightLayer(entryId)) {
          scope.atTooltip.close();
        }
      }),
      menuItem("<span>Duplicate height layer</span>", () => {
        if (duplicateHeightLayer(entryId)) {
          scope.atTooltip.close();
        }
      }),
      menuItem("<span>Delete height layer</span>", () => {
        if (deleteHeightLayer(entryId)) {
          scope.atTooltip.close();
        }
      }),
    ], "height-layer:" + entryId);
    setStatus("Opened height layer menu for " + scope.getHeightLayerDisplayName(entry) + ".", false);
    return true;
  }

  function openCatalogEntityContextMenu(entry, event) {
    const entryId = String(entry?.id || "").trim();
    if (!entryId || !event) {
      return false;
    }
    event.preventDefault();
    event.stopPropagation();
    const placedNpc = scope.findPlacedNpcByTemplateId(entryId);
    const selectedNpc = scope.npcOverlays.find((npc) => npc.id === scope.selectedNpcId) || null;
    const menuItems = [menuLabel(entry.name || entryId)];
      if (scope.activeInstanceBrushId === entryId) {
        menuItems.push(menuItem("<span>Clear placement selection</span>", () => {
          clearInstanceTemplatePlacement();
          scope.atTooltip.close();
        }, "active"));
      } else {
        menuItems.push(menuItem("<span>Select for placement</span>", () => {
          selectInstanceTemplateForPlacement(entry);
          scope.atTooltip.close();
        }));
      }
      menuItems.push(menuItem("<span>Jump to placed entity</span>", () => {
        if (focusPlacedInstanceForTemplate(entry)) {
          scope.atTooltip.close();
        }
      }, "", { disabled: !placedNpc }));
      menuItems.push(menuItem("<span>Assign to selected entity</span>", () => {
        if (!selectedNpc) {
          return;
        }
        scope.assignNpcToSlot(selectedNpc.id, entryId);
        scope.atTooltip.close();
      }, "", { disabled: !selectedNpc }), menuSeparator(), menuLabel("Entity Type"));
      ["friendly", "hostile", "prop"].forEach((type) => {
        menuItems.push(menuItem(
          "<span>" + scope.runtimeEscapeHtml(getEntityTypeLabel(type)) + "</span>",
          () => {
            if (scope.setNpcCatalogEntityType?.(entryId, type)) {
              setActiveEntityCategory(type, { focusPanel: false, status: false });
              scope.atTooltip.close();
            }
          },
          normalizeEntityType(entry.entityType, "friendly") === type ? "active" : "",
        ));
      });
    openContextMenuAtPoint(scope.atTooltip, event.clientX, event.clientY, menuItems, "instance-template:" + entryId);
    setStatus("Opened entity menu for " + (entry.name || entry.id) + ".", false);
    return true;
  }

  function openDrawLayerContextMenu(layer, event) {
    const layerNumber = Number(layer?.layer) || 0;
    if (!event) {
      return false;
    }
    const layerLabel = scope.getLayerDisplayName(layer);
    const isActiveLayer = scope.editingTargetKind !== "height" && !scope.viewingAllLayers && layerNumber === Number(scope.activeLayer);
    const nonBaseLayers = scope.roomLayers
      .map((entry) => Number(entry.layer) || 0)
      .filter((entry) => entry > 0)
      .sort((left, right) => left - right);
    const isTopLayer = layerNumber > 0 && nonBaseLayers.length > 0 && layerNumber === nonBaseLayers[nonBaseLayers.length - 1];
    const isBottomNonBaseLayer = layerNumber > 0 && nonBaseLayers.length > 0 && layerNumber === nonBaseLayers[0];
    event.preventDefault();
    event.stopPropagation();
    openContextMenuAtPoint(scope.atTooltip, event.clientX, event.clientY, [
      menuLabel(layerLabel + " | depth " + layerNumber),
      menuItem("<span>Edit on this layer</span>", () => {
        activateLayerForEditing(layerNumber);
        scope.atTooltip.close();
      }, isActiveLayer ? "active" : ""),
      menuItem("<span>Show only this layer</span>", () => {
        isolateLayer(layerNumber);
        scope.atTooltip.close();
      }),
      menuItem("<span>Reveal every layer</span>", () => {
        revealAllLayers();
        scope.atTooltip.close();
      }),
      menuSeparator(),
      menuItem("<span>Rename layer</span>", () => {
        if (renameLayerByNumber(layerNumber)) {
          scope.atTooltip.close();
        }
      }),
      menuItem("<span>Move to top</span>", () => {
        if (moveLayerToExtreme(layerNumber, "top")) {
          scope.atTooltip.close();
        }
      }, "", { disabled: layerNumber === 0 || isTopLayer }),
      menuItem("<span>Move toward base</span>", () => {
        if (moveLayerToExtreme(layerNumber, "bottom")) {
          scope.atTooltip.close();
        }
      }, "", { disabled: layerNumber === 0 || isBottomNonBaseLayer }),
      menuSeparator(),
      menuItem("<span>Delete layer</span>", () => {
        deleteLayerByNumber(layerNumber);
        scope.atTooltip.close();
      }, "", { disabled: layerNumber === 0 }),
    ], "layer-row:" + layerNumber);
    setStatus("Opened layer menu for layer " + layerNumber + ".", false);
    return true;
  }

  function renderPaintPalette() {
    const activeGraphicsTab = getActiveGraphicsTab();
    const tileIds = scope.getPaintableTileIds();
    refreshGraphicsCategoryTabs();
    bindTileSearchToolbarButton();
    refreshTileSearchButtonState();
    if (scope.activeBrushTileId && !tileIds.includes(scope.activeBrushTileId)) {
      scope.activeBrushTileId = "";
    }
    if (activeGraphicsTab !== "tiles") {
      const spriteEntries = getSpriteGraphicsEntries(activeGraphicsTab);
      const panelKey = getGraphicsFolderPanelKey(activeGraphicsTab);
      const itemIds = spriteEntries.map((entry) => String(entry.id || "").trim()).filter(Boolean);
      if (scope.activeGraphicsRecordId && !itemIds.includes(String(scope.activeGraphicsRecordId || "").trim())) {
        scope.activeGraphicsRecordId = itemIds[0] || "";
      }
      if (tilePaletteSearchState.active) {
        scope.paintPaletteEl.innerHTML = "";
        const searchWrapEl = document.createElement("div");
        searchWrapEl.className = "tile-search-mode";
        const searchInputEl = document.createElement("input");
        searchInputEl.type = "text";
        searchInputEl.className = "tile-search-field";
        searchInputEl.placeholder = "Search by name, id, folder, or tags";
        searchInputEl.value = tilePaletteSearchState.query;
        searchInputEl.spellcheck = false;
        searchInputEl.addEventListener("input", () => {
          tilePaletteSearchState.query = searchInputEl.value;
          renderPaintPalette();
        });
        searchInputEl.addEventListener("keydown", (event) => {
          if (event.key === "Escape") {
            event.preventDefault();
            tilePaletteSearchState.active = false;
            tilePaletteSearchState.query = "";
            renderPaintPalette();
            return;
          }
          if (event.key === "Enter") {
            const firstResult = getSpriteGraphicSearchResults(activeGraphicsTab, spriteEntries)[0];
            if (firstResult) {
              event.preventDefault();
              selectSpriteGraphicFromSearchResult(activeGraphicsTab, firstResult);
            }
          }
        });
        const searchMetaEl = document.createElement("div");
        searchMetaEl.className = "tile-search-meta";
        const searchResultsEl = document.createElement("div");
        searchResultsEl.className = "tile-search-results";
        const results = getSpriteGraphicSearchResults(activeGraphicsTab, spriteEntries);
        searchMetaEl.textContent = results.length === 1
          ? "1 matching graphic"
          : `${results.length} matching graphics`;
        if (results.length <= 0) {
          const emptyEl = document.createElement("div");
          emptyEl.className = "tile-search-empty";
          emptyEl.textContent = "No graphics matched that search. Try name, id, folder, or tag.";
          searchResultsEl.appendChild(emptyEl);
        } else {
          results.forEach((result) => {
            const btn = document.createElement("button");
            btn.type = "button";
            btn.className = "history-row npc-row tile-search-result-row" + (String(result.id || "").trim() === String(scope.activeGraphicsRecordId || "").trim() ? " active" : "");
            const header = document.createElement("div");
            header.className = "npc-row-header";
            const thumb = result.dataUrl
              ? '<img class="npc-thumb" alt="graphic preview" src="' + scope.runtimeEscapeHtml(result.dataUrl) + '">'
              : '<span class="npc-thumb-fallback">Art</span>';
            const metaParts = [scope.runtimeEscapeHtml(String(result.id || ""))];
            if (result.folderName) {
              metaParts.push("folder: " + scope.runtimeEscapeHtml(result.folderName));
            }
            if (Array.isArray(result.tags) && result.tags.length > 0) {
              metaParts.push("tags: " + scope.runtimeEscapeHtml(result.tags.join(", ")));
            }
            header.innerHTML =
              thumb +
              "<div><span>" + scope.runtimeEscapeHtml(String(result.name || result.id || "Graphic")) + "</span>" +
              '<span class="history-meta">' + metaParts.join(" | ") + "</span></div>";
            header.addEventListener("click", () => {
              selectSpriteGraphicFromSearchResult(activeGraphicsTab, result);
            });
            header.addEventListener("contextmenu", (event) => {
              openSpriteGraphicsContextMenu(activeGraphicsTab, result, event);
            });
            btn.appendChild(header);
            searchResultsEl.appendChild(btn);
          });
        }
        searchWrapEl.appendChild(searchInputEl);
        searchWrapEl.appendChild(searchMetaEl);
        searchWrapEl.appendChild(searchResultsEl);
        scope.paintPaletteEl.appendChild(searchWrapEl);
        window.requestAnimationFrame(() => {
          if (tilePaletteSearchState.active && searchInputEl.isConnected) {
            searchInputEl.focus();
            searchInputEl.setSelectionRange(searchInputEl.value.length, searchInputEl.value.length);
          }
        });
        return;
      }
      renderFolderedSelectorList({
        scope,
        container: scope.paintPaletteEl,
        panelKey,
        items: spriteEntries,
        getItemId: (entry) => entry.id,
        emptyMessage: activeGraphicsTab === "other" ? "No other graphics found." : "No sprites found.",
        baseLabel: "Base Panel",
        onMove: (dragging, dropTarget) => {
          movePanelNodeAndRefresh(panelKey, itemIds, dragging, dropTarget, renderPaintPalette);
        },
        onToggleFolder: (folderId) => {
          togglePanelFolderAndRefresh(panelKey, itemIds, folderId, renderPaintPalette);
        },
        onRenameFolder: (folderId) => {
          renamePanelFolderAndRefresh(panelKey, itemIds, folderId, renderPaintPalette);
        },
        onDeleteFolder: (folderId) => {
          deletePanelFolderAndRefresh(panelKey, itemIds, folderId, renderPaintPalette);
        },
        renderItemRow: (entry) => {
          const btn = document.createElement("div");
          btn.className = "history-row npc-row" + (String(entry.id || "").trim() === String(scope.activeGraphicsRecordId || "").trim() ? " active" : "");
          const header = document.createElement("div");
          header.className = "npc-row-header";
          const thumb = entry.dataUrl
            ? '<img class="npc-thumb" alt="graphic preview" src="' + scope.runtimeEscapeHtml(entry.dataUrl) + '">'
            : '<span class="npc-thumb-fallback">Art</span>';
          const metaParts = [scope.runtimeEscapeHtml(String(entry.id || ""))];
          if (Array.isArray(entry.tags) && entry.tags.length > 0) {
            metaParts.push("tags: " + scope.runtimeEscapeHtml(entry.tags.join(", ")));
          }
          header.innerHTML =
            thumb +
            "<div><span>" + scope.runtimeEscapeHtml(String(entry.name || entry.id || "Graphic")) + "</span>" +
            '<span class="history-meta">' + metaParts.join(" | ") + "</span></div>";
          header.addEventListener("click", () => {
            selectGraphicsRecord(activeGraphicsTab, entry.id);
          });
          header.addEventListener("dblclick", () => {
            scope.openTileArtEditorWindow?.(activeGraphicsTab === "other" ? "other" : "sprite", entry.id);
          });
          header.addEventListener("contextmenu", (event) => {
            openSpriteGraphicsContextMenu(activeGraphicsTab, entry, event);
          });
          btn.appendChild(header);
          return btn;
        },
      });
      return;
    }
    const tileEntries = tileIds.map((tileId) => ({
      id: tileId,
      tile: scope.getTileEntryById(tileId),
    }));
    if (tilePaletteSearchState.active) {
      scope.paintPaletteEl.innerHTML = "";
      const searchWrapEl = document.createElement("div");
      searchWrapEl.className = "tile-search-mode";
      const searchInputEl = document.createElement("input");
      searchInputEl.type = "text";
      searchInputEl.className = "tile-search-field";
      searchInputEl.placeholder = "Search by name, id, symbol, folder, or tags";
      searchInputEl.value = tilePaletteSearchState.query;
      searchInputEl.spellcheck = false;
      searchInputEl.addEventListener("input", () => {
        tilePaletteSearchState.query = searchInputEl.value;
        renderPaintPalette();
      });
      searchInputEl.addEventListener("keydown", (event) => {
        if (event.key === "Escape") {
          event.preventDefault();
          tilePaletteSearchState.active = false;
          tilePaletteSearchState.query = "";
          renderPaintPalette();
          return;
        }
        if (event.key === "Enter") {
          const firstResult = getTileSearchResults(tileEntries, tileIds)[0];
          if (firstResult) {
            event.preventDefault();
            selectTileFromSearchResult(firstResult.tileId, tileIds);
          }
        }
      });
      const searchMetaEl = document.createElement("div");
      searchMetaEl.className = "tile-search-meta";
      const searchResultsEl = document.createElement("div");
      searchResultsEl.className = "tile-search-results";
      const results = getTileSearchResults(tileEntries, tileIds);
      searchMetaEl.textContent = results.length === 1
        ? "1 matching tile"
        : `${results.length} matching tiles`;
      if (results.length <= 0) {
        const emptyEl = document.createElement("div");
        emptyEl.className = "tile-search-empty";
        emptyEl.textContent = "No tiles matched that search. Try name, id, symbol, folder, or tag.";
        searchResultsEl.appendChild(emptyEl);
      } else {
        results.forEach((result) => {
          const btn = document.createElement("button");
          btn.type = "button";
          btn.className = "history-row npc-row tile-search-result-row" + (result.tileId === scope.activeBrushTileId ? " active" : "");
          const header = document.createElement("div");
          header.className = "npc-row-header";
          const thumb = result.tile.dataUrl
            ? '<img class="npc-thumb" alt="tile sprite" src="' + scope.runtimeEscapeHtml(result.tile.dataUrl) + '">'
            : '<span class="npc-thumb-fallback">Tile</span>';
          const metaParts = [
            scope.runtimeEscapeHtml(String(result.tileId || "")),
          ];
          if (result.folderName) {
            metaParts.push("folder: " + scope.runtimeEscapeHtml(result.folderName));
          }
          if (result.tags.length > 0) {
            metaParts.push("tags: " + scope.runtimeEscapeHtml(result.tags.join(", ")));
          }
          header.innerHTML =
            thumb +
            "<div><span>" + scope.runtimeEscapeHtml(String(result.tileName || result.tileId || "Tile")) + "</span>" +
            '<span class="history-meta">' + metaParts.join(" | ") + "</span></div>";
          header.addEventListener("click", () => {
            selectTileFromSearchResult(result.tileId, tileIds);
          });
          header.addEventListener("contextmenu", (event) => {
            openTilePaletteContextMenu(result.tileId, result.tile, event);
          });
          btn.appendChild(header);
          searchResultsEl.appendChild(btn);
        });
      }
      searchWrapEl.appendChild(searchInputEl);
      searchWrapEl.appendChild(searchMetaEl);
      searchWrapEl.appendChild(searchResultsEl);
      scope.paintPaletteEl.appendChild(searchWrapEl);
      window.requestAnimationFrame(() => {
        if (tilePaletteSearchState.active && searchInputEl.isConnected) {
          searchInputEl.focus();
          searchInputEl.setSelectionRange(searchInputEl.value.length, searchInputEl.value.length);
        }
      });
      return;
    }
    renderFolderedSelectorList({
      scope,
      container: scope.paintPaletteEl,
      panelKey: "tiles",
      items: tileEntries,
      getItemId: (entry) => entry.id,
      emptyMessage: "No tiles found.",
      baseLabel: "Base Panel",
      onMove: (dragging, dropTarget) => {
        movePanelNodeAndRefresh("tiles", tileIds, dragging, dropTarget, renderPaintPalette);
      },
      onToggleFolder: (folderId) => {
        togglePanelFolderAndRefresh("tiles", tileIds, folderId, renderPaintPalette);
      },
      onRenameFolder: (folderId) => {
        renamePanelFolderAndRefresh("tiles", tileIds, folderId, renderPaintPalette);
      },
      onDeleteFolder: (folderId) => {
        deletePanelFolderAndRefresh("tiles", tileIds, folderId, renderPaintPalette);
      },
      renderItemRow: (entry) => {
        const tileId = entry.id;
        const tile = entry.tile;
        const btn = document.createElement("div");
        btn.className = "history-row npc-row" + (tileId === scope.activeBrushTileId ? " active" : "");

        const header = document.createElement("div");
        header.className = "npc-row-header";
        const thumb = tile.dataUrl
          ? '<img class="npc-thumb" alt="tile sprite" src="' + scope.runtimeEscapeHtml(tile.dataUrl) + '">'
          : '<span class="npc-thumb-fallback">Tile</span>';
        const meta = scope.runtimeEscapeHtml(String(tileId || ""));
        header.innerHTML =
          thumb +
          "<div><span>" + scope.runtimeEscapeHtml(String(tile.name || tileId || "Tile")) + "</span>" +
          '<span class="history-meta">' + meta + "</span></div>";
        header.addEventListener("click", () => {
          selectPaintTile(tileId);
        });
        header.addEventListener("contextmenu", (event) => {
          openTilePaletteContextMenu(tileId, tile, event);
        });
        btn.appendChild(header);
        return btn;
      },
    });
  }

  function renderHeightLayerList() {
    if (!scope.heightLayerListEl) {
      return;
    }
    const activeHeightLayerId = String(scope.activeHeightLayerId || "").trim();
    scope.heightLayerListEl.innerHTML = "";
    const entries = getSortedHeightLayers();
    if (entries.length === 0) {
      const emptyEl = document.createElement("div");
      emptyEl.className = "history-preview-empty";
      emptyEl.textContent = "No height layers yet. Add one to start painting sparse overrides.";
      scope.heightLayerListEl.appendChild(emptyEl);
      return;
    }
    entries.forEach((entry) => {
      const entryId = String(entry.id || "").trim();
      const isActive = scope.editingTargetKind === "height" && activeHeightLayerId === entryId;
      const wrap = document.createElement("div");
      wrap.className = "layer-row-wrap";
      wrap.setAttribute("data-height-layer-id", entryId);
      const dragHandleBtn = document.createElement("button");
      dragHandleBtn.type = "button";
      dragHandleBtn.className = "layer-drag-handle";
      dragHandleBtn.title = "Drag to change height order";
      dragHandleBtn.setAttribute("aria-label", dragHandleBtn.title);
      dragHandleBtn.innerHTML = '<span class="layer-drag-icon">&#8597;</span>';
      dragHandleBtn.disabled = entries.length <= 1;
      wrap.appendChild(dragHandleBtn);
      const spacerEl = document.createElement("span");
      spacerEl.className = "layer-visibility-btn";
      spacerEl.setAttribute("aria-hidden", "true");
      spacerEl.style.visibility = "hidden";
      wrap.appendChild(spacerEl);
      const row = document.createElement("button");
      row.type = "button";
      row.className = "layer-row" + (isActive ? " active" : "");
      row.textContent = scope.getHeightLayerDisplayName(entry) + " (Z" + Math.max(1, Number(entry.z) || 1) + ")";
      row.addEventListener("click", () => {
        selectHeightLayerForEditing(entryId);
      });
      row.addEventListener("contextmenu", (event) => {
        openHeightLayerContextMenu(entry, event);
      });
      wrap.appendChild(row);
      const duplicateBtn = document.createElement("button");
      duplicateBtn.type = "button";
      duplicateBtn.className = "layer-delete-btn";
      duplicateBtn.innerHTML = "&#128203;";
      duplicateBtn.title = "Duplicate height layer";
      duplicateBtn.setAttribute("aria-label", duplicateBtn.title);
      duplicateBtn.addEventListener("click", (event) => {
        event.stopPropagation();
        duplicateHeightLayer(entryId);
      });
      wrap.appendChild(duplicateBtn);
      const delBtn = document.createElement("button");
      delBtn.type = "button";
      delBtn.className = "layer-delete-btn";
      delBtn.innerHTML = "&#128465;";
      delBtn.title = "Delete height layer";
      delBtn.addEventListener("click", (event) => {
        event.stopPropagation();
        deleteHeightLayer(entryId);
      });
      wrap.appendChild(delBtn);
      scope.heightLayerListEl.appendChild(wrap);
    });
    heightLayerListReorderController.refresh();
  }

  function renderInstancePalette() {
    if (!scope.instancePaletteEl) return;
    refreshInstanceSectionState();
    refreshEntityTypeTabs();
    bindEntitySearchToolbarButton();
    if (renderEntitySearchMode()) {
      return;
    }
    const activeEntityType = normalizeEntityType(scope.activeEntityCategory, "friendly");
    const templates = scope.getNpcCatalogRecords()
      .filter((entry) => normalizeEntityType(entry?.entityType, "friendly") === activeEntityType);
    const templateIds = templates.map((entry) => String(entry.id || "").trim()).filter(Boolean);
    if (scope.activeInstanceBrushId && !templateIds.includes(String(scope.activeInstanceBrushId || "").trim())) {
      scope.activeInstanceBrushId = "";
    }
    renderFolderedSelectorList({
      scope,
      container: scope.instancePaletteEl,
      panelKey: "instanceTemplates",
      items: templates,
      getItemId: (entry) => entry.id,
      emptyMessage: "No " + getEntityTypeLabel(activeEntityType).toLowerCase() + " catalog entries found.",
      baseLabel: "Base Panel",
      onMove: (dragging, dropTarget) => {
        movePanelNodeAndRefresh("instanceTemplates", templateIds, dragging, dropTarget, renderInstancePalette);
      },
      onToggleFolder: (folderId) => {
        togglePanelFolderAndRefresh("instanceTemplates", templateIds, folderId, renderInstancePalette);
      },
      onRenameFolder: (folderId) => {
        renamePanelFolderAndRefresh("instanceTemplates", templateIds, folderId, renderInstancePalette);
      },
      onDeleteFolder: (folderId) => {
        deletePanelFolderAndRefresh("instanceTemplates", templateIds, folderId, renderInstancePalette);
      },
      renderItemRow: (entry) => {
        const btn = document.createElement("div");
        btn.className = "history-row npc-row" + (scope.activeInstanceBrushId === entry.id ? " active" : "");
        const header = document.createElement("div");
        header.className = "npc-row-header";
        const thumb = entry.dataUrl
          ? '<img class="npc-thumb" alt="npc" src="' + scope.runtimeEscapeHtml(entry.dataUrl) + '">'
          : '<span class="npc-thumb-fallback">NPC</span>';
        const meta = [
          '<span class="entity-type-badge">' + scope.runtimeEscapeHtml(getEntityTypeLabel(entry.entityType)) + "</span>",
          scope.runtimeEscapeHtml(entry.id),
        ];
        header.innerHTML = thumb + "<div><span>" + scope.runtimeEscapeHtml(entry.name || entry.id) + "</span>" +
          '<span class="history-meta">' + meta.join(" | ") + "</span></div>";
        header.addEventListener("click", () => {
          if (scope.activeInstanceBrushId === entry.id) {
            clearInstanceTemplatePlacement();
            return;
          }
          selectInstanceTemplateForPlacement(entry);
        });
        header.addEventListener("contextmenu", (event) => {
          openCatalogEntityContextMenu(entry, event);
        });
        btn.appendChild(header);
        return btn;
      },
    });
  }

  function renderPrototypePanel(listEl, panelKey, emptyMessage) {
    if (!listEl) {
      return;
    }
    renderFolderedSelectorList({
      scope,
      container: listEl,
      panelKey,
      items: [],
      getItemId: () => "",
      emptyMessage,
      baseLabel: "Base Panel",
      onMove: (dragging, dropTarget) => {
        movePanelNodeAndRefresh(panelKey, [], dragging, dropTarget, () => renderPrototypePanel(listEl, panelKey, emptyMessage));
      },
      onToggleFolder: (folderId) => {
        togglePanelFolderAndRefresh(panelKey, [], folderId, () => renderPrototypePanel(listEl, panelKey, emptyMessage));
      },
      onRenameFolder: (folderId) => {
        renamePanelFolderAndRefresh(panelKey, [], folderId, () => renderPrototypePanel(listEl, panelKey, emptyMessage));
      },
      onDeleteFolder: (folderId) => {
        deletePanelFolderAndRefresh(panelKey, [], folderId, () => renderPrototypePanel(listEl, panelKey, emptyMessage));
      },
      renderItemRow: () => document.createElement("div"),
    });
  }

  function renderTriggerList() {
    renderPrototypePanel(scope.triggerListEl, "triggers", "No triggers yet.");
  }

  function renderMonsterList() {
    renderPrototypePanel(scope.monsterListEl, "monsters", "No monsters yet.");
  }

  function renderPathList() {
    renderPrototypePanel(scope.pathListEl, "paths", "No paths yet.");
  }

  function renderTransitionList() {
    renderPrototypePanel(scope.transitionListEl, "transitions", "No transitions yet.");
  }

  function renderLayerList() {
    scope.syncLayerVisibilityState();
    if (!scope.viewingAllLayers) {
      scope.setLayerVisibility(scope.activeLayer, true);
    }
    refreshLayerSectionState();
    scope.layerListEl.innerHTML = "";
    const allRow = document.createElement("button");
    allRow.type = "button";
    allRow.className = "layer-row" + (scope.viewingAllLayers ? " active" : "");
    allRow.textContent = "All Layers";
    allRow.addEventListener("click", () => {
      scope.viewingAllLayers = true;
      scope.selectedTile = null;
      renderLayerList();
      scope.renderNpcList();
      scope.draw();
    });
    scope.layerListEl.appendChild(allRow);

    scope.roomLayers
      .slice()
      .sort((a, b) => a.layer - b.layer)
      .forEach((layer) => {
        const layerNumber = Number(layer.layer) || 0;
        const layerLabel = scope.getLayerDisplayName(layer);
        const isVisible = scope.isLayerVisible(layerNumber);
        const isActiveLayer = scope.editingTargetKind !== "height" && !scope.viewingAllLayers && layerNumber === Number(scope.activeLayer);
        const wrap = document.createElement("div");
        wrap.className = "layer-row-wrap";
        wrap.setAttribute("data-layer-number", String(layerNumber));

        const dragHandleBtn = document.createElement("button");
        dragHandleBtn.type = "button";
        dragHandleBtn.className = "layer-drag-handle";
        dragHandleBtn.title = layerNumber === 0 ? "Background stays anchored at the bottom" : "Drag to change draw depth";
        dragHandleBtn.setAttribute("aria-label", dragHandleBtn.title);
        dragHandleBtn.innerHTML = '<span class="layer-drag-icon">&#8597;</span>';
        dragHandleBtn.disabled = layerNumber === 0;
        wrap.appendChild(dragHandleBtn);

        const visibilityBtn = document.createElement("button");
        visibilityBtn.type = "button";
        visibilityBtn.className = "layer-visibility-btn" + (isVisible ? " active" : " hidden");
        visibilityBtn.title = isActiveLayer
          ? "Active layer stays visible while selected"
          : (isVisible ? "Hide layer " + layerNumber : "Show layer " + layerNumber);
        visibilityBtn.setAttribute("aria-pressed", isVisible ? "true" : "false");
        visibilityBtn.innerHTML = '<span class="layer-visibility-icon">&#128065;</span>';
        visibilityBtn.addEventListener("click", (event) => {
          event.stopPropagation();
          if (isActiveLayer) {
            setStatus("Active layer stays visible while selected.", false);
            return;
          }
          const nextVisible = !scope.isLayerVisible(layerNumber);
          scope.setLayerVisibility(layerNumber, nextVisible);
          renderLayerList();
          scope.draw();
          setStatus((nextVisible ? "Showing" : "Hid") + " layer " + layerNumber + ".", false);
        });
        wrap.appendChild(visibilityBtn);

        const row = document.createElement("button");
        row.type = "button";
        row.className = "layer-row" + (isActiveLayer ? " active" : "");
        row.textContent = layerLabel;
        row.addEventListener("click", () => {
          activateLayerForEditing(layerNumber);
        });
        row.addEventListener("contextmenu", (event) => {
          openDrawLayerContextMenu(layer, event);
        });
        wrap.appendChild(row);

        const delBtn = document.createElement("button");
        delBtn.type = "button";
        delBtn.className = "layer-delete-btn";
        delBtn.innerHTML = "&#128465;";
        delBtn.title = layerNumber === 0 ? "Background layer cannot be deleted" : "Delete layer";
        delBtn.disabled = layerNumber === 0;
        delBtn.addEventListener("click", (event) => {
          event.stopPropagation();
          deleteLayerByNumber(layerNumber);
        });
        wrap.appendChild(delBtn);

        scope.layerListEl.appendChild(wrap);
      });
    layerListReorderController.refresh();
    renderHeightLayerList();
    refreshMenuLayerSelect();
  }

  function refreshMenuLayerSelect() {
    if (!scope.menuLayerSelectEl) {
      return;
    }
    const currentValue = scope.viewingAllLayers ? "all" : String(Number(scope.activeLayer) || 0);
    scope.menuLayerSelectEl.innerHTML = "";
    const allOption = document.createElement("option");
    allOption.value = "all";
    allOption.textContent = "All Layers";
    scope.menuLayerSelectEl.appendChild(allOption);
    scope.roomLayers
      .slice()
      .sort((a, b) => a.layer - b.layer)
      .forEach((layer) => {
        const option = document.createElement("option");
        option.value = String(Number(layer.layer) || 0);
        option.textContent = scope.getLayerDisplayName(layer);
        scope.menuLayerSelectEl.appendChild(option);
      });
    scope.menuLayerSelectEl.value = currentValue;
    if (scope.menuLayerSelectEl.value !== currentValue) {
      scope.menuLayerSelectEl.value = "all";
    }
  }

  function handleMenuLayerSelectionChange() {
    if (!scope.menuLayerSelectEl) {
      return;
    }
    const nextValue = String(scope.menuLayerSelectEl.value || "").trim();
    if (nextValue === "all") {
      scope.viewingAllLayers = true;
      scope.selectedTile = null;
      renderLayerList();
      scope.renderNpcList();
      scope.draw();
      setStatus("Viewing all layers.", false);
      return;
    }
    activateLayerForEditing(Number(nextValue) || 0);
  }

  return {
    addHeightLayer,
    duplicateHeightLayer,
    setStatus,
    setSidebarTab,
    refreshInformationPanel,
    refreshBackgroundModeButton,
    cycleBackgroundCellMode,
    refreshInformationDraftState,
    cancelDimensionEdit,
    handleDimensionKeydown,
    cancelInformationEdits,
    applyInformationEdits,
    hasUnsavedChanges,
    renderPaintPalette,
    openTilePaletteContextMenu,
    getTileIdForCell,
    renderHeightLayerList,
    renderInstancePalette,
    refreshEntityTypeTabs,
    setActiveEntityCategory,
    renderTriggerList,
    renderMonsterList,
    renderPathList,
    renderTransitionList,
    collapseActiveSidebarTabSections,
    renderLayerList,
    refreshMenuLayerSelect,
    handleMenuLayerSelectionChange,
    toggleDrawLayerSection,
    toggleHeightLayerSection,
    toggleTemplateSection,
    togglePlacedSection,
    refreshLayerSectionState,
    refreshInstanceSectionState,
  };
}
