import JoanGlyphEngine, {
  PIXEL_SHAPES,
  PIXEL_SWITCHES,
  TRANSITIONS,
  listSprites,
} from "./joan-engine.js";
import { FIELD_IDS } from "./fields.js";
import {
  normalizePreviewMode,
  previewModeDetails,
} from "./preview-layout.js";
import {
  createThumbnailRecipe,
  renderThumbnailRestFrame,
  shouldAnimateThumbnail,
} from "./preview-thumbnails.js";
import {
  SELECT_CONTROL_HELP,
  VALUE_CONTROL_HELP,
  placeControlTooltip,
} from "./control-help.js";

const byId = (id) => document.getElementById(id);
const elements = {
  heroCanvas: byId("heroEngineCanvas"),
  canvas: byId("engineCanvas"),
  canvasFrame: byId("canvasFrame"),
  canvasHint: byId("canvasHint"),
  frameRenderBadge: byId("frameRenderBadge"),
  previewFitButton: byId("previewFitButton"),
  previewActualButton: byId("previewActualButton"),
  liveRegion: byId("liveRegion"),
  stateTitle: byId("stateTitle"),
  stateDescription: byId("stateDescription"),
  stateCategory: byId("stateCategory"),
  stateSearchInput: byId("stateSearchInput"),
  statePickerSelect: byId("statePickerSelect"),
  statePickerStatus: byId("statePickerStatus"),
  spriteGrid: byId("spriteGrid"),
  fieldSelect: byId("fieldSelect"),
  fieldScopeBadge: byId("fieldScopeBadge"),
  transitionSelect: byId("transitionSelect"),
  transitionScopeBadge: byId("transitionScopeBadge"),
  pixelShapeSelect: byId("pixelShapeSelect"),
  pixelShapeScopeBadge: byId("pixelShapeScopeBadge"),
  pixelSwitchSelect: byId("pixelSwitchSelect"),
  pixelSwitchScopeBadge: byId("pixelSwitchScopeBadge"),
  orbBoundaryControl: byId("orbBoundaryControl"),
  orbBoundaryInput: byId("orbBoundaryInput"),
  orbBoundaryScopeBadge: byId("orbBoundaryScopeBadge"),
  orbBoundaryAvailability: byId("orbBoundaryAvailability"),
  orbBackgroundControl: byId("orbBackgroundControl"),
  orbBackgroundEnabledInput: byId("orbBackgroundEnabledInput"),
  orbBackgroundEnabledLabel: byId("orbBackgroundEnabledLabel"),
  orbBackgroundModeSelect: byId("orbBackgroundModeSelect"),
  orbBackgroundColorInput: byId("orbBackgroundColorInput"),
  orbBackgroundScopeBadge: byId("orbBackgroundScopeBadge"),
  orbBackgroundAvailability: byId("orbBackgroundAvailability"),
  nonErrorPaletteSection: byId("nonErrorPaletteSection"),
  paletteScopeBadge: byId("paletteScopeBadge"),
  motionScopeBadge: byId("motionScopeBadge"),
  paletteInputs: [...document.querySelectorAll("[data-palette-color]")],
  glowColorControl: byId("glowColorControl"),
  glowColorInput: byId("glowColorInput"),
  glowEnabledInput: byId("glowEnabledInput"),
  glowEnabledLabel: byId("glowEnabledLabel"),
  resetPaletteButton: byId("resetPaletteButton"),
  resolutionRange: byId("resolutionRange"),
  resolutionValue: byId("resolutionValue"),
  speedRange: byId("speedRange"),
  speedValue: byId("speedValue"),
  densityRange: byId("densityRange"),
  densityValue: byId("densityValue"),
  seedInput: byId("seedInput"),
  randomizeButton: byId("randomizeButton"),
  overrideSummary: byId("overrideSummary"),
  workflowStatus: byId("workflowStatus"),
  recentPresetSelect: byId("recentPresetSelect"),
  savePresetButton: byId("savePresetButton"),
  savePresetGalleryButton: byId("savePresetGalleryButton"),
  savedPresetGallery: byId("savedPresetGallery"),
  savedPresetEmpty: byId("savedPresetEmpty"),
  savedPresetCount: byId("savedPresetCount"),
  deletedPresetNotice: byId("deletedPresetNotice"),
  deletedPresetMessage: byId("deletedPresetMessage"),
  undoDeletePresetButton: byId("undoDeletePresetButton"),
  undoStudioButton: byId("undoStudioButton"),
  resetOverridesButton: byId("resetOverridesButton"),
  openExportButton: byId("openExportButton"),
  playButton: byId("playButton"),
  autopilotButton: byId("autopilotButton"),
  themeButton: byId("themeButton"),
  openRecipeButton: byId("openRecipeButton"),
  recipeFile: byId("recipeFile"),
  importButton: byId("importButton"),
  glyphFile: byId("glyphFile"),
  exportPngButton: byId("exportPngButton"),
  exportSvgButton: byId("exportSvgButton"),
  exportAnimatedSvgButton: byId("exportAnimatedSvgButton"),
  exportConfigButton: byId("exportConfigButton"),
  copyCodeButton: byId("copyCodeButton"),
  codeSnippet: byId("codeSnippet"),
  fpsValue: byId("fpsValue"),
  activeValue: byId("activeValue"),
  currentStateValue: byId("currentStateValue"),
  progressControl: byId("progressControl"),
  progressRange: byId("progressRange"),
  progressValue: byId("progressValue"),
  signalTokenButton: byId("signalTokenButton"),
  signalSearchButton: byId("signalSearchButton"),
  signalAudioButton: byId("signalAudioButton"),
  exportDialog: byId("exportDialog"),
  exportForm: byId("exportForm"),
  closeExportButton: byId("closeExportButton"),
  cancelExportButton: byId("cancelExportButton"),
  exportPreviewCanvas: byId("exportPreviewCanvas"),
  exportPreviewNote: byId("exportPreviewNote"),
  exportFormatSelect: byId("exportFormatSelect"),
  exportSizeSelect: byId("exportSizeSelect"),
  exportFilenameInput: byId("exportFilenameInput"),
  exportBackgroundInput: byId("exportBackgroundInput"),
  exportDimensionsValue: byId("exportDimensionsValue"),
  exportCompositionValue: byId("exportCompositionValue"),
  exportMotionValue: byId("exportMotionValue"),
  exportBackgroundValue: byId("exportBackgroundValue"),
  confirmExportButton: byId("confirmExportButton"),
};

const sprites = listSprites();
const spriteById = new Map(sprites.map((sprite) => [sprite.id, sprite]));
const builtInSpriteIds = new Set(sprites.map((sprite) => sprite.id));
const STUDIO_PACKAGE = "@joan/procedural-glyph-engine";
const STUDIO_VERSION = "5.0.0";
const RECENT_PRESET_KEY = "joan.studio.recent-presets.v1";
const THEME_STORAGE_KEY = "joan.studio.theme.v1";
const MAX_RECENT_PRESETS = 24;
const SAFE_COLOR =
  /^(?:#[\da-f]{3,8}|(?:rgba?|hsla?)\([\d\s.,%+\/-]+\)|transparent|aqua|black|blue|fuchsia|gray|green|grey|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow)$/i;
const humanize = (value) =>
  String(value)
    .replaceAll(".", " · ")
    .replaceAll("-", " ")
    .replace(/\b\w/g, (letter) => letter.toUpperCase());
const formatNumber = (value, digits = 2) =>
  Number(value).toFixed(digits).replace(/\.?0+$/, "");
const supportsGestaltBoundary = (recipe) =>
  recipe?.composition?.gestaltBoundary === true;

function addOptions(select, values, getLabel = humanize) {
  if (!select) return;
  select.textContent = "";
  for (const value of values) {
    const option = document.createElement("option");
    option.value = value;
    option.textContent = getLabel(value);
    select.append(option);
  }
}

addOptions(
  elements.fieldSelect,
  ["__recipe__", ...FIELD_IDS],
  (value) => (value === "__recipe__" ? "Recipe field stack" : humanize(value)),
);
addOptions(
  elements.transitionSelect,
  ["__recipe__", ...TRANSITIONS],
  (value) => (value === "__recipe__" ? "Recipe transition" : humanize(value)),
);
const PIXEL_SHAPE_LABELS = Object.freeze({
  "square-cross": "Square + cross",
  "square-cross-ring": "Square + cross + ring",
});
addOptions(
  elements.pixelShapeSelect,
  PIXEL_SHAPES,
  (value) => PIXEL_SHAPE_LABELS[value] || humanize(value),
);
addOptions(elements.pixelSwitchSelect, PIXEL_SWITCHES);

const controlTipBindings = new Map();
const CONTROL_TOOLTIP_HOVER_DELAY_MS = 1000;
let activeControlTip = null;
let dismissedControlTip = null;
let controlTipOpenTimer = null;
let controlTipCloseTimer = null;
let controlTipHideTimer = null;
const controlTipPortal = document.createElement("div");
controlTipPortal.className = "control-tooltip";
controlTipPortal.hidden = true;
controlTipPortal.setAttribute("aria-hidden", "true");
document.body.append(controlTipPortal);

function controlTipText(control) {
  const selectHelp = SELECT_CONTROL_HELP[control.id];
  if (selectHelp) {
    const optionHelp = selectHelp.options[control.value] || "";
    return `${selectHelp.summary} ${optionHelp}`.trim();
  }

  const base = VALUE_CONTROL_HELP[control.id] || "";
  switch (control.id) {
    case "resolutionRange":
      return `${base} Current grid: ${control.value}×${control.value}.`;
    case "speedRange":
      return `${base} Current speed: ${formatNumber(control.value)}×.`;
    case "densityRange":
      return `${base} Current density: ${formatNumber(control.value)}.`;
    case "backgroundColorInput":
    case "offColorInput":
    case "inkColorInput":
    case "accentColorInput":
      return `${base} Current color: ${control.value.toUpperCase()}.`;
    case "glowColorInput":
      return control.disabled
        ? `${base} Glow is currently off.`
        : `${base} Current color: ${control.value.toUpperCase()}.`;
    case "glowEnabledInput":
      return `${base} Glow is currently ${control.checked ? "on" : "off"}.`;
    case "orbBoundaryInput":
      return control.disabled
        ? `${base} This state does not use an orb boundary; your choice remains saved.`
        : `${base} Current mode: ${control.checked ? "Gestalt" : "Defined"}.`;
    case "orbBackgroundEnabledInput":
      return control.disabled
        ? `${base} This state does not use a configurable orb; your choice remains saved.`
        : `${base} The orb background is currently ${control.checked ? "on" : "off"}.`;
    case "orbBackgroundColorInput":
      return control.disabled
        ? `${base} Enable the orb background to choose a color.`
        : `${base} Current color: ${control.value.toUpperCase()}.`;
    default:
      return base;
  }
}

function positionActiveControlTip() {
  if (!activeControlTip || controlTipPortal.hidden) return;
  const { host } = activeControlTip;
  if (!host.isConnected) return;
  const anchor = host.getBoundingClientRect();
  const tooltip = controlTipPortal.getBoundingClientRect();
  const position = placeControlTooltip(
    anchor,
    tooltip,
    {
      width: window.innerWidth,
      height: window.innerHeight,
    },
    { gap: 8, gutter: 12 },
  );
  controlTipPortal.style.left = `${position.left}px`;
  controlTipPortal.style.top = `${position.top}px`;
  controlTipPortal.dataset.placement = position.placement;
}

function updateControlTip(control) {
  const binding = controlTipBindings.get(control);
  if (!binding) return;
  const text = controlTipText(control);
  binding.description.textContent = text;
  if (activeControlTip?.control === control) {
    controlTipPortal.textContent = text;
    positionActiveControlTip();
  }
}

function showControlTip(control) {
  const binding = controlTipBindings.get(control);
  if (!binding || dismissedControlTip === control) return;
  window.clearTimeout(controlTipOpenTimer);
  window.clearTimeout(controlTipCloseTimer);
  window.clearTimeout(controlTipHideTimer);
  activeControlTip = binding;
  updateControlTip(control);
  controlTipPortal.textContent = binding.description.textContent;
  controlTipPortal.hidden = false;
  controlTipPortal.classList.add("is-visible");
  positionActiveControlTip();
}

function hideControlTip() {
  window.clearTimeout(controlTipOpenTimer);
  window.clearTimeout(controlTipCloseTimer);
  controlTipPortal.classList.remove("is-visible");
  activeControlTip = null;
  controlTipHideTimer = window.setTimeout(() => {
    if (!activeControlTip) controlTipPortal.hidden = true;
  }, 150);
}

function scheduleControlTipOpen(control) {
  window.clearTimeout(controlTipOpenTimer);
  controlTipOpenTimer = window.setTimeout(() => {
    const binding = controlTipBindings.get(control);
    if (binding?.host.matches(":hover")) showControlTip(control);
  }, CONTROL_TOOLTIP_HOVER_DELAY_MS);
}

function scheduleControlTipClose() {
  window.clearTimeout(controlTipCloseTimer);
  controlTipCloseTimer = window.setTimeout(() => {
    const hostHasFocus = activeControlTip?.host.matches(":focus-within");
    const hostHasPointer = activeControlTip?.host.matches(":hover");
    if (!hostHasFocus && !hostHasPointer) {
      hideControlTip();
    }
  }, 120);
}

function appendDescriptionId(control, id) {
  const ids = new Set(
    (control.getAttribute("aria-describedby") || "")
      .split(/\s+/)
      .filter(Boolean),
  );
  ids.add(id);
  control.setAttribute("aria-describedby", [...ids].join(" "));
}

function registerControlTip(control) {
  if (!control?.id || controlTipBindings.has(control)) return;
  const label = control.closest("label");
  const labelControls = label?.querySelectorAll(
    "button, input, select, textarea",
  );
  const host =
    control.matches("button") || labelControls?.length > 1 ? control : label;
  if (!host) return;

  const description = document.createElement("span");
  description.className = "visually-hidden";
  description.id = `${control.id}Help`;
  description.setAttribute("role", "tooltip");
  document.body.append(description);
  appendDescriptionId(control, description.id);
  host.classList.add("control-with-tooltip");

  const binding = { control, host, description };
  controlTipBindings.set(control, binding);
  updateControlTip(control);

  host.addEventListener("pointerenter", (event) => {
    if (event.pointerType !== "touch") scheduleControlTipOpen(control);
  });
  host.addEventListener("pointerleave", () => {
    window.clearTimeout(controlTipOpenTimer);
    if (dismissedControlTip === control) dismissedControlTip = null;
    scheduleControlTipClose();
  });
  host.addEventListener("focusin", () => {
    window.clearTimeout(controlTipOpenTimer);
    showControlTip(control);
  });
  host.addEventListener("focusout", () => {
    if (dismissedControlTip === control) dismissedControlTip = null;
    scheduleControlTipClose();
  });
  control.addEventListener("input", () => updateControlTip(control));
  control.addEventListener("change", () => updateControlTip(control));
}

function setupControlTips() {
  for (const id of Object.keys(SELECT_CONTROL_HELP)) {
    registerControlTip(byId(id));
  }
  for (const id of Object.keys(VALUE_CONTROL_HELP)) {
    registerControlTip(byId(id));
  }

  document.addEventListener("pointerdown", (event) => {
    if (
      activeControlTip &&
      !activeControlTip.host.contains(event.target) &&
      !controlTipPortal.contains(event.target)
    ) {
      hideControlTip();
    }
  });
  document.addEventListener("keydown", (event) => {
    if (event.key !== "Escape" || !activeControlTip) return;
    dismissedControlTip = activeControlTip.control;
    hideControlTip();
  });
  window.addEventListener("scroll", positionActiveControlTip, {
    passive: true,
  });
  window.addEventListener("resize", positionActiveControlTip);
}

setupControlTips();

const engine = new JoanGlyphEngine(elements.canvas, {
  sprite: "ai.idle",
  seed: elements.seedInput?.value || "world-class-ai",
  gridSize: Number(elements.resolutionRange?.value) || 68,
  speed: Number(elements.speedRange?.value) || 1,
  density: Number(elements.densityRange?.value) || 1,
  pixelShape: null,
  pixelSwitch: null,
  transition: null,
  orbBoundary: "defined",
  orbBackgroundColor: null,
  orbBackgroundMode: "none",
  ariaLive: elements.liveRegion,
  quality: "auto",
  dprMax: 1.5,
});

window.joanEngine = engine;

let heroEngine = null;

const previewEngines = [];
const previewById = new Map();
const previewDescriptors = new Map();
const hoveredPreviewIds = new Set();
const focusedPreviewIds = new Set();
const spriteButtons = new Map();
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
let selectedSpriteId = engine.currentRecipe.id;
let autopilotTimer = null;
let autopilotIndex = 0;
let audioTimer = null;
let isLight = document.documentElement.dataset.theme === "light";
let pixelShapeOverridden = false;
let pixelSwitchOverridden = false;
let transitionOverridden = false;
let transitionOverrideValue = null;
let customNonErrorPalette = {};
let savedGlowOverride = null;
let hadSavedGlowOverride = false;
let orbBackgroundColorChoice = "#262626";
let orbBackgroundModeChoice = "solid";
let previewMode = "fit";
let baselineStudioState = null;
let undoStack = [];
let isRestoringStudioState = false;
let recentPresets = [];
let deletedPreset = null;
const activeMutations = new Set();
let workflowStatusTimer = null;
let exportReturnFocus = null;
let previewResolutionVersion = 0;
let previewRefreshVersion = 0;
let previewsDisposed = false;
let previewCardObserver = null;
let savedPresetPreviewObserver = null;
const savedPresetPreviewQueue = new Map();
let pendingResolution = engine.gridSize;
let resolutionCommitTimer = null;
const RESOLUTION_COMMIT_DELAY_MS = 90;
const urgentPreviewIds = new Set();
let urgentPreviewScheduled = false;
const previewWorkQueue = [];
let previewWorkScheduled = false;

function scheduleIdlePreviewWork(callback) {
  if (typeof window.requestIdleCallback === "function") {
    window.requestIdleCallback(callback, { timeout: 240 });
  } else {
    window.setTimeout(callback, 16);
  }
}

function runPreviewWorkSlice(deadline) {
  if (previewsDisposed) {
    previewWorkQueue.length = 0;
    previewWorkScheduled = false;
    return;
  }
  const task = previewWorkQueue.shift();
  try {
    task?.(deadline);
  } finally {
    if (previewWorkQueue.length) scheduleIdlePreviewWork(runPreviewWorkSlice);
    else previewWorkScheduled = false;
  }
}

function enqueuePreviewWork(task, { priority = false } = {}) {
  if (typeof task !== "function" || previewsDisposed) return;
  if (priority) previewWorkQueue.unshift(task);
  else previewWorkQueue.push(task);
  if (previewWorkScheduled) return;
  previewWorkScheduled = true;
  scheduleIdlePreviewWork(runPreviewWorkSlice);
}

function setPreviewResolution(preview, resolution) {
  if (preview.gridSize !== resolution) preview.setResolution(resolution);
  if (preview.canvas?.dataset) {
    preview.canvas.dataset.gridSize = String(resolution);
  }
}

function currentPreviewProgress() {
  const value = Number(elements.progressRange?.value);
  return Number.isFinite(value)
    ? Math.min(1, Math.max(0, value / 100))
    : 0.48;
}

function previewId(preview) {
  return preview?.previewSpriteId || preview?.currentRecipe?.id || null;
}

function previewShouldAnimate(id) {
  return (
    !reduceMotion.matches &&
    shouldAnimateThumbnail(id, {
      selectedId: selectedSpriteId,
      mainRunning: engine.running,
      hoveredIds: hoveredPreviewIds,
      focusedIds: focusedPreviewIds,
    })
  );
}

function freezePreview(preview) {
  return renderThumbnailRestFrame(preview, {
    progress: currentPreviewProgress(),
  });
}

function freezePreviewIfInactive(id) {
  const preview = previewById.get(id);
  if (preview && !previewShouldAnimate(id)) freezePreview(preview);
}

function prioritizedPreviewQueue() {
  const queue = [];
  const seen = new Set();
  const add = (preview) => {
    if (!preview || seen.has(preview)) return;
    seen.add(preview);
    queue.push(preview);
  };
  add(previewById.get(selectedSpriteId));
  for (const id of hoveredPreviewIds) add(previewById.get(id));
  for (const id of focusedPreviewIds) add(previewById.get(id));
  for (const preview of previewEngines) add(preview);
  return queue;
}

function schedulePreviewResolutionSync(value = engine.gridSize) {
  const resolution = Math.round(Number(value) || engine.gridSize);
  const version = ++previewResolutionVersion;
  const queue = prioritizedPreviewQueue();
  const updateNext = () => {
    if (version !== previewResolutionVersion) return;
    const preview = queue.shift();
    if (!preview) return;
    setPreviewResolution(preview, resolution);
    if (!previewShouldAnimate(previewId(preview))) freezePreview(preview);
    if (queue.length) enqueuePreviewWork(updateNext);
  };
  enqueuePreviewWork(updateNext);
}

function cancelPendingResolutionCommit() {
  if (resolutionCommitTimer !== null) {
    window.clearTimeout(resolutionCommitTimer);
    resolutionCommitTimer = null;
  }
}

function commitPendingResolution() {
  cancelPendingResolutionCommit();
  const value = Math.round(Number(pendingResolution) || engine.gridSize);
  if (engine.gridSize !== value) engine.setResolution(value);
  applyPreviewMode(previewMode);
  scheduleStudioMetadataUpdate();
  return value;
}

function scheduleResolutionCommit(value) {
  pendingResolution = Math.round(Number(value) || engine.gridSize);
  cancelPendingResolutionCommit();
  resolutionCommitTimer = window.setTimeout(
    commitPendingResolution,
    RESOLUTION_COMMIT_DELAY_MS,
  );
}

function schedulePreviewRefresh() {
  const version = ++previewRefreshVersion;
  const queue = prioritizedPreviewQueue().filter((preview) => {
    return !previewShouldAnimate(previewId(preview));
  });
  const refreshNext = () => {
    if (version !== previewRefreshVersion) return;
    const preview = queue.shift();
    if (!preview) return;
    if (!previewShouldAnimate(previewId(preview))) freezePreview(preview);
    if (queue.length) enqueuePreviewWork(refreshNext);
  };
  enqueuePreviewWork(refreshNext);
}

function applyPreviewMode(mode) {
  const details = previewModeDetails(mode, engine.gridSize);
  previewMode = normalizePreviewMode(details.mode);
  const actual = previewMode === "actual";

  if (engine.options.previewMode !== previewMode) {
    engine.setOptions({ previewMode });
  }
  elements.canvasFrame?.classList.toggle("is-actual-preview", actual);
  elements.canvasFrame?.style.setProperty(
    "--preview-grid-size",
    `${details.resolution}px`,
  );
  elements.previewFitButton?.setAttribute("aria-pressed", String(!actual));
  elements.previewActualButton?.setAttribute("aria-pressed", String(actual));

  if (elements.previewActualButton) {
    elements.previewActualButton.title = actual
      ? `Actual size: ${details.resolution}×${details.resolution}px. Each grid cell uses one CSS pixel.`
      : `Show actual size at ${details.resolution}×${details.resolution}px, with one CSS pixel per grid cell.`;
  }
  if (elements.frameRenderBadge) {
    elements.frameRenderBadge.textContent = actual ? "1:1 render" : "Live render";
  }
  if (elements.canvasHint) {
    elements.canvasHint.textContent = actual
      ? `${details.resolution}×${details.resolution}px · one cell = one CSS pixel`
      : "Move · press · drop a glyph";
  }
}

const lightPalette = {
  background: "#e5e6e2",
  off: "#c7cbc8",
  ink: "#17191e",
  variants: {
    neutral: { accent: "#464c52", glow: "#8b9096" },
    info: { accent: "#50565b", glow: "#8a8f94" },
    success: { accent: "#303438", glow: "#777b7e" },
    warning: { accent: "#5b5a56", glow: "#92908a" },
    danger: { accent: "#3f4245", glow: "#838689" },
    celebration: { accent: "#585d63", glow: "#95999e" },
  },
};

const darkPalette = {
  background: "#070708",
  off: "#1f2022",
  ink: "#f2f1ee",
  variants: {
    neutral: { accent: "#c9c8c4", glow: "#8b8d90" },
    info: { accent: "#b7babd", glow: "#777b80" },
    success: { accent: "#d6d6d0", glow: "#959792" },
    warning: { accent: "#b9b6af", glow: "#82807b" },
    danger: { accent: "#c5c4c1", glow: "#8d8c89" },
    celebration: { accent: "#eeeeea", glow: "#aaa9a5" },
  },
};

function studioPaletteOverride() {
  const palette = {
    ...(isLight ? lightPalette : darkPalette),
    ...customNonErrorPalette,
  };
  return palette;
}

function setGlowControlEnabled(enabled) {
  const isEnabled = Boolean(enabled);
  if (elements.glowEnabledInput) elements.glowEnabledInput.checked = isEnabled;
  if (elements.glowEnabledLabel) {
    elements.glowEnabledLabel.textContent = isEnabled ? "Color" : "None";
  }
  if (elements.glowColorInput) elements.glowColorInput.disabled = !isEnabled;
  elements.glowColorControl?.classList.toggle("is-glow-disabled", !isEnabled);

  const output = elements.glowColorInput?.parentElement?.querySelector("output");
  if (output) {
    output.textContent = isEnabled
      ? elements.glowColorInput.value.toUpperCase()
      : "NONE";
  }
  updateControlTip(elements.glowEnabledInput);
  updateControlTip(elements.glowColorInput);
}

function normalizedOrbBackgroundColor(value) {
  if (typeof value !== "string") return null;
  const color = value.trim();
  return color && color.toLowerCase() !== "transparent" ? color : null;
}

function normalizedOrbBackgroundMode(value, color = null) {
  if (value === "none") return "none";
  if (value === "pixelated" || value === "solid") return value;
  return normalizedOrbBackgroundColor(color) ? "solid" : "none";
}

function setOrbBackgroundControlState(
  color = engine.options.orbBackgroundColor,
  available = supportsGestaltBoundary(engine.currentRecipe),
  mode = engine.options.orbBackgroundMode,
) {
  const activeColor = normalizedOrbBackgroundColor(color);
  const activeMode = normalizedOrbBackgroundMode(mode, activeColor);
  const enabled = Boolean(activeColor) && activeMode !== "none";
  if (activeColor) orbBackgroundColorChoice = activeColor;
  if (activeMode !== "none") orbBackgroundModeChoice = activeMode;

  if (elements.orbBackgroundControl) {
    elements.orbBackgroundControl.dataset.available = String(available);
    elements.orbBackgroundControl.dataset.enabled = String(enabled);
  }
  if (elements.orbBackgroundEnabledInput) {
    elements.orbBackgroundEnabledInput.checked = enabled;
    elements.orbBackgroundEnabledInput.disabled = !available;
  }
  if (elements.orbBackgroundEnabledLabel) {
    elements.orbBackgroundEnabledLabel.textContent = enabled ? "On" : "None";
  }
  if (elements.orbBackgroundModeSelect) {
    elements.orbBackgroundModeSelect.value =
      activeMode === "none" ? orbBackgroundModeChoice : activeMode;
    elements.orbBackgroundModeSelect.disabled = !available || !enabled;
  }
  if (elements.orbBackgroundColorInput) {
    if (/^#[\da-f]{6}$/i.test(activeColor || "")) {
      elements.orbBackgroundColorInput.value = activeColor;
    } else if (/^#[\da-f]{6}$/i.test(orbBackgroundColorChoice)) {
      elements.orbBackgroundColorInput.value = orbBackgroundColorChoice;
    }
    elements.orbBackgroundColorInput.disabled = !available || !enabled;
    const output = elements.orbBackgroundColorInput.parentElement?.querySelector(
      "output",
    );
    if (output) output.textContent = enabled ? activeColor.toUpperCase() : "NONE";
  }
  if (elements.orbBackgroundAvailability) {
    elements.orbBackgroundAvailability.textContent = available
      ? enabled
        ? activeMode === "pixelated"
          ? "Grid-aligned disk behind the orb pixels"
          : "Solid circle behind the orb pixels"
        : "Optional color behind the orb pixels"
      : "Available for Ready and ambient orbs · choice stays saved";
  }
  updateControlTip(elements.orbBackgroundEnabledInput);
  updateControlTip(elements.orbBackgroundModeSelect);
  updateControlTip(elements.orbBackgroundColorInput);
}

function syncOrbAppearanceToPreviews({ refresh = true } = {}) {
  for (const preview of previewEngines) {
    preview.options.orbBoundary = engine.options.orbBoundary;
    preview.options.orbBackgroundColor = engine.options.orbBackgroundColor;
    preview.options.orbBackgroundMode = engine.options.orbBackgroundMode;
  }
  if (refresh) schedulePreviewRefresh();
}

function updatePaletteControls(palette) {
  for (const input of elements.paletteInputs) {
    const key = input.dataset.paletteColor;
    const value = palette?.[key];
    if (/^#[\da-f]{6}$/i.test(value || "")) input.value = value;
    const output = input.parentElement?.querySelector("output");
    if (key === "glow") {
      const enabled = String(value).trim().toLowerCase() !== "transparent";
      setGlowControlEnabled(enabled);
    } else if (output) {
      output.textContent = input.value.toUpperCase();
    }
    updateControlTip(input);
  }
}

function authoredPaletteForControls(recipe) {
  const palette = recipe?.palette || {};
  return {
    background: palette.background,
    off: palette.off || palette.shadow,
    ink: palette.ink,
    accent: palette.accent,
    glow: palette.glow || palette.accent,
  };
}

function nonErrorPaletteForProtectedPreview(palette) {
  if (!palette || typeof palette !== "object") return palette;
  return {
    ...palette,
    ...(palette.variants?.neutral || {}),
  };
}

function applyStudioPalette({
  refreshPreviews = true,
  immediatePreviewRefresh = false,
} = {}) {
  const palette = studioPaletteOverride();
  engine.setNonErrorPalette(palette);
  heroEngine?.setNonErrorPalette(palette);
  for (const preview of previewEngines) {
    preview.setNonErrorPalette(palette, { render: false });
  }
  if (immediatePreviewRefresh) {
    for (const preview of previewEngines) freezePreview(preview);
  } else if (refreshPreviews) {
    schedulePreviewRefresh();
  }
  if (engine.currentRecipe.id !== "status.error") {
    updatePaletteControls(engine.exportConfig().palette);
  } else if (palette) {
    updatePaletteControls(nonErrorPaletteForProtectedPreview(palette));
  } else {
    updatePaletteControls(authoredPaletteForControls(spriteById.get("ai.idle")));
  }
  updateWorkflowState();
  updateCode();
}

let paletteUpdateFrame = null;
function scheduleStudioPaletteUpdate() {
  if (paletteUpdateFrame !== null) return;
  paletteUpdateFrame = requestAnimationFrame(() => {
    paletteUpdateFrame = null;
    applyStudioPalette({ refreshPreviews: false });
  });
}

function flushStudioPaletteUpdate() {
  if (paletteUpdateFrame !== null) {
    cancelAnimationFrame(paletteUpdateFrame);
    paletteUpdateFrame = null;
  }
  applyStudioPalette();
}

let studioMetadataFrame = null;
function scheduleStudioMetadataUpdate() {
  if (studioMetadataFrame !== null) return;
  studioMetadataFrame = requestAnimationFrame(() => {
    studioMetadataFrame = null;
    updateWorkflowState();
    updateCode();
  });
}

function recipePixelSwitch(recipe) {
  const pixelSwitch = recipe?.pixelSwitch;
  return typeof pixelSwitch === "string"
    ? pixelSwitch
    : pixelSwitch?.name ||
        pixelSwitch?.type ||
        pixelSwitch?.mode ||
        "ordered-dither";
}

function normalizeStructuredOption(value, values, keys, canonicalKey) {
  if (typeof value === "string") return values.includes(value) ? value : null;
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
  const name = keys.map((key) => value[key]).find((entry) => values.includes(entry));
  if (!name) return null;
  return { ...cloneData(value), [canonicalKey]: name };
}

function normalizeTransitionOption(value) {
  return normalizeStructuredOption(
    value,
    TRANSITIONS,
    ["name", "type", "enter"],
    "name",
  );
}

function normalizePixelSwitchOption(value) {
  return normalizeStructuredOption(
    value,
    PIXEL_SWITCHES,
    ["mode", "name", "type"],
    "mode",
  );
}

function transitionOptionName(value) {
  const normalized = normalizeTransitionOption(value);
  return typeof normalized === "string" ? normalized : normalized?.name || null;
}

function pixelSwitchOptionMode(value) {
  const normalized = normalizePixelSwitchOption(value);
  return typeof normalized === "string" ? normalized : normalized?.mode || null;
}

function sameStudioValue(left, right) {
  return JSON.stringify(left) === JSON.stringify(right);
}

function formatCodeValue(value, indent = "  ") {
  const serialized = JSON.stringify(value, null, 2);
  return serialized ? serialized.replaceAll("\n", `\n${indent}`) : "null";
}

function recipeDescription(recipe) {
  return (
    recipe?.semantic?.description ||
    recipe?.semantic?.meaning ||
    recipe?.description ||
    "A deterministic semantic sprite composed from a crisp mask or field orb, procedural texture, and persistent pixel gate."
  );
}

function recipeLabel(recipe) {
  return (
    recipe?.labels?.default ||
    recipe?.semantic?.label ||
    recipe?.label ||
    humanize(recipe?.id || "state")
  );
}

function showWorkflowStatus(message, { announce = true, tone = "neutral" } = {}) {
  window.clearTimeout(workflowStatusTimer);
  if (elements.workflowStatus) {
    elements.workflowStatus.textContent = message;
    elements.workflowStatus.dataset.tone = tone;
  }
  if (announce && elements.liveRegion) elements.liveRegion.textContent = message;
  workflowStatusTimer = window.setTimeout(() => {
    if (!elements.workflowStatus) return;
    elements.workflowStatus.textContent = "Ready to customize";
    delete elements.workflowStatus.dataset.tone;
  }, 4200);
}

function setScopeBadge(element, text, kind = "recipe") {
  if (!element) return;
  element.textContent = text;
  element.dataset.scope = kind;
}

function buildStatePicker(query = "") {
  const select = elements.statePickerSelect;
  if (!select) return;
  const normalized = query.trim().toLowerCase();
  const matches = sprites.filter((recipe) => {
    if (!normalized) return true;
    return [
      recipe.id,
      recipeLabel(recipe),
      recipe.semantic?.category,
      recipeDescription(recipe),
    ]
      .filter(Boolean)
      .some((value) => String(value).toLowerCase().includes(normalized));
  });

  const selectedMatches = matches.some((recipe) => recipe.id === selectedSpriteId);
  const groups = new Map();
  for (const recipe of matches) {
    const category = humanize(recipe.semantic?.category || "Other");
    if (!groups.has(category)) groups.set(category, []);
    groups.get(category).push(recipe);
  }

  select.textContent = "";
  if (!selectedMatches && spriteById.has(selectedSpriteId)) {
    const currentGroup = document.createElement("optgroup");
    currentGroup.label = "Current state";
    const current = spriteById.get(selectedSpriteId);
    const option = document.createElement("option");
    option.value = current.id;
    option.textContent = recipeLabel(current);
    currentGroup.append(option);
    select.append(currentGroup);
  }
  for (const [category, recipes] of groups) {
    const group = document.createElement("optgroup");
    group.label = category;
    for (const recipe of recipes) {
      const option = document.createElement("option");
      option.value = recipe.id;
      option.textContent = recipeLabel(recipe);
      group.append(option);
    }
    select.append(group);
  }

  if (!select.options.length) {
    const option = document.createElement("option");
    option.textContent = "No matching states";
    option.disabled = true;
    select.append(option);
    select.disabled = true;
  } else {
    select.disabled = false;
    select.value = selectedSpriteId;
  }
  if (elements.statePickerStatus) {
    elements.statePickerStatus.textContent = normalized
      ? `${matches.length} matching ${matches.length === 1 ? "state" : "states"}`
      : `${sprites.length} states available in ${groups.size} categories`;
  }
}

function cloneData(value) {
  if (typeof structuredClone === "function") return structuredClone(value);
  return JSON.parse(JSON.stringify(value));
}

function captureStudioState() {
  return {
    sprite: engine.currentRecipe.id,
    fieldOverride: engine.fieldOverride || null,
    transitionOverride: transitionOverridden
      ? normalizeTransitionOption(transitionOverrideValue)
      : null,
    pixelShape: engine.options.pixelShape || elements.pixelShapeSelect?.value || null,
    pixelShapeOverridden,
    pixelSwitch: pixelSwitchOverridden
      ? normalizePixelSwitchOption(engine.options.pixelSwitch)
      : null,
    pixelSwitchOverridden,
    orbBoundary:
      engine.options.orbBoundary === "gestalt" ? "gestalt" : "defined",
    orbBackgroundColor:
      normalizedOrbBackgroundColor(engine.options.orbBackgroundColor),
    orbBackgroundMode: normalizedOrbBackgroundMode(
      engine.options.orbBackgroundMode,
      engine.options.orbBackgroundColor,
    ),
    orbBackgroundColorChoice,
    orbBackgroundModeChoice,
    gridSize: engine.gridSize,
    speed: Number(engine.options.speed),
    density: Number(engine.options.density),
    seed: engine.seed,
    progress: Number(engine.progress),
    palette: { ...customNonErrorPalette },
    savedGlowOverride,
    hadSavedGlowOverride,
    previewMode,
  };
}

function changedSettingCount(state, baseline = baselineStudioState) {
  if (!baseline) return 0;
  let count = 0;
  if (state.fieldOverride !== baseline.fieldOverride) count += 1;
  if (!sameStudioValue(state.transitionOverride, baseline.transitionOverride)) count += 1;
  if (state.pixelShape !== baseline.pixelShape) count += 1;
  if (
    state.pixelSwitchOverridden !== baseline.pixelSwitchOverridden ||
    !sameStudioValue(state.pixelSwitch, baseline.pixelSwitch)
  ) {
    count += 1;
  }
  if (state.orbBoundary !== baseline.orbBoundary) count += 1;
  if (
    state.orbBackgroundColor !== baseline.orbBackgroundColor ||
    state.orbBackgroundMode !== baseline.orbBackgroundMode
  ) {
    count += 1;
  }
  if (state.gridSize !== baseline.gridSize) count += 1;
  if (state.speed !== baseline.speed) count += 1;
  if (state.density !== baseline.density) count += 1;
  if (state.seed !== baseline.seed) count += 1;
  if (JSON.stringify(state.palette) !== JSON.stringify(baseline.palette)) count += 1;
  return count;
}

function updateWorkflowState() {
  const state = captureStudioState();
  const changes = changedSettingCount(state);
  const motionCustom = Boolean(
    baselineStudioState &&
      (state.gridSize !== baselineStudioState.gridSize ||
        state.speed !== baselineStudioState.speed ||
        state.density !== baselineStudioState.density),
  );
  const pixelShapeCustom = Boolean(
    baselineStudioState && state.pixelShape !== baselineStudioState.pixelShape,
  );

  setScopeBadge(
    elements.fieldScopeBadge,
    state.fieldOverride ? "Custom" : "Recipe",
    state.fieldOverride ? "custom" : "recipe",
  );
  setScopeBadge(
    elements.transitionScopeBadge,
    state.transitionOverride ? "Custom" : "Recipe",
    state.transitionOverride ? "custom" : "recipe",
  );
  setScopeBadge(
    elements.pixelShapeScopeBadge,
    pixelShapeCustom ? "Custom · carries" : "Carries",
    pixelShapeCustom ? "custom" : "carry",
  );
  setScopeBadge(
    elements.pixelSwitchScopeBadge,
    state.pixelSwitchOverridden ? "Custom · carries" : "Recipe",
    state.pixelSwitchOverridden ? "custom" : "recipe",
  );
  const boundaryAvailable = supportsGestaltBoundary(engine.currentRecipe);
  setScopeBadge(
    elements.orbBoundaryScopeBadge,
    boundaryAvailable ? "Carries" : "Orb only",
    boundaryAvailable
      ? state.orbBoundary === "gestalt"
        ? "custom"
        : "carry"
      : "recipe",
  );
  setScopeBadge(
    elements.orbBackgroundScopeBadge,
    boundaryAvailable
      ? state.orbBackgroundMode !== "none" && state.orbBackgroundColor
        ? `${state.orbBackgroundMode === "pixelated" ? "Pixels" : "Solid"} · carries`
        : "None"
      : "Orb only",
    boundaryAvailable
      ? state.orbBackgroundMode !== "none" && state.orbBackgroundColor
        ? "custom"
        : "carry"
      : "recipe",
  );
  setScopeBadge(
    elements.paletteScopeBadge,
    Object.keys(state.palette).length ? "Custom · carries" : "Theme",
    Object.keys(state.palette).length ? "custom" : "carry",
  );
  setScopeBadge(
    elements.motionScopeBadge,
    motionCustom ? "Custom" : "Recipe",
    motionCustom ? "custom" : "recipe",
  );
  setScopeBadge(
    elements.overrideSummary,
    changes ? `${changes} custom ${changes === 1 ? "change" : "changes"}` : "Recipe base",
    changes ? "custom" : "recipe",
  );
  if (elements.resetOverridesButton) {
    elements.resetOverridesButton.disabled = changes === 0;
  }
  if (elements.undoStudioButton) {
    elements.undoStudioButton.disabled = undoStack.length === 0;
    const latest = undoStack.at(-1);
    elements.undoStudioButton.title = latest
      ? `Undo: ${latest.label}`
      : "Nothing to undo";
  }
}

function pushUndoSnapshot(label) {
  if (isRestoringStudioState) return;
  undoStack.push({ label, state: cloneData(captureStudioState()) });
  if (undoStack.length > 24) undoStack.shift();
  updateWorkflowState();
}

function beginMutation(key, label) {
  if (activeMutations.has(key)) return;
  activeMutations.add(key);
  pushUndoSnapshot(label);
}

function endMutation(key) {
  activeMutations.delete(key);
}

function syncStudioInputs(state) {
  if (elements.fieldSelect) {
    elements.fieldSelect.value = state.fieldOverride || "__recipe__";
  }
  if (elements.transitionSelect) {
    elements.transitionSelect.value =
      transitionOptionName(state.transitionOverride) || "__recipe__";
  }
  if (elements.pixelShapeSelect && state.pixelShape) {
    elements.pixelShapeSelect.value = state.pixelShape;
  }
  if (elements.pixelSwitchSelect) {
    const recipe = spriteById.get(state.sprite);
    elements.pixelSwitchSelect.value = state.pixelSwitchOverridden
      ? pixelSwitchOptionMode(state.pixelSwitch)
      : recipePixelSwitch(recipe);
  }
  if (elements.orbBoundaryInput) {
    elements.orbBoundaryInput.checked = state.orbBoundary === "gestalt";
  }
  orbBackgroundColorChoice =
    state.orbBackgroundColorChoice ||
    state.orbBackgroundColor ||
    orbBackgroundColorChoice;
  orbBackgroundModeChoice = normalizedOrbBackgroundMode(
    state.orbBackgroundModeChoice,
    state.orbBackgroundColorChoice,
  );
  if (orbBackgroundModeChoice === "none") orbBackgroundModeChoice = "solid";
  setOrbBackgroundControlState(
    state.orbBackgroundColor,
    supportsGestaltBoundary(spriteById.get(state.sprite)),
    state.orbBackgroundMode,
  );
  if (elements.resolutionRange) elements.resolutionRange.value = String(state.gridSize);
  if (elements.resolutionValue) {
    elements.resolutionValue.textContent = `${state.gridSize}×${state.gridSize}`;
  }
  if (elements.speedRange) elements.speedRange.value = String(state.speed);
  if (elements.speedValue) elements.speedValue.textContent = `${formatNumber(state.speed)}×`;
  if (elements.densityRange) elements.densityRange.value = String(state.density);
  if (elements.densityValue) {
    elements.densityValue.textContent = formatNumber(state.density);
  }
  if (elements.seedInput) elements.seedInput.value = state.seed;
  if (elements.progressRange) {
    elements.progressRange.value = String(Math.round(state.progress * 100));
  }
  if (elements.progressValue) {
    elements.progressValue.textContent = `${Math.round(state.progress * 100)}%`;
  }
}

function restoreStudioState(state, { announce = null } = {}) {
  if (!state || !spriteById.has(state.sprite)) return;
  isRestoringStudioState = true;
  try {
    stopAutopilot();
    cancelPendingResolutionCommit();
    pendingResolution = state.gridSize;
    activeMutations.clear();
    transitionOverrideValue = normalizeTransitionOption(state.transitionOverride);
    transitionOverridden = Boolean(transitionOverrideValue);
    pixelShapeOverridden = Boolean(state.pixelShapeOverridden);
    const pixelSwitchOverride = normalizePixelSwitchOption(state.pixelSwitch);
    pixelSwitchOverridden = Boolean(
      state.pixelSwitchOverridden && pixelSwitchOverride,
    );
    customNonErrorPalette = { ...(state.palette || {}) };
    savedGlowOverride = state.savedGlowOverride ?? null;
    hadSavedGlowOverride = Boolean(state.hadSavedGlowOverride);
    orbBackgroundColorChoice =
      state.orbBackgroundColorChoice ||
      state.orbBackgroundColor ||
      orbBackgroundColorChoice;
    orbBackgroundModeChoice = normalizedOrbBackgroundMode(
      state.orbBackgroundModeChoice,
      state.orbBackgroundColorChoice,
    );
    if (orbBackgroundModeChoice === "none") orbBackgroundModeChoice = "solid";

    engine.setOptions({
      pixelShape: pixelShapeOverridden ? state.pixelShape : null,
      pixelSwitch: pixelSwitchOverridden ? pixelSwitchOverride : null,
      transition: transitionOverrideValue,
      speed: state.speed,
      density: state.density,
      orbBoundary: state.orbBoundary === "gestalt" ? "gestalt" : "defined",
      orbBackgroundColor:
        normalizedOrbBackgroundColor(state.orbBackgroundColor),
      orbBackgroundMode: normalizedOrbBackgroundMode(
        state.orbBackgroundMode,
        state.orbBackgroundColor,
      ),
    });
    engine.setResolution(state.gridSize);
    engine.setSeed(state.seed);
    engine.setSprite(state.sprite, {
      transition: "instant",
      immediate: true,
      preservePhase: true,
      useRecipeFields: !state.fieldOverride,
    });
    if (state.fieldOverride) engine.setField(state.fieldOverride);
    engine.setProgress(state.progress);
    syncStudioInputs({
      ...state,
      transitionOverride: transitionOverrideValue,
      pixelSwitch: pixelSwitchOverride,
      pixelSwitchOverridden,
    });
    applyPreviewMode(state.previewMode || previewMode);
    updateStateUI(spriteById.get(state.sprite));
    schedulePreviewResolutionSync(state.gridSize);
    applyStudioPalette();
    syncOrbAppearanceToPreviews();
    buildStatePicker(elements.stateSearchInput?.value || "");
    updateWorkflowState();
    updateCode();
  } finally {
    isRestoringStudioState = false;
  }
  if (announce) showWorkflowStatus(announce, { tone: "success" });
}

function resetStudioOverrides() {
  if (!baselineStudioState) return;
  pushUndoSnapshot("reset custom changes");
  const reset = {
    ...cloneData(baselineStudioState),
    sprite: selectedSpriteId,
    previewMode,
  };
  restoreStudioState(reset, { announce: "Custom changes reset to the recipe base." });
}

function undoStudioChange() {
  const entry = undoStack.pop();
  if (!entry) return;
  restoreStudioState(entry.state, { announce: `Undid ${entry.label}.` });
  updateWorkflowState();
}

function portableRecipe() {
  if (!builtInSpriteIds.has(engine.currentRecipe.id)) {
    throw new Error(
      "Imported artwork is not embedded in recipe JSON. Export an image, or reopen the artwork separately.",
    );
  }
  const config = engine.exportConfig();
  const transitionOverride = transitionOverridden
    ? normalizeTransitionOption(transitionOverrideValue)
    : null;
  const pixelSwitchOverride = pixelSwitchOverridden
    ? normalizePixelSwitchOption(engine.options.pixelSwitch)
    : null;
  return {
    ...config,
    transition: transitionOverride || config.transition,
    pixelSwitch: pixelSwitchOverride || config.pixelSwitch,
    package: STUDIO_PACKAGE,
    version: STUDIO_VERSION,
    studio: {
      schema: 1,
      fieldOverride: engine.fieldOverride || null,
      transitionOverride,
      pixelShapeCarries: true,
      pixelSwitchOverridden,
      pixelSwitchOverride,
      orbBackgroundColorChoice,
      orbBackgroundModeChoice,
      customNonErrorPalette: { ...customNonErrorPalette },
    },
  };
}

function normalizePalette(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
  const palette = {};
  for (const key of ["background", "off", "ink", "accent", "glow"]) {
    const color = value[key];
    if (typeof color === "string" && SAFE_COLOR.test(color.trim())) {
      palette[key] = color.trim();
    }
  }
  return palette;
}

function normalizeImportedRecipe(source) {
  if (!source || typeof source !== "object" || Array.isArray(source)) {
    throw new TypeError("Recipe JSON must contain one object.");
  }
  if (source.package && source.package !== STUDIO_PACKAGE) {
    throw new TypeError(`This file targets ${source.package}, not Orby.`);
  }
  if (typeof source.version !== "string") {
    throw new TypeError("Recipe version is missing.");
  }
  const major = Number(source.version.split(".")[0]);
  if (major !== 5) {
    throw new RangeError(`Recipe version ${source.version} is not compatible with v5.`);
  }
  if (!spriteById.has(source.sprite)) {
    throw new RangeError(`Unknown recipe state: ${source.sprite || "missing"}.`);
  }

  const numberInRange = (value, fallback, minimum, maximum) => {
    const number = Number(value);
    return Number.isFinite(number)
      ? Math.min(maximum, Math.max(minimum, number))
      : fallback;
  };
  const studio = source.studio && typeof source.studio === "object" ? source.studio : {};
  const fieldOverride =
    source.fieldMode === "override" && FIELD_IDS.includes(source.field)
      ? source.field
      : FIELD_IDS.includes(studio.fieldOverride)
        ? studio.fieldOverride
        : null;
  const transitionOverride = normalizeTransitionOption(
    Object.hasOwn(studio, "transitionOverride")
      ? studio.transitionOverride
      : source.transition,
  );
  const pixelShape = PIXEL_SHAPES.includes(source.pixelShape)
    ? source.pixelShape
    : baselineStudioState?.pixelShape || "disc";
  const pixelShapeOverridden = Object.hasOwn(studio, "pixelShapeCarries")
    ? Boolean(studio.pixelShapeCarries && PIXEL_SHAPES.includes(source.pixelShape))
    : PIXEL_SHAPES.includes(source.pixelShape);
  const pixelSwitch = normalizePixelSwitchOption(
    Object.hasOwn(studio, "pixelSwitchOverride")
      ? studio.pixelSwitchOverride
      : source.pixelSwitch,
  );
  const paletteSource = studio.customNonErrorPalette || source.nonErrorPalette;
  const importedOrbBackgroundColor =
    typeof source.orbBackgroundColor === "string" &&
    SAFE_COLOR.test(source.orbBackgroundColor.trim()) &&
    source.orbBackgroundColor.trim().toLowerCase() !== "transparent"
      ? source.orbBackgroundColor.trim()
      : null;
  const importedOrbBackgroundMode = ["none", "solid", "pixelated"].includes(
    source.orbBackgroundMode,
  )
    ? source.orbBackgroundMode
    : importedOrbBackgroundColor
      ? "solid"
      : "none";
  const importedOrbBackgroundColorChoice =
    typeof studio.orbBackgroundColorChoice === "string" &&
    SAFE_COLOR.test(studio.orbBackgroundColorChoice.trim())
      ? studio.orbBackgroundColorChoice.trim()
      : importedOrbBackgroundColor || "#262626";
  const importedOrbBackgroundModeChoice =
    studio.orbBackgroundModeChoice === "pixelated" ||
    studio.orbBackgroundModeChoice === "solid"
      ? studio.orbBackgroundModeChoice
      : importedOrbBackgroundMode === "pixelated"
        ? "pixelated"
        : "solid";

  return {
    package: STUDIO_PACKAGE,
    version: source.version,
    sprite: source.sprite,
    seed: String(source.seed || "joan-v5").slice(0, 160),
    gridSize: Math.round(numberInRange(source.gridSize, 68, 12, 96)),
    speed: numberInRange(source.speed, 1, 0.1, 5),
    density: numberInRange(source.density, 1, 0.35, 1.6),
    progress: numberInRange(source.progress, 0, 0, 1),
    fieldOverride,
    transitionOverride,
    pixelShape,
    pixelShapeOverridden,
    pixelSwitch,
    pixelSwitchOverridden: Object.hasOwn(studio, "pixelSwitchOverridden")
      ? Boolean(studio.pixelSwitchOverridden && pixelSwitch)
      : Boolean(pixelSwitch),
    orbBoundary: source.orbBoundary === "gestalt" ? "gestalt" : "defined",
    orbBackgroundColor: importedOrbBackgroundColor,
    orbBackgroundMode: importedOrbBackgroundMode,
    orbBackgroundColorChoice: importedOrbBackgroundColorChoice,
    orbBackgroundModeChoice: importedOrbBackgroundModeChoice,
    palette: normalizePalette(paletteSource),
    savedGlowOverride: null,
    hadSavedGlowOverride: false,
    previewMode,
  };
}

function presetCreatedAtFromId(id) {
  const match = /^preset-(\d{10,})-/.exec(String(id || ""));
  const timestamp = Number(match?.[1]);
  if (!Number.isFinite(timestamp)) return null;
  const date = new Date(timestamp);
  return Number.isNaN(date.getTime()) ? null : date.toISOString();
}

function normalizeStoredPreset(item, index = 0) {
  if (!item || typeof item !== "object" || !item.recipe) return null;
  const id =
    typeof item.id === "string" && item.id.trim()
      ? item.id.trim().slice(0, 160)
      : `preset-migrated-${index}`;
  const candidateDate =
    typeof item.createdAt === "string" ? new Date(item.createdAt) : null;
  const createdAt =
    candidateDate && !Number.isNaN(candidateDate.getTime())
      ? candidateDate.toISOString()
      : presetCreatedAtFromId(id);
  return {
    id,
    label: String(item.label || "Saved preset").slice(0, 72),
    createdAt,
    recipe: item.recipe,
  };
}

function presetStateLabel(preset) {
  const recipe = spriteById.get(preset?.recipe?.sprite);
  return recipe ? recipeLabel(recipe) : humanize(preset?.recipe?.sprite || "Saved state");
}

function presetTitle(preset) {
  return String(preset?.label || presetStateLabel(preset)).trim();
}

function presetDateLabel(preset) {
  if (!preset?.createdAt) return "Saved locally";
  const date = new Date(preset.createdAt);
  if (Number.isNaN(date.getTime())) return "Saved locally";
  return new Intl.DateTimeFormat(undefined, {
    month: "short",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
  }).format(date);
}

function paintSavedPresetPreview(canvas, preset) {
  const spriteId = preset?.recipe?.sprite;
  if (!canvas || !spriteById.has(spriteId)) return;
  const previewKey = preset.id;
  const queued = savedPresetPreviewQueue.get(previewKey);
  if (queued) {
    queued.push(canvas);
    return;
  }
  const canvases = [canvas];
  savedPresetPreviewQueue.set(previewKey, canvases);
  enqueuePreviewWork(() => {
    savedPresetPreviewQueue.delete(previewKey);
    if (previewsDisposed) return;
    let preview = null;
    try {
      const state = normalizeImportedRecipe(preset.recipe);
      const source = document.createElement("canvas");
      source.width = 50;
      source.height = 50;
      const options = {
        sprite: state.sprite,
        seed: state.seed,
        gridSize: state.gridSize,
        speed: state.speed,
        density: state.density,
        progress: state.progress,
        pixelShape: state.pixelShapeOverridden ? state.pixelShape : null,
        pixelSwitch: state.pixelSwitchOverridden ? state.pixelSwitch : null,
        transition: null,
        orbBoundary: state.orbBoundary,
        orbBackgroundColor: state.orbBackgroundColor,
        orbBackgroundMode: state.orbBackgroundMode,
        nonErrorPalette: {
          ...studioPaletteOverride(),
          ...state.palette,
        },
        autoplay: false,
        autoResize: false,
        interactive: false,
        background: true,
        offPixels: false,
        quality: "low",
        dprMax: 1,
        reducedMotion: true,
      };
      if (state.fieldOverride) options.field = state.fieldOverride;
      preview = new JoanGlyphEngine(source, options);
      renderThumbnailRestFrame(preview, { progress: state.progress });
      for (const target of canvases) {
        if (!target.isConnected) continue;
        const context = target.getContext("2d");
        if (!context) continue;
        context.clearRect(0, 0, target.width, target.height);
        context.imageSmoothingEnabled = false;
        context.drawImage(source, 0, 0, target.width, target.height);
        target.dataset.previewState = "ready";
      }
    } catch {
      // A legacy preset that no longer validates still gets the authored state
      // thumbnail instead of leaving a permanently empty tile.
      const fallback = ensurePreviewEngine(spriteId);
      const source = previewDescriptors.get(spriteId)?.canvas;
      if (!fallback || !source) return;
      if (!previewShouldAnimate(spriteId)) freezePreview(fallback);
      for (const target of canvases) {
        if (!target.isConnected) continue;
        const context = target.getContext("2d");
        if (!context) continue;
        context.clearRect(0, 0, target.width, target.height);
        context.imageSmoothingEnabled = false;
        context.drawImage(source, 0, 0, target.width, target.height);
        target.dataset.previewState = "ready";
      }
    } finally {
      preview?.destroy();
    }
  });
}

function observeSavedPresetPreview(canvas, preset) {
  if (typeof IntersectionObserver !== "function") {
    paintSavedPresetPreview(canvas, preset);
    return;
  }
  if (!savedPresetPreviewObserver) {
    savedPresetPreviewObserver = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          if (!entry.isIntersecting) continue;
          savedPresetPreviewObserver.unobserve(entry.target);
          const current = recentPresets.find(
            (item) => item.id === entry.target.dataset.presetId,
          );
          if (current) paintSavedPresetPreview(entry.target, current);
        }
      },
      {
        root: elements.savedPresetGallery,
        rootMargin: "120px 0px",
        threshold: 0.01,
      },
    );
  }
  canvas.dataset.presetId = preset.id;
  savedPresetPreviewObserver.observe(canvas);
}

function renderRecentPresets() {
  const hasPresets = recentPresets.length > 0;
  const select = elements.recentPresetSelect;
  if (select) {
    select.textContent = "";
    const placeholder = document.createElement("option");
    placeholder.value = "";
    placeholder.textContent = hasPresets ? "Quick open preset" : "No saved presets";
    select.append(placeholder);
    for (const preset of recentPresets) {
      const option = document.createElement("option");
      option.value = preset.id;
      option.textContent = `${presetTitle(preset)} · ${presetDateLabel(preset)}`;
      select.append(option);
    }
    select.disabled = !hasPresets;
  }

  if (elements.savedPresetCount) {
    const count = recentPresets.length;
    elements.savedPresetCount.textContent = `${count} preset${count === 1 ? "" : "s"}`;
  }
  if (elements.savedPresetEmpty) elements.savedPresetEmpty.hidden = hasPresets;
  if (!elements.savedPresetGallery) return;

  savedPresetPreviewObserver?.disconnect();
  elements.savedPresetGallery.textContent = "";
  elements.savedPresetGallery.hidden = !hasPresets;
  if (!hasPresets) return;

  const fragment = document.createDocumentFragment();
  const previews = [];
  for (const preset of recentPresets) {
    const stateLabel = presetStateLabel(preset);
    const titleLabel = presetTitle(preset);
    const accessibleIdentity =
      titleLabel === stateLabel ? stateLabel : `${titleLabel}, ${stateLabel}`;
    const dateLabel = presetDateLabel(preset);
    const savedDescription = preset.createdAt ? `saved ${dateLabel}` : dateLabel;
    const resolution = Math.round(Number(preset.recipe?.gridSize) || 68);
    const item = document.createElement("article");
    item.className = "saved-preset-card";
    item.dataset.presetId = preset.id;
    item.setAttribute("role", "listitem");

    const openButton = document.createElement("button");
    openButton.type = "button";
    openButton.className = "saved-preset-card__open";
    openButton.dataset.openPreset = preset.id;
    openButton.setAttribute(
      "aria-label",
      `Open ${accessibleIdentity}, ${savedDescription}`,
    );

    const previewWrap = document.createElement("span");
    previewWrap.className = "saved-preset-card__preview";
    const canvas = document.createElement("canvas");
    canvas.width = 50;
    canvas.height = 50;
    canvas.dataset.previewState = "pending";
    canvas.setAttribute("aria-hidden", "true");
    previewWrap.append(canvas);

    const copy = document.createElement("span");
    copy.className = "saved-preset-card__copy";
    const label = document.createElement("strong");
    label.textContent = titleLabel;
    const state = document.createElement("small");
    state.textContent =
      titleLabel === stateLabel
        ? preset.recipe?.sprite || stateLabel
        : `${stateLabel} · ${preset.recipe?.sprite || "custom"}`;
    const metadata = document.createElement("span");
    metadata.className = "saved-preset-card__metadata";
    metadata.textContent = `${resolution}×${resolution} · ${dateLabel}`;
    copy.append(label, state, metadata);
    openButton.append(previewWrap, copy);

    const deleteButton = document.createElement("button");
    deleteButton.type = "button";
    deleteButton.className = "saved-preset-card__delete";
    deleteButton.dataset.deletePreset = preset.id;
    deleteButton.textContent = "Delete";
    deleteButton.setAttribute(
      "aria-label",
      `Delete ${accessibleIdentity}, ${savedDescription}`,
    );

    item.append(openButton, deleteButton);
    fragment.append(item);
    previews.push([canvas, preset]);
  }
  elements.savedPresetGallery.append(fragment);
  for (const [canvas, preset] of previews) observeSavedPresetPreview(canvas, preset);
}

function persistRecentPresets() {
  try {
    localStorage.setItem(RECENT_PRESET_KEY, JSON.stringify(recentPresets));
  } catch {
    showWorkflowStatus("Preset changes apply to this session only.", {
      tone: "warning",
    });
  }
  renderRecentPresets();
}

function loadRecentPresets() {
  let stored = [];
  try {
    const parsed = JSON.parse(localStorage.getItem(RECENT_PRESET_KEY) || "[]");
    stored = Array.isArray(parsed) ? parsed : [];
  } catch {
    stored = [];
  }
  recentPresets = stored
    .map((item, index) => normalizeStoredPreset(item, index))
    .filter(Boolean)
    .slice(0, MAX_RECENT_PRESETS);
  if (
    JSON.stringify(stored.slice(0, MAX_RECENT_PRESETS)) !==
    JSON.stringify(recentPresets)
  ) {
    try {
      localStorage.setItem(RECENT_PRESET_KEY, JSON.stringify(recentPresets));
    } catch {
      // Keep migrated presets available for this session when storage is read-only.
    }
  }
  renderRecentPresets();
}

function rememberPreset(recipe, label) {
  const createdAt = new Date().toISOString();
  const id = `preset-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
  recentPresets = [
    {
      id,
      label: String(label || "Saved preset").slice(0, 72),
      createdAt,
      recipe,
    },
    ...recentPresets,
  ].slice(0, MAX_RECENT_PRESETS);
  persistRecentPresets();
  return id;
}

function openRecentPreset(id) {
  const preset = recentPresets.find((item) => item.id === id);
  if (!preset) return;
  try {
    applyPortableRecipe(preset.recipe, { label: preset.label });
  } catch (error) {
    showWorkflowStatus(error?.message || "The saved preset could not be opened.", {
      tone: "danger",
    });
  }
}

function deleteRecentPreset(id) {
  const index = recentPresets.findIndex((item) => item.id === id);
  if (index < 0) return;
  const [preset] = recentPresets.splice(index, 1);
  deletedPreset = { preset, index };
  persistRecentPresets();
  if (elements.deletedPresetMessage) {
    elements.deletedPresetMessage.textContent = `${presetTitle(preset)} deleted.`;
  }
  if (elements.deletedPresetNotice) elements.deletedPresetNotice.hidden = false;
  showWorkflowStatus(`${presetTitle(preset)} deleted. Undo is available below.`, {
    tone: "warning",
  });
  window.requestAnimationFrame(() => elements.undoDeletePresetButton?.focus());
}

function undoDeletedPreset() {
  if (!deletedPreset) return;
  const { preset, index } = deletedPreset;
  recentPresets.splice(Math.min(index, recentPresets.length), 0, preset);
  recentPresets = recentPresets.slice(0, MAX_RECENT_PRESETS);
  deletedPreset = null;
  persistRecentPresets();
  if (elements.deletedPresetNotice) elements.deletedPresetNotice.hidden = true;
  showWorkflowStatus(`${presetTitle(preset)} restored.`, { tone: "success" });
  window.requestAnimationFrame(() => {
    const restoredButton = [
      ...(elements.savedPresetGallery?.querySelectorAll("[data-open-preset]") || []),
    ].find((button) => button.dataset.openPreset === preset.id);
    restoredButton?.focus();
  });
}

function saveCurrentPreset() {
  let recipe;
  try {
    recipe = portableRecipe();
  } catch (error) {
    showWorkflowStatus(error.message, { tone: "danger" });
    return;
  }
  const date = new Intl.DateTimeFormat(undefined, {
    month: "short",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
  }).format(new Date());
  const label = recipeLabel(engine.currentRecipe);
  rememberPreset(recipe, label);
  showWorkflowStatus(`Saved ${label} · ${date} to recent presets.`, {
    tone: "success",
  });
}

function applyPortableRecipe(source, { label = "recipe", remember = false } = {}) {
  const normalized = normalizeImportedRecipe(source);
  pushUndoSnapshot(`open ${label}`);
  restoreStudioState(normalized, {
    announce: `Opened ${label}. ${recipeLabel(spriteById.get(normalized.sprite))} is on stage.`,
  });
  if (remember) rememberPreset(source, label);
}

async function openRecipeFile(file) {
  if (!file) return;
  if (file.size > 1024 * 1024) {
    throw new RangeError("Recipe files must be smaller than 1 MB.");
  }
  const source = JSON.parse(await file.text());
  const label = file.name.replace(/\.joan\.json$|\.json$/i, "") || "recipe";
  applyPortableRecipe(source, { label, remember: true });
}

function updateCode() {
  if (!elements.codeSnippet) return;
  const config = engine.exportConfig();
  const paletteLine = config.nonErrorPalette
    ? `\n  nonErrorPalette: ${formatCodeValue(config.nonErrorPalette, "  ")},`
    : "";
  const fieldLine =
    config.fieldMode === "override" ? `\n  field: "${config.field}",` : "";
  const transitionValue = transitionOverridden
    ? normalizeTransitionOption(transitionOverrideValue)
    : null;
  const transitionLine = transitionValue
    ? `\n  transition: ${formatCodeValue(transitionValue)},`
    : "";
  elements.codeSnippet.textContent = `import { createGlyph } from "@joan/procedural-glyph-engine";
import { playStateSequence } from "@joan/procedural-glyph-engine/state-sequence";

const glyph = createGlyph("#ai-glyph", {
  sprite: "${config.sprite}",
  seed: "${config.seed.replaceAll('"', '\\"')}",
  gridSize: ${config.gridSize},
  speed: ${formatNumber(config.speed)},
  density: ${formatNumber(config.density)},${paletteLine}${fieldLine}${transitionLine}
  orbBoundary: "${config.orbBoundary}",
  orbBackgroundColor: ${JSON.stringify(config.orbBackgroundColor)},
  orbBackgroundMode: "${config.orbBackgroundMode}",
  pixelShape: "${config.pixelShape}",
  pixelSwitch: ${formatCodeValue(config.pixelSwitch)},
  interactive: true
});

const playback = playStateSequence(glyph, [
  { sprite: "${config.sprite}", holdMs: 5_000 },
  { sprite: "status.success", transition: "path-draw" }
]);

await playback.finished;`;
}

function updateStateUI(recipe) {
  if (!recipe) return;
  const label = recipeLabel(recipe);
  const category = recipe.semantic?.category || recipe.category || "semantic";
  const previousSelectedId = selectedSpriteId;
  selectedSpriteId = recipe.id;
  ensurePreviewEngine(recipe.id);
  if (previousSelectedId !== selectedSpriteId) {
    freezePreviewIfInactive(previousSelectedId);
  }
  if (elements.stateTitle) elements.stateTitle.textContent = label;
  if (elements.stateDescription) {
    elements.stateDescription.textContent = recipeDescription(recipe);
  }
  if (elements.stateCategory) elements.stateCategory.textContent = humanize(category);
  if (elements.currentStateValue) elements.currentStateValue.textContent = recipe.id;
  const libraryHasSelection = spriteButtons.has(recipe.id);
  for (const [id, button] of spriteButtons) {
    const selected = id === recipe.id;
    button.classList.toggle("is-selected", selected);
    button.setAttribute("aria-checked", String(selected));
    button.tabIndex = selected || (!libraryHasSelection && id === sprites[0]?.id) ? 0 : -1;
  }
  for (const button of document.querySelectorAll("[data-quick-sprite]")) {
    const selected = button.dataset.quickSprite === recipe.id;
    button.classList.toggle("is-selected", selected);
    button.setAttribute("aria-pressed", String(selected));
  }

  if (elements.fieldSelect) {
    elements.fieldSelect.value = engine.fieldOverride || "__recipe__";
  }
  if (elements.transitionSelect && !transitionOverridden) {
    elements.transitionSelect.value = "__recipe__";
  }
  if (elements.pixelShapeSelect && !pixelShapeOverridden) {
    const shapeAliases = { circle: "disc", "rounded-square": "square" };
    const sourceShape =
      recipe.pixelShape || recipe.render?.pixelShape || recipe.pixel?.shape;
    const shape = shapeAliases[sourceShape] || sourceShape;
    if (PIXEL_SHAPES.includes(shape)) elements.pixelShapeSelect.value = shape;
  }
  if (elements.pixelSwitchSelect && !pixelSwitchOverridden) {
    const pixelSwitch = recipePixelSwitch(recipe);
    if (PIXEL_SWITCHES.includes(pixelSwitch)) {
      elements.pixelSwitchSelect.value = pixelSwitch;
    }
  }
  updateControlTip(elements.fieldSelect);
  updateControlTip(elements.transitionSelect);
  updateControlTip(elements.pixelShapeSelect);
  updateControlTip(elements.pixelSwitchSelect);

  const boundaryAvailable = supportsGestaltBoundary(recipe);
  if (elements.orbBoundaryControl) {
    elements.orbBoundaryControl.dataset.available = String(boundaryAvailable);
  }
  if (elements.orbBoundaryInput) {
    elements.orbBoundaryInput.disabled = !boundaryAvailable;
    elements.orbBoundaryInput.checked =
      engine.options.orbBoundary === "gestalt";
  }
  if (elements.orbBoundaryAvailability) {
    elements.orbBoundaryAvailability.textContent = boundaryAvailable
      ? engine.options.orbBoundary === "gestalt"
        ? "Circle implied by clustered edge pixels"
        : "Continuous authored rim"
      : "Available for Ready and ambient orbs · choice stays saved";
  }
  updateControlTip(elements.orbBoundaryInput);
  setOrbBackgroundControlState(
    engine.options.orbBackgroundColor,
    boundaryAvailable,
  );

  const isErrorState = recipe.id === "status.error";
  elements.nonErrorPaletteSection?.classList.toggle(
    "is-error-state",
    isErrorState,
  );
  if (!isErrorState) updatePaletteControls(engine.exportConfig().palette);

  const supportsProgress =
    recipe.id === "ai.progress" || recipe.id === "transfer.active";
  elements.progressControl?.classList.toggle("is-visible", supportsProgress);
  elements.progressControl?.setAttribute("aria-hidden", String(!supportsProgress));
  if (elements.statePickerSelect) elements.statePickerSelect.value = recipe.id;
  buildStatePicker(elements.stateSearchInput?.value || "");
  updateWorkflowState();
  updateCode();
}

function selectSprite(id, options = {}) {
  const recipe = spriteById.get(id);
  if (!recipe) return;
  if (options.recordUndo !== false && recipe.id !== selectedSpriteId) {
    pushUndoSnapshot(`select ${recipeLabel(recipe)}`);
  }
  engine.setSprite(id, {
    transition:
      options.transition ||
      (transitionOverridden ? transitionOverrideValue : undefined),
    duration: options.duration,
    preservePhase: options.preservePhase,
  });
  updateStateUI(recipe);
}

function ensurePreviewEngine(id) {
  if (previewsDisposed) return null;
  const existing = previewById.get(id);
  if (existing) {
    previewCardObserver?.unobserve(spriteButtons.get(id));
    return existing;
  }
  const descriptor = previewDescriptors.get(id);
  if (!descriptor) return null;
  const { canvas, index, recipe } = descriptor;
  const preview = new JoanGlyphEngine(canvas, {
    sprite: createThumbnailRecipe(recipe),
    seed: `preview-${index}`,
    gridSize: engine.gridSize,
    progress: currentPreviewProgress(),
    autoplay: false,
    autoResize: false,
    interactive: false,
    background: true,
    offPixels: false,
    quality: "low",
    dprMax: 1,
    fps: 12,
    // The constructor paints the authored representative pose once. Restore
    // the user's motion preference immediately afterward for active cards.
    reducedMotion: true,
    nonErrorPalette: studioPaletteOverride(),
    orbBoundary: engine.options.orbBoundary,
    orbBackgroundColor: engine.options.orbBackgroundColor,
    orbBackgroundMode: engine.options.orbBackgroundMode,
  });
  preview.previewSpriteId = recipe.id;
  preview.reducedMotion = reduceMotion.matches;
  previewEngines.push(preview);
  previewById.set(recipe.id, preview);
  previewCardObserver?.unobserve(spriteButtons.get(recipe.id));
  setPreviewResolution(preview, engine.gridSize);
  if (preview.canvas?.dataset) preview.canvas.dataset.previewState = "ready";
  return preview;
}

function scheduleDeferredPreviewConstruction() {
  // IntersectionObserver constructs only cards approaching the viewport. The
  // idle sweep remains as a compatibility fallback for older embedded hosts.
  if (typeof IntersectionObserver === "function") return;
  const pendingIds = sprites
    .map((recipe) => recipe.id)
    .filter((id) => id !== selectedSpriteId);
  const buildNext = (deadline) => {
    if (previewsDisposed) return;
    const minimumBatch = 1;
    const maximumBatch = 1;
    let built = 0;

    while (pendingIds.length && built < maximumBatch) {
      const id = pendingIds.shift();
      if (!id || previewById.has(id)) continue;
      ensurePreviewEngine(id);
      built += 1;

      const remaining =
        typeof deadline?.timeRemaining === "function"
          ? deadline.timeRemaining()
          : 0;
      if (built >= minimumBatch && remaining < 6) break;
    }

    if (pendingIds.length) enqueuePreviewWork(buildNext);
  };
  if (pendingIds.length) enqueuePreviewWork(buildNext);
}

function flushUrgentPreviewConstruction() {
  urgentPreviewScheduled = false;
  const id = urgentPreviewIds.values().next().value;
  if (id) urgentPreviewIds.delete(id);
  if (id && !previewById.has(id)) ensurePreviewEngine(id);
  if (urgentPreviewIds.size && !previewsDisposed) {
    scheduleUrgentPreviewConstruction();
  }
}

function scheduleUrgentPreviewConstruction() {
  if (urgentPreviewScheduled || !urgentPreviewIds.size || previewsDisposed) return;
  urgentPreviewScheduled = true;
  enqueuePreviewWork(flushUrgentPreviewConstruction, { priority: true });
}

function prioritizePreviewConstruction(ids) {
  for (const id of ids) {
    if (id && !previewById.has(id)) urgentPreviewIds.add(id);
  }
  scheduleUrgentPreviewConstruction();
}

function startPreviewCardObserver() {
  if (typeof IntersectionObserver !== "function") return;
  previewCardObserver = new IntersectionObserver(
    (entries) => {
      const visibleIds = [];
      for (const entry of entries) {
        const id = entry.target.dataset.sprite;
        if (entry.isIntersecting) visibleIds.push(id);
        else urgentPreviewIds.delete(id);
      }
      prioritizePreviewConstruction(visibleIds);
    },
    { rootMargin: "600px 0px", threshold: 0.01 },
  );
  for (const [id, button] of spriteButtons) {
    if (!previewById.has(id)) previewCardObserver.observe(button);
  }
}

function buildSpriteLibrary() {
  if (!elements.spriteGrid) return;
  const fragment = document.createDocumentFragment();
  sprites.forEach((recipe, index) => {
    const button = document.createElement("button");
    button.type = "button";
    button.className = "sprite-card";
    button.dataset.sprite = recipe.id;
    button.setAttribute("role", "radio");
    button.setAttribute("aria-checked", String(recipe.id === selectedSpriteId));
    button.tabIndex = recipe.id === selectedSpriteId ? 0 : -1;
    button.setAttribute(
      "aria-label",
      `${
        recipe.labels?.default ||
        recipe.semantic?.label ||
        recipe.label ||
        humanize(recipe.id)
      }: ${recipeDescription(recipe)}`,
    );

    const previewWrap = document.createElement("span");
    previewWrap.className = "sprite-preview";
    const canvas = document.createElement("canvas");
    canvas.width = 64;
    canvas.height = 64;
    canvas.dataset.previewState = "pending";
    canvas.setAttribute("aria-hidden", "true");
    previewWrap.append(canvas);

    const copy = document.createElement("span");
    copy.className = "sprite-copy";
    const label = document.createElement("strong");
    label.textContent =
      recipe.labels?.default ||
      recipe.semantic?.label ||
      recipe.label ||
      humanize(recipe.id);
    const idLabel = document.createElement("small");
    idLabel.textContent = recipe.id;
    copy.append(label, idLabel);
    button.append(previewWrap, copy);
    button.addEventListener("pointerenter", () => {
      hoveredPreviewIds.add(recipe.id);
      ensurePreviewEngine(recipe.id);
    });
    button.addEventListener("pointerleave", () => {
      hoveredPreviewIds.delete(recipe.id);
      freezePreviewIfInactive(recipe.id);
    });
    button.addEventListener("focus", () => {
      if (!button.matches(":focus-visible")) return;
      focusedPreviewIds.add(recipe.id);
      ensurePreviewEngine(recipe.id);
    });
    button.addEventListener("blur", () => {
      focusedPreviewIds.delete(recipe.id);
      freezePreviewIfInactive(recipe.id);
    });
    button.addEventListener("click", () => selectSprite(recipe.id));
    button.addEventListener("keydown", (event) => {
      const keys = ["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp", "Home", "End"];
      if (!keys.includes(event.key)) return;
      event.preventDefault();
      const direction = event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : -1;
      let nextIndex = index;
      if (event.key === "Home") nextIndex = 0;
      else if (event.key === "End") nextIndex = sprites.length - 1;
      else nextIndex = (index + direction + sprites.length) % sprites.length;
      const next = spriteButtons.get(sprites[nextIndex].id);
      selectSprite(sprites[nextIndex].id);
      next?.focus();
    });
    fragment.append(button);
    spriteButtons.set(recipe.id, button);
    previewDescriptors.set(recipe.id, { canvas, index, recipe });
  });
  elements.spriteGrid.append(fragment);
  ensurePreviewEngine(selectedSpriteId);
  startPreviewCardObserver();
  scheduleDeferredPreviewConstruction();
}

setTheme(isLight, { persist: false });
heroEngine = elements.heroCanvas
  ? new JoanGlyphEngine(elements.heroCanvas, {
      sprite: "ai.idle",
      seed: "world-class-ai",
      gridSize: 68,
      speed: 1,
      density: 1,
      interactive: false,
      background: false,
      offPixels: true,
      fps: 30,
      quality: "auto",
      dprMax: 1.5,
      nonErrorPalette: studioPaletteOverride(),
    })
  : null;
buildSpriteLibrary();
updateStateUI(engine.currentRecipe);
applyPreviewMode(previewMode);
const initialPixelShape = elements.pixelShapeSelect?.value;
if (PIXEL_SHAPES.includes(initialPixelShape)) {
  engine.setPixelShape(initialPixelShape);
  pixelShapeOverridden = true;
}
engine.setProgress((Number(elements.progressRange?.value) || 0) / 100);
baselineStudioState = cloneData(captureStudioState());
buildStatePicker();
loadRecentPresets();
updateWorkflowState();
updateCode();

let previewLast = 0;
function animatePreviews(timestamp) {
  requestAnimationFrame(animatePreviews);
  if (reduceMotion.matches || document.hidden || timestamp - previewLast < 90) return;
  const previewDt = previewLast
    ? Math.min(0.12, (timestamp - previewLast) / 1000)
    : 1 / 12;
  previewLast = timestamp;
  for (const preview of previewEngines) {
    const id = previewId(preview);
    if (!preview.visible || !previewShouldAnimate(id)) continue;
    if (preview.gridSize !== engine.gridSize) {
      setPreviewResolution(preview, engine.gridSize);
    }
    preview.renderFrame(timestamp / 1000, previewDt);
  }
}
requestAnimationFrame(animatePreviews);

function updatePlayButton() {
  if (!elements.playButton) return;
  elements.playButton.textContent = engine.running ? "Pause motion" : "Play motion";
  elements.playButton.setAttribute("aria-pressed", String(engine.running));
}
updatePlayButton();

function stopAutopilot() {
  if (autopilotTimer !== null) window.clearInterval(autopilotTimer);
  autopilotTimer = null;
  elements.autopilotButton?.classList.remove("is-active");
  elements.autopilotButton?.setAttribute("aria-pressed", "false");
  if (elements.autopilotButton) {
    elements.autopilotButton.textContent = "Auto-play AI states";
    elements.autopilotButton.title =
      "Automatically cycles through 10 common AI states every 4 seconds. Click again to stop.";
  }
}

const story = [
  "ai.idle",
  "ai.listening",
  "ai.thinking",
  "ai.thinking-deep",
  "ai.still-working",
  "ai.searching",
  "ai.tool-use",
  "ai.generating",
  "ai.speaking",
  "status.success",
];

function toggleAutopilot() {
  if (autopilotTimer !== null) {
    stopAutopilot();
    return;
  }
  autopilotIndex = Math.max(0, story.indexOf(selectedSpriteId));
  selectSprite(story[autopilotIndex], { duration: 0.5, recordUndo: false });
  autopilotTimer = window.setInterval(() => {
    autopilotIndex = (autopilotIndex + 1) % story.length;
    selectSprite(story[autopilotIndex], { duration: 0.5, recordUndo: false });
    if (story[autopilotIndex] === "ai.generating") {
      engine.signal("token", { energy: 0.8 });
    }
    if (story[autopilotIndex] === "status.success") {
      engine.signal("complete", { energy: 1 });
    }
  }, 4200);
  elements.autopilotButton?.classList.add("is-active");
  elements.autopilotButton?.setAttribute("aria-pressed", "true");
  if (elements.autopilotButton) {
    elements.autopilotButton.textContent = "Stop AI state auto-play";
    elements.autopilotButton.title = "Stop the automatic AI state sequence.";
  }
}

function setTheme(light, { persist = true } = {}) {
  isLight = Boolean(light);
  const theme = isLight ? "light" : "dark";
  document.documentElement.dataset.theme = theme;
  document.documentElement.style.colorScheme = theme;
  applyStudioPalette({ immediatePreviewRefresh: true });
  if (elements.themeButton) {
    const nextThemeLabel = isLight ? "Dark mode" : "Light mode";
    elements.themeButton.textContent = nextThemeLabel;
    elements.themeButton.setAttribute(
      "aria-label",
      `Switch to ${nextThemeLabel}`,
    );
    elements.themeButton.setAttribute("aria-checked", String(isLight));
    elements.themeButton.title = `Switch to ${nextThemeLabel}`;
  }
  const themeColor = document.querySelector('meta[name="theme-color"]');
  if (themeColor) themeColor.content = isLight ? "#eeece6" : "#050506";
  if (persist) {
    try {
      window.localStorage.setItem(THEME_STORAGE_KEY, theme);
    } catch {
      // Storage can be unavailable in private or embedded contexts.
    }
  }
  updateWorkflowState();
}

function randomSeed() {
  pushUndoSnapshot("generate a new seed");
  const bytes = new Uint32Array(2);
  crypto.getRandomValues(bytes);
  const value = `joan-${bytes[0].toString(36)}-${bytes[1].toString(36)}`;
  if (elements.seedInput) elements.seedInput.value = value;
  engine.setSeed(value);
  engine.signal("reseed", { energy: 0.6 });
  updateWorkflowState();
  updateCode();
}

async function copyCode() {
  const code = elements.codeSnippet?.textContent || "";
  try {
    await navigator.clipboard.writeText(code);
  } catch {
    const range = document.createRange();
    range.selectNodeContents(elements.codeSnippet);
    const selection = window.getSelection();
    selection.removeAllRanges();
    selection.addRange(range);
    document.execCommand("copy");
    selection.removeAllRanges();
  }
  const old = elements.copyCodeButton?.textContent;
  if (elements.copyCodeButton) elements.copyCodeButton.textContent = "Copied";
  showWorkflowStatus("Integration code copied.", { tone: "success" });
  window.setTimeout(() => {
    if (elements.copyCodeButton) elements.copyCodeButton.textContent = old || "Copy";
  }, 1400);
}

function downloadJSON(
  filename = `${engine.currentRecipe.id.replaceAll(".", "-")}.joan.json`,
) {
  const data = JSON.stringify(portableRecipe(), null, 2);
  const blob = new Blob([`${data}\n`], { type: "application/json" });
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = filename;
  anchor.click();
  URL.revokeObjectURL(url);
  return blob;
}

async function downloadStudioPNG(
  filename = `${engine.currentRecipe.id.replaceAll(".", "-")}.png`,
  { background = true } = {},
) {
  const previousMode = previewMode;
  const previousBackground = engine.options.background;

  engine.setOptions({ previewMode: "fit", background });
  engine.paint();
  try {
    return await engine.downloadPNG(filename);
  } finally {
    engine.setOptions({
      previewMode: previousMode,
      background: previousBackground,
    });
    engine.paint();
  }
}

const EXPORT_FORMATS = Object.freeze({
  png: {
    extension: ".png",
    label: "PNG",
    motion: "Current frame",
  },
  svg: {
    extension: ".svg",
    label: "SVG",
    motion: "Current frame",
  },
  "animated-svg": {
    extension: ".animated.svg",
    label: "Animated SVG",
    motion: "2.4 s · 12 fps · 48×48 max grid",
  },
  config: {
    extension: ".joan.json",
    label: "Orby recipe",
    motion: "Deterministic parameters",
  },
});

function exportBaseName() {
  return engine.currentRecipe.id.replaceAll(".", "-");
}

function defaultExportFilename(format) {
  return `${exportBaseName()}${EXPORT_FORMATS[format]?.extension || ".png"}`;
}

function sanitizeExportFilename(value, format) {
  const meta = EXPORT_FORMATS[format] || EXPORT_FORMATS.png;
  let filename = String(value || "")
    .trim()
    .replace(/[\\/:*?"<>|]+/g, "-")
    .replace(/^\.+/, "");
  if (!filename) filename = defaultExportFilename(format);
  const knownExtension = [...Object.values(EXPORT_FORMATS)]
    .sort((left, right) => right.extension.length - left.extension.length)
    .find(({ extension }) => filename.toLowerCase().endsWith(extension));
  if (knownExtension && knownExtension.extension !== meta.extension) {
    filename = filename.slice(0, -knownExtension.extension.length);
  }
  if (!filename.toLowerCase().endsWith(meta.extension)) filename += meta.extension;
  return filename.slice(0, 128);
}

function renderExportPreview() {
  const canvas = elements.exportPreviewCanvas;
  const context = canvas?.getContext("2d");
  if (!canvas || !context) return;
  const includeBackground = elements.exportBackgroundInput?.checked !== false;
  const previousMode = engine.options.previewMode;
  const previousBackground = engine.options.background;
  try {
    engine.setOptions({ previewMode: "fit", background: includeBackground });
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.imageSmoothingEnabled = false;
    context.drawImage(engine.canvas, 0, 0, canvas.width, canvas.height);
  } finally {
    engine.setOptions({
      previewMode: previousMode,
      background: previousBackground,
    });
  }
  canvas.closest(".export-preview__frame")?.classList.toggle(
    "is-transparent",
    !includeBackground,
  );
}

function updateExportPreflight({ resetFilename = false } = {}) {
  const format = elements.exportFormatSelect?.value || "png";
  const meta = EXPORT_FORMATS[format] || EXPORT_FORMATS.png;
  const vector = format === "svg" || format === "animated-svg";
  const recipe = format === "config";
  const size = Number(elements.exportSizeSelect?.value) || 512;
  const includeBackground = elements.exportBackgroundInput?.checked !== false;
  const recipePortable = builtInSpriteIds.has(engine.currentRecipe.id);

  if (elements.exportSizeSelect) elements.exportSizeSelect.disabled = !vector;
  if (elements.exportBackgroundInput) elements.exportBackgroundInput.disabled = recipe;
  if (elements.exportFilenameInput && resetFilename) {
    elements.exportFilenameInput.value = defaultExportFilename(format);
  }
  if (elements.exportDimensionsValue) {
    elements.exportDimensionsValue.textContent = recipe
      ? "Portable JSON"
      : format === "png"
        ? `${engine.canvas.width}×${engine.canvas.height} px`
        : `${size}×${size} px`;
  }
  if (elements.exportCompositionValue) {
    elements.exportCompositionValue.textContent = recipe
      ? "Parameters only"
      : "Fit · full frame";
  }
  if (elements.exportMotionValue) elements.exportMotionValue.textContent = meta.motion;
  if (elements.exportBackgroundValue) {
    elements.exportBackgroundValue.textContent = recipe
      ? "Stored in palette"
      : includeBackground
        ? "Included"
        : "Transparent";
  }
  if (elements.exportPreviewNote) {
    elements.exportPreviewNote.textContent = recipe
      ? recipePortable
        ? "The recipe stores deterministic controls and can be reopened in this studio."
        : "Imported artwork is not embedded in JSON. Export an image or reopen the artwork separately."
      : "Export uses the fitted full-frame composition. Fit and 1:1 only change the inspection view.";
  }
  if (elements.confirmExportButton) {
    elements.confirmExportButton.textContent = `Export ${meta.label}`;
    elements.confirmExportButton.disabled = recipe && !recipePortable;
  }
  renderExportPreview();
}

function openExportPreflight(format = "png") {
  if (!EXPORT_FORMATS[format] || !elements.exportDialog) return;
  exportReturnFocus = document.activeElement;
  if (elements.exportFormatSelect) elements.exportFormatSelect.value = format;
  updateExportPreflight({ resetFilename: true });
  if (typeof elements.exportDialog.showModal === "function") {
    elements.exportDialog.showModal();
  } else {
    elements.exportDialog.setAttribute("open", "");
  }
  elements.exportFormatSelect?.focus();
}

function closeExportPreflight() {
  if (!elements.exportDialog?.open) return;
  if (typeof elements.exportDialog.close === "function") elements.exportDialog.close();
  else elements.exportDialog.removeAttribute("open");
  if (exportReturnFocus?.isConnected) exportReturnFocus.focus();
  exportReturnFocus = null;
}

async function performPreflightExport() {
  const format = elements.exportFormatSelect?.value || "png";
  const meta = EXPORT_FORMATS[format] || EXPORT_FORMATS.png;
  const filename = sanitizeExportFilename(
    elements.exportFilenameInput?.value,
    format,
  );
  const size = Number(elements.exportSizeSelect?.value) || 512;
  const background = elements.exportBackgroundInput?.checked !== false;
  const button = elements.confirmExportButton;
  if (button) {
    button.disabled = true;
    button.textContent = format === "animated-svg" ? "Rendering frames…" : "Preparing…";
  }
  try {
    if (format === "png") {
      await downloadStudioPNG(filename, { background });
    } else if (format === "svg") {
      await engine.downloadSVG(filename, { size, background });
    } else if (format === "animated-svg") {
      await engine.downloadAnimatedSVG(filename, {
        size,
        background,
        duration: 2.4,
        fps: 12,
      });
    } else {
      downloadJSON(filename);
    }
    closeExportPreflight();
    showWorkflowStatus(`Exported ${filename}.`, { tone: "success" });
  } catch (error) {
    showWorkflowStatus(error?.message || `${meta.label} export failed.`, {
      tone: "danger",
    });
  } finally {
    if (button) {
      button.disabled = format === "config" && !builtInSpriteIds.has(engine.currentRecipe.id);
      button.textContent = `Export ${meta.label}`;
    }
  }
}

function simulateAudio() {
  if (audioTimer !== null) {
    window.clearInterval(audioTimer);
    audioTimer = null;
  }
  selectSprite("ai.speaking");
  const started = performance.now();
  audioTimer = window.setInterval(() => {
    const elapsed = (performance.now() - started) / 1000;
    const level =
      (0.2 + 0.8 * Math.abs(Math.sin(elapsed * 8.7) * Math.sin(elapsed * 3.1 + 0.4))) *
      Math.max(0, 1 - elapsed / 2.6);
    engine.setAudioLevel(level);
    if (elapsed > 2.6) {
      window.clearInterval(audioTimer);
      audioTimer = null;
      engine.setAudioLevel(0);
    }
  }, 45);
}

elements.playButton?.addEventListener("click", () => {
  engine.toggle();
  updatePlayButton();
});
elements.autopilotButton?.addEventListener("click", toggleAutopilot);
elements.themeButton?.addEventListener("click", () => setTheme(!isLight));
elements.randomizeButton?.addEventListener("click", randomSeed);
for (const input of elements.paletteInputs) {
  input.addEventListener("input", (event) => {
    const key = event.currentTarget.dataset.paletteColor;
    beginMutation(`palette-${key}`, `change ${key} color`);
    customNonErrorPalette = {
      ...customNonErrorPalette,
      [key]: event.currentTarget.value,
    };
    scheduleStudioPaletteUpdate();
  });
  input.addEventListener("change", () => {
    endMutation(`palette-${input.dataset.paletteColor}`);
    flushStudioPaletteUpdate();
  });
}
elements.glowEnabledInput?.addEventListener("change", (event) => {
  pushUndoSnapshot(event.currentTarget.checked ? "enable glow" : "disable glow");
  const enabled = event.currentTarget.checked;
  const nextPalette = { ...customNonErrorPalette };

  if (enabled) {
    if (hadSavedGlowOverride) nextPalette.glow = savedGlowOverride;
    else delete nextPalette.glow;
    savedGlowOverride = null;
    hadSavedGlowOverride = false;
  } else {
    const currentGlow = nextPalette.glow;
    hadSavedGlowOverride =
      typeof currentGlow === "string" &&
      currentGlow.trim().toLowerCase() !== "transparent";
    savedGlowOverride = hadSavedGlowOverride ? currentGlow : null;
    nextPalette.glow = "transparent";
  }

  customNonErrorPalette = nextPalette;
  applyStudioPalette();
});
elements.resetPaletteButton?.addEventListener("click", () => {
  if (!Object.keys(customNonErrorPalette).length) return;
  pushUndoSnapshot("reset custom colors");
  customNonErrorPalette = {};
  savedGlowOverride = null;
  hadSavedGlowOverride = false;
  applyStudioPalette();
});
elements.copyCodeButton?.addEventListener("click", copyCode);
elements.openExportButton?.addEventListener("click", () => openExportPreflight("png"));
elements.exportConfigButton?.addEventListener("click", () =>
  openExportPreflight("config"),
);
elements.exportPngButton?.addEventListener("click", () => openExportPreflight("png"));
elements.exportSvgButton?.addEventListener("click", () => openExportPreflight("svg"));
elements.exportAnimatedSvgButton?.addEventListener("click", () =>
  openExportPreflight("animated-svg"),
);
elements.undoStudioButton?.addEventListener("click", undoStudioChange);
elements.resetOverridesButton?.addEventListener("click", resetStudioOverrides);
elements.savePresetButton?.addEventListener("click", saveCurrentPreset);
elements.savePresetGalleryButton?.addEventListener("click", saveCurrentPreset);

elements.fieldSelect?.addEventListener("change", (event) => {
  pushUndoSnapshot("change the noise field");
  if (event.target.value === "__recipe__") engine.useRecipeFields();
  else engine.setField(event.target.value);
  updateWorkflowState();
  updateCode();
});
elements.transitionSelect?.addEventListener("change", (event) => {
  pushUndoSnapshot("change the transition");
  if (event.target.value === "__recipe__") {
    transitionOverridden = false;
    transitionOverrideValue = null;
    engine.setOptions({ transition: null });
  } else {
    transitionOverridden = true;
    transitionOverrideValue = event.target.value;
    engine.setOptions({ transition: transitionOverrideValue });
    engine.setSprite(engine.currentRecipe, {
      transition: transitionOverrideValue,
      duration: 0.55,
    });
  }
  updateWorkflowState();
  updateCode();
});
elements.pixelShapeSelect?.addEventListener("change", (event) => {
  pushUndoSnapshot("change pixel geometry");
  pixelShapeOverridden = true;
  engine.setPixelShape(event.target.value);
  updateWorkflowState();
  updateCode();
});
elements.pixelSwitchSelect?.addEventListener("change", (event) => {
  pushUndoSnapshot("change the switch system");
  pixelSwitchOverridden = true;
  engine.setPixelSwitch(event.target.value);
  updateWorkflowState();
  updateCode();
});
elements.orbBoundaryInput?.addEventListener("change", (event) => {
  pushUndoSnapshot(
    event.currentTarget.checked
      ? "use a Gestalt orb boundary"
      : "use a defined orb boundary",
  );
  engine.setOptions({
    orbBoundary: event.currentTarget.checked ? "gestalt" : "defined",
  });
  syncOrbAppearanceToPreviews();
  updateStateUI(engine.currentRecipe);
});
elements.orbBackgroundEnabledInput?.addEventListener("change", (event) => {
  pushUndoSnapshot(
    event.currentTarget.checked
      ? "enable the orb background"
      : "remove the orb background",
  );
  if (event.currentTarget.checked) {
    const selectedColor =
      normalizedOrbBackgroundColor(orbBackgroundColorChoice) ||
      elements.orbBackgroundColorInput?.value ||
      "#262626";
    orbBackgroundColorChoice = selectedColor;
    engine.setOptions({
      orbBackgroundColor: selectedColor,
      orbBackgroundMode: orbBackgroundModeChoice,
    });
  } else {
    const activeColor = normalizedOrbBackgroundColor(
      engine.options.orbBackgroundColor,
    );
    if (activeColor) orbBackgroundColorChoice = activeColor;
    const activeMode = normalizedOrbBackgroundMode(
      engine.options.orbBackgroundMode,
      activeColor,
    );
    if (activeMode !== "none") orbBackgroundModeChoice = activeMode;
    engine.setOptions({ orbBackgroundColor: null, orbBackgroundMode: "none" });
  }
  syncOrbAppearanceToPreviews();
  setOrbBackgroundControlState();
  updateWorkflowState();
  updateCode();
});
elements.orbBackgroundModeSelect?.addEventListener("change", (event) => {
  const mode = normalizedOrbBackgroundMode(event.currentTarget.value);
  if (mode === "none") return;
  pushUndoSnapshot(
    mode === "pixelated"
      ? "use a pixelated orb background"
      : "use a solid orb background",
  );
  orbBackgroundModeChoice = mode;
  engine.setOptions({ orbBackgroundMode: mode });
  syncOrbAppearanceToPreviews();
  setOrbBackgroundControlState();
  updateWorkflowState();
  updateCode();
});
elements.orbBackgroundColorInput?.addEventListener("input", (event) => {
  beginMutation("orb-background-color", "change the orb background color");
  orbBackgroundColorChoice = event.currentTarget.value;
  engine.options.orbBackgroundColor = orbBackgroundColorChoice;
  engine.paint();
  syncOrbAppearanceToPreviews();
  setOrbBackgroundControlState(
    orbBackgroundColorChoice,
    true,
    orbBackgroundModeChoice,
  );
  scheduleStudioMetadataUpdate();
});
elements.orbBackgroundColorInput?.addEventListener("change", () => {
  engine.setOptions({
    orbBackgroundColor: orbBackgroundColorChoice,
    orbBackgroundMode: orbBackgroundModeChoice,
  });
  syncOrbAppearanceToPreviews();
  endMutation("orb-background-color");
  updateWorkflowState();
  updateCode();
});
elements.resolutionRange?.addEventListener("input", (event) => {
  beginMutation("resolution", "change resolution");
  const value = Number(event.target.value);
  if (elements.resolutionValue) elements.resolutionValue.textContent = `${value}×${value}`;
  scheduleResolutionCommit(value);
});
elements.resolutionRange?.addEventListener("change", () => {
  const value = commitPendingResolution();
  endMutation("resolution");
  schedulePreviewResolutionSync(value);
});
elements.speedRange?.addEventListener("input", (event) => {
  beginMutation("speed", "change velocity");
  const value = Number(event.target.value);
  if (elements.speedValue) elements.speedValue.textContent = `${formatNumber(value)}×`;
  engine.setOptions({ speed: value });
  scheduleStudioMetadataUpdate();
});
elements.speedRange?.addEventListener("change", () => endMutation("speed"));
elements.densityRange?.addEventListener("input", (event) => {
  beginMutation("density", "change density");
  const value = Number(event.target.value);
  if (elements.densityValue) elements.densityValue.textContent = formatNumber(value);
  engine.setOptions({ density: value });
  scheduleStudioMetadataUpdate();
});
elements.densityRange?.addEventListener("change", () => endMutation("density"));
elements.seedInput?.addEventListener("change", (event) => {
  pushUndoSnapshot("change the seed");
  engine.setSeed(event.target.value.trim() || "joan-v5");
  elements.seedInput.value = engine.seed;
  updateWorkflowState();
  updateCode();
});
elements.progressRange?.addEventListener("input", (event) => {
  beginMutation("progress", "change semantic progress");
  const value = Number(event.target.value) / 100;
  engine.setProgress(value);
  for (const id of ["ai.progress", "transfer.active"]) {
    const preview = previewById.get(id);
    if (!preview) continue;
    preview.setProgress(value);
    if (!previewShouldAnimate(id)) freezePreview(preview);
  }
  if (elements.progressValue) elements.progressValue.textContent = `${Math.round(value * 100)}%`;
});
elements.progressRange?.addEventListener("change", () => endMutation("progress"));
elements.signalTokenButton?.addEventListener("click", () => {
  selectSprite("ai.generating");
  engine.signal("token", { energy: 0.9 });
});
elements.signalSearchButton?.addEventListener("click", () => {
  selectSprite("ai.searching");
  engine.signal("search.hit", {
    energy: 0.9,
    x: -0.5 + Math.random(),
    y: -0.5 + Math.random(),
  });
});
elements.signalAudioButton?.addEventListener("click", simulateAudio);
elements.previewFitButton?.addEventListener("click", () => applyPreviewMode("fit"));
elements.previewActualButton?.addEventListener("click", () =>
  applyPreviewMode("actual"),
);

elements.stateSearchInput?.addEventListener("input", (event) => {
  buildStatePicker(event.currentTarget.value);
});
elements.stateSearchInput?.addEventListener("keydown", (event) => {
  if (event.key !== "Escape" || !event.currentTarget.value) return;
  event.preventDefault();
  event.currentTarget.value = "";
  buildStatePicker();
});
elements.statePickerSelect?.addEventListener("change", (event) => {
  if (spriteById.has(event.target.value)) selectSprite(event.target.value);
});
elements.recentPresetSelect?.addEventListener("change", (event) => {
  openRecentPreset(event.target.value);
  event.target.value = "";
});
elements.savedPresetGallery?.addEventListener("click", (event) => {
  const deleteButton = event.target.closest("[data-delete-preset]");
  if (deleteButton) {
    deleteRecentPreset(deleteButton.dataset.deletePreset);
    return;
  }
  const openButton = event.target.closest("[data-open-preset]");
  if (openButton) openRecentPreset(openButton.dataset.openPreset);
});
elements.undoDeletePresetButton?.addEventListener("click", undoDeletedPreset);

elements.exportFormatSelect?.addEventListener("change", () =>
  updateExportPreflight({ resetFilename: true }),
);
elements.exportSizeSelect?.addEventListener("change", () => updateExportPreflight());
elements.exportBackgroundInput?.addEventListener("change", () =>
  updateExportPreflight(),
);
elements.exportForm?.addEventListener("submit", (event) => {
  event.preventDefault();
  void performPreflightExport();
});
elements.closeExportButton?.addEventListener("click", closeExportPreflight);
elements.cancelExportButton?.addEventListener("click", closeExportPreflight);
elements.exportDialog?.addEventListener("click", (event) => {
  if (event.target === elements.exportDialog) closeExportPreflight();
});
elements.exportDialog?.addEventListener("close", () => {
  if (exportReturnFocus?.isConnected) exportReturnFocus.focus();
  exportReturnFocus = null;
});

elements.openRecipeButton?.addEventListener("click", () => elements.recipeFile?.click());
elements.recipeFile?.addEventListener("change", async (event) => {
  const [file] = event.target.files || [];
  if (!file) return;
  elements.openRecipeButton.disabled = true;
  elements.openRecipeButton.textContent = "Opening…";
  try {
    await openRecipeFile(file);
  } catch (error) {
    showWorkflowStatus(error?.message || "The recipe could not be opened.", {
      tone: "danger",
    });
  } finally {
    elements.openRecipeButton.disabled = false;
    elements.openRecipeButton.textContent = "Open recipe";
    event.target.value = "";
  }
});

elements.importButton?.addEventListener("click", () => elements.glyphFile?.click());
elements.glyphFile?.addEventListener("change", async (event) => {
  const [file] = event.target.files || [];
  if (!file) return;
  elements.importButton.disabled = true;
  elements.importButton.textContent = "Rasterizing…";
  try {
    pushUndoSnapshot("import artwork");
    await engine.loadGlyphFile(file, {
      id: "custom.product-glyph",
      label: file.name.replace(/\.[^.]+$/, ""),
    });
    spriteById.set(engine.currentRecipe.id, engine.currentRecipe);
    updateStateUI(engine.currentRecipe);
    showWorkflowStatus(`Imported ${file.name}.`, { tone: "success" });
  } catch (error) {
    showWorkflowStatus(error.message, { tone: "danger" });
  } finally {
    elements.importButton.disabled = false;
    elements.importButton.textContent = "Import artwork";
    event.target.value = "";
  }
});

for (const button of document.querySelectorAll("[data-quick-sprite]")) {
  button.addEventListener("click", () => selectSprite(button.dataset.quickSprite));
}

elements.canvas?.addEventListener("dragover", (event) => {
  if (!event.dataTransfer?.types.includes("Files")) return;
  event.preventDefault();
  elements.canvas.closest(".canvas-frame")?.classList.add("is-dragging");
});
elements.canvas?.addEventListener("dragleave", () => {
  elements.canvas.closest(".canvas-frame")?.classList.remove("is-dragging");
});
elements.canvas?.addEventListener("drop", async (event) => {
  event.preventDefault();
  elements.canvas.closest(".canvas-frame")?.classList.remove("is-dragging");
  const [file] = event.dataTransfer?.files || [];
  if (file) {
    pushUndoSnapshot("import artwork");
    await engine.loadGlyphFile(file, {
      id: "custom.product-glyph",
      label: file.name.replace(/\.[^.]+$/, ""),
    });
    spriteById.set(engine.currentRecipe.id, engine.currentRecipe);
    updateStateUI(engine.currentRecipe);
    showWorkflowStatus(`Imported ${file.name}.`, { tone: "success" });
  }
});

engine.addEventListener("stats", (event) => {
  if (elements.fpsValue) elements.fpsValue.textContent = String(event.detail.fps || "—");
  if (elements.activeValue) {
    elements.activeValue.textContent = event.detail.activePixels.toLocaleString();
  }
});
engine.addEventListener("play", updatePlayButton);
engine.addEventListener("pause", () => {
  updatePlayButton();
  freezePreviewIfInactive(selectedSpriteId);
});
engine.addEventListener("timelinecomplete", (event) => {
  const recipe = spriteById.get(event.detail?.to);
  if (recipe) updateStateUI(recipe);
});

document.addEventListener("keydown", (event) => {
  if (event.defaultPrevented || elements.exportDialog?.open) return;
  const tag = event.target?.tagName;
  if (["INPUT", "SELECT", "TEXTAREA", "BUTTON", "CANVAS"].includes(tag)) return;
  if (event.code === "Space") {
    event.preventDefault();
    engine.toggle();
    updatePlayButton();
  } else if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
    const index = Math.max(0, sprites.findIndex((sprite) => sprite.id === selectedSpriteId));
    const direction = event.key === "ArrowRight" ? 1 : -1;
    selectSprite(sprites[(index + direction + sprites.length) % sprites.length].id);
  } else if (event.key.toLowerCase() === "r") {
    randomSeed();
  } else if (event.key.toLowerCase() === "a") {
    toggleAutopilot();
  }
});

reduceMotion.addEventListener("change", () => {
  previewLast = 0;
  for (const preview of previewEngines) {
    preview.reducedMotion = reduceMotion.matches;
  }
  schedulePreviewRefresh();
});

window.addEventListener("beforeunload", () => {
  stopAutopilot();
  cancelPendingResolutionCommit();
  if (audioTimer !== null) window.clearInterval(audioTimer);
  previewsDisposed = true;
  previewWorkQueue.length = 0;
  previewCardObserver?.disconnect();
  savedPresetPreviewObserver?.disconnect();
  heroEngine?.destroy();
  engine.destroy();
  for (const preview of previewEngines) preview.destroy();
});
