(() => {
  const storageKey = "plotline.theme";
  const choices = document.querySelectorAll("[data-theme-choice]");

  const readTheme = () => {
    try {
      return localStorage.getItem(storageKey);
    } catch {
      return null;
    }
  };

  const saveTheme = (theme) => {
    try {
      localStorage.setItem(storageKey, theme);
    } catch {
      // Theme still applies for the current page if persistence is unavailable.
    }
  };

  const applyTheme = (theme) => {
    const nextTheme = theme === "dark" ? "dark" : "light";
    document.documentElement.dataset.theme = nextTheme;
    document.documentElement.dataset.bsTheme = nextTheme;
    saveTheme(nextTheme);
    choices.forEach((button) => {
      const isActive = button.dataset.themeChoice === nextTheme;
      button.classList.toggle("active", isActive);
      button.setAttribute("aria-pressed", String(isActive));
    });
  };

  choices.forEach((button) => {
    button.addEventListener("click", () => applyTheme(button.dataset.themeChoice));
  });

  applyTheme(readTheme() || "light");
})();

(() => {
  const banner = document.querySelector("[data-trial-banner]");
  if (!banner) {
    return;
  }

  const key = `plotline.trialBanner.dismissed.${banner.dataset.trialBannerKey || "current"}`;
  try {
    if (localStorage.getItem(key) === "true") {
      banner.hidden = true;
      return;
    }
  } catch {
    // The banner can still be dismissed for this page view if storage is unavailable.
  }

  banner.querySelector("[data-trial-banner-dismiss]")?.addEventListener("click", () => {
    banner.hidden = true;
    try {
      localStorage.setItem(key, "true");
    } catch {
      // Dismissal persistence is a convenience only.
    }
  });
})();

(() => {
  const toggle = document.querySelector("[data-word-companion-follow-toggle]");
  if (!toggle || !window.signalR) {
    return;
  }

  const storageKey = "plotdirector.followWordCompanion";
  const unsafePathPrefixes = [
    "/pricing",
    "/about",
    "/contact",
    "/privacy",
    "/terms",
    "/admin",
    "/billing",
    "/account",
    "/login",
    "/register",
    "/feature-requests/admin",
    "/featurerequests/admin",
    "/manageaccount"
  ];
  let lastSceneId = null;
  let lastCharactersProjectId = null;

  const readEnabled = () => {
    try {
      return localStorage.getItem(storageKey) === "true";
    } catch {
      return false;
    }
  };

  const writeEnabled = (value) => {
    try {
      localStorage.setItem(storageKey, value ? "true" : "false");
    } catch {
      // Follow mode is optional, so storage failures should not interrupt the app.
    }
  };

  const applyToggle = (enabled) => {
    toggle.setAttribute("aria-pressed", String(enabled));
    toggle.classList.toggle("active", enabled);
    toggle.title = enabled
      ? "PlotDirector will follow the current Word Companion scene."
      : "Enable this to let PlotDirector follow the current Word Companion scene.";
  };

  const isSafeCurrentPage = () => {
    const path = window.location.pathname.toLowerCase();
    if (path === "/" || path === "") {
      return false;
    }

    return !unsafePathPrefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
  };

  const safeRelativeUrl = (url, expectedPrefix) => {
    if (typeof url !== "string" || !url.startsWith(expectedPrefix)) {
      return "";
    }

    try {
      const parsed = new URL(url, window.location.origin);
      return parsed.origin === window.location.origin ? `${parsed.pathname}${parsed.search}${parsed.hash}` : "";
    } catch {
      return "";
    }
  };

  const navigateIfAllowed = (url) => {
    if (!readEnabled() || !isSafeCurrentPage() || !url) {
      return;
    }

    const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
    if (current === url) {
      return;
    }

    window.location.assign(url);
  };

  applyToggle(readEnabled());
  toggle.addEventListener("click", () => {
    const enabled = !readEnabled();
    writeEnabled(enabled);
    applyToggle(enabled);
  });

  const connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/word-companion-follow")
    .withAutomaticReconnect()
    .build();

  connection.on("wordCompanionSceneChanged", (payload) => {
    const sceneId = Number.parseInt(payload?.sceneId || payload?.SceneId || "", 10);
    if (!Number.isInteger(sceneId) || sceneId <= 0 || sceneId === lastSceneId) {
      return;
    }

    lastSceneId = sceneId;
    const sceneUrl = safeRelativeUrl(payload?.sceneUrl || payload?.SceneUrl || "", "/Scenes/Edit/");
    navigateIfAllowed(sceneUrl);
  });

  connection.on("wordCompanionCharactersCreated", (payload) => {
    const projectId = Number.parseInt(payload?.projectId || payload?.ProjectId || "", 10);
    if (!Number.isInteger(projectId) || projectId <= 0 || projectId === lastCharactersProjectId) {
      return;
    }

    lastCharactersProjectId = projectId;
    const charactersUrl = safeRelativeUrl(payload?.charactersUrl || payload?.CharactersUrl || "", "/Characters");
    navigateIfAllowed(charactersUrl);
  });

  connection.start().catch((error) => {
    console.debug("Word Companion follow connection unavailable.", error);
  });
})();

(() => {
  const modalElement = document.getElementById("plotlineConfirmModal");
  const modal = modalElement && window.bootstrap ? new bootstrap.Modal(modalElement) : null;
  const modalTitle = modalElement?.querySelector("#plotlineConfirmModalTitle");
  const modalMessage = modalElement?.querySelector("#plotlineConfirmModalMessage");
  const modalSubmit = modalElement?.querySelector("[data-confirm-submit]");
  const deleteField = modalElement?.querySelector("[data-delete-confirm-field]");
  const deleteInput = modalElement?.querySelector("[data-delete-confirm-input]");
  let pendingForm = null;
  let pendingSubmitter = null;

  const splitMessage = (message) => {
    const parts = message.split(/\n\s*\n/);
    return {
      title: parts.shift()?.trim() || "Confirm action",
      body: parts.join("\n\n").trim()
    };
  };

  const resetModal = () => {
    pendingForm = null;
    pendingSubmitter = null;
    if (deleteInput) {
      deleteInput.value = "";
    }
    deleteField?.classList.add("d-none");
    modalSubmit?.removeAttribute("disabled");
  };

  modalElement?.addEventListener("hidden.bs.modal", resetModal);

  deleteInput?.addEventListener("input", () => {
    if (pendingForm?.dataset.requireDeleteText === "true" && modalSubmit) {
      modalSubmit.disabled = deleteInput.value.trim() !== "DELETE";
    }
  });

  modalSubmit?.addEventListener("click", () => {
    if (!pendingForm) {
      return;
    }

    if (pendingForm.dataset.requireDeleteText === "true") {
      const target = pendingForm.querySelector("[data-delete-confirm-target], [name='confirmationText']");
      if (target) {
        target.value = deleteInput?.value.trim() || "";
      }
    }

    pendingForm.dataset.confirmed = "true";
    modal?.hide();
    if (pendingForm.requestSubmit && pendingSubmitter) {
      pendingForm.requestSubmit(pendingSubmitter);
    } else if (pendingForm.requestSubmit) {
      pendingForm.requestSubmit();
    } else {
      pendingForm.submit();
    }
  });

  document.addEventListener("submit", (event) => {
    const form = event.target;
    if (
      !(form instanceof HTMLFormElement)
      || form.dataset.confirmed === "true"
      || form.dataset.confirmSkip === "true"
      || form.dataset.noDeleteConfirm === "true"
    ) {
      return;
    }

    const explicitMessage = form.dataset.confirmMessage;
    const submitter = event.submitter instanceof HTMLElement ? event.submitter : form.querySelector("button[type='submit']");
    const buttonText = submitter?.textContent || "";
    const actionPath = form.getAttribute("action") || "";
    const restoreAction = /restore/i.test(buttonText) || /\/restore\b/i.test(actionPath);
    const deleteForeverAction = /delete forever/i.test(buttonText) || /\/deleteforever\b/i.test(actionPath);
    const archiveAction = !restoreAction && !deleteForeverAction && (/archive/i.test(buttonText) || /\/archive\b/i.test(actionPath));
    const removeAction = !deleteForeverAction && /(delete|remove)/i.test(buttonText);
    if (!explicitMessage && !archiveAction && !restoreAction && !removeAction && !deleteForeverAction) {
      return;
    }

    const message = explicitMessage
      || (archiveAction
        ? "Archive this item?\n\nThis will hide it from active lists and timelines, but the data will be kept and can be restored later."
        : restoreAction
          ? "Restore this item?\n\nThis will return it to active lists and timelines where applicable."
          : deleteForeverAction
            ? "Permanently delete this item?\n\nThis cannot be undone. Any linked planning data may also be removed or become unavailable. Only continue if you are certain."
            : "Remove this item?\n\nThis removes the link or supporting record. Major story items are archived instead of permanently deleted.");

    if (!modal || !modalElement || !modalTitle || !modalMessage || !modalSubmit) {
      return;
    }

    event.preventDefault();
    const content = splitMessage(message);
    pendingForm = form;
    pendingSubmitter = submitter;
    modalTitle.textContent = form.dataset.confirmTitle || content.title;
    modalMessage.textContent = form.dataset.confirmBody || content.body;
    modalSubmit.textContent = form.dataset.confirmButtonText || buttonText.trim() || "Confirm";
    modalSubmit.className = `btn ${form.dataset.confirmButtonClass || (restoreAction ? "btn-primary" : "btn-danger")}`;

    const needsDeleteText = form.dataset.requireDeleteText === "true" || deleteForeverAction;
    deleteField?.classList.toggle("d-none", !needsDeleteText);
    if (needsDeleteText && modalSubmit) {
      modalSubmit.disabled = true;
      deleteInput?.focus();
    }

    modal.show();
    if (needsDeleteText) {
      window.setTimeout(() => deleteInput?.focus(), 200);
    }
  });
})();

(() => {
  if (!window.bootstrap) {
    return;
  }

  const popovers = new Map();
  let activeIcon = null;
  const initializedIcons = new WeakSet();

  const tooltipElement = (icon) => icon.querySelector("[data-help-tooltip]") || icon;
  const hideTooltip = (icon) => bootstrap.Tooltip.getInstance(tooltipElement(icon))?.hide();
  const hideActivePopover = (exceptIcon = null) => {
    if (activeIcon && activeIcon !== exceptIcon) {
      bootstrap.Popover.getInstance(activeIcon)?.hide();
    }
  };
  const allHelpIcons = () => document.querySelectorAll("[data-help-icon]");
  const hideAllHelpTooltips = () => allHelpIcons().forEach((icon) => hideTooltip(icon));
  const helpIconsIn = (scope) => {
    if (!scope) {
      return [];
    }
    const icons = [];
    if (scope.matches?.("[data-help-icon]")) {
      icons.push(scope);
    }
    scope.querySelectorAll?.("[data-help-icon]").forEach((icon) => icons.push(icon));
    return icons;
  };

  const initHelpIcons = (scope = document) => {
    helpIconsIn(scope).forEach((icon) => {
      if (initializedIcons.has(icon)) {
        return;
      }
      initializedIcons.add(icon);

      const tooltipTarget = tooltipElement(icon);
      const micro = tooltipTarget.dataset.helpMicro || "Help article not found.";
      const title = icon.dataset.helpTitle || "Help unavailable";
      const summary = icon.dataset.helpSummary || "Help content has not yet been written for this feature.";

      bootstrap.Tooltip.getOrCreateInstance(tooltipTarget, {
        title: micro,
        trigger: "hover focus",
        placement: "top",
        container: "body"
      });

      const popover = bootstrap.Popover.getOrCreateInstance(icon, {
        title,
        trigger: "click",
        placement: "auto",
        container: "body",
        html: true,
        customClass: "plotline-help-popover",
        content() {
          const wrapper = document.createElement("div");
          const summaryText = document.createElement("p");
          summaryText.className = "mb-2";
          summaryText.textContent = summary;

          const moreLink = document.createElement("a");
          moreLink.href = icon.dataset.helpUrl || "#";
          moreLink.textContent = "More...";
          moreLink.className = "plotline-help-more";
          moreLink.addEventListener("click", (event) => {
            event.preventDefault();
            hideActivePopover();
            hideAllHelpTooltips();
            icon.dispatchEvent(new CustomEvent("plotline:help-more", {
              bubbles: true,
              detail: { key: icon.dataset.helpKey || "" }
            }));
          });

          wrapper.append(summaryText, moreLink);
          return wrapper;
        }
      });
      popovers.set(icon, popover);

      icon.addEventListener("click", (event) => {
        if (!window.PlotLineHelpDrawer?.isActive?.()) {
          return;
        }

        event.preventDefault();
        event.stopPropagation();
        hideActivePopover();
        hideTooltip(icon);
        window.PlotLineHelpPopovers?.hideAll();
        window.PlotLineHelpDrawer.open(icon.dataset.helpKey || "");
      });
      icon.addEventListener("show.bs.popover", (event) => {
        if (window.PlotLineHelpDrawer?.isActive?.()) {
          event.preventDefault();
          return;
        }

        hideActivePopover(icon);
        hideTooltip(icon);
      });
      icon.addEventListener("shown.bs.popover", () => {
        activeIcon = icon;
        hideTooltip(icon);
      });
      icon.addEventListener("hidden.bs.popover", () => {
        if (activeIcon === icon) {
          activeIcon = null;
        }
      });
    });
  };

  document.addEventListener("click", (event) => {
    const target = event.target instanceof Element ? event.target : null;
    if (!target || !activeIcon) {
      return;
    }

    if (target.closest("[data-help-icon]")) {
      return;
    }

    const activePopoverElement = document.querySelector(`#${activeIcon.getAttribute("aria-describedby")}`);
    if (activePopoverElement?.contains(target)) {
      return;
    }

    hideActivePopover();
  });

  document.addEventListener("keydown", (event) => {
    if (event.key === "Escape") {
      hideActivePopover();
    }
  });

  window.PlotLineHelpPopovers = {
    hideAll() {
      popovers.forEach((popover) => popover.hide());
      hideAllHelpTooltips();
      activeIcon = null;
    }
  };
  window.PlotLineHelpIcons = {
    init: initHelpIcons
  };
  initHelpIcons();
})();

(() => {
  const layout = document.querySelector("[data-help-layout]");
  const drawer = document.querySelector("[data-help-drawer]");
  const content = document.querySelector("[data-help-content]");
  const splitter = document.querySelector("[data-help-splitter]");
  const closeButton = document.querySelector("[data-help-close]");
  const collapseButton = document.querySelector("[data-help-collapse]");
  const expandStrip = document.querySelector("[data-help-expand]");
  const pinButton = document.querySelector("[data-help-pin]");
  const heading = document.querySelector("[data-help-drawer-heading]");
  if (!layout || !drawer || !content) {
    return;
  }

  const storage = {
    width: "plotline.helpDrawer.width",
    pinned: "plotline.helpDrawer.pinned",
    key: "plotline.helpDrawer.key",
    collapsed: "plotline.helpDrawer.collapsed"
  };

  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));

  const readBool = (key) => {
    try {
      return localStorage.getItem(key) === "true";
    } catch {
      return false;
    }
  };

  const writeValue = (key, value) => {
    try {
      localStorage.setItem(key, value);
    } catch {
      // Help drawer preferences are conveniences; blocked storage should not break help.
    }
  };

  const setWidth = (value) => {
    const width = `${clamp(Number.parseFloat(value) || 25, 20, 50)}%`;
    layout.style.setProperty("--help-drawer-width", width);
    writeValue(storage.width, width);
  };

  const setPinned = (isPinned) => {
    writeValue(storage.pinned, String(isPinned));
    pinButton?.setAttribute("aria-pressed", String(isPinned));
    if (pinButton) {
      pinButton.textContent = isPinned ? "Unpin" : "Pin";
    }
  };

  const setCollapsed = (isCollapsed) => {
    layout.classList.toggle("help-collapsed", isCollapsed);
    writeValue(storage.collapsed, String(isCollapsed));
    if (collapseButton) {
      collapseButton.textContent = isCollapsed ? "Expand" : "Collapse";
    }
  };

  const openDrawer = async (key, options = {}) => {
    const contextKey = key || "";
    layout.classList.add("help-open");
    if (!options.keepCollapsed) {
      setCollapsed(false);
    }
    writeValue(storage.key, contextKey);
    if (heading) {
      heading.textContent = contextKey || "Help";
    }
    content.innerHTML = "<p class=\"muted\">Loading help...</p>";

    try {
      const response = await fetch(`/HelpDrawer/Article?key=${encodeURIComponent(contextKey)}`, {
        headers: { "X-Requested-With": "XMLHttpRequest" }
      });
      content.innerHTML = response.ok
        ? await response.text()
        : "<p class=\"muted\">Help for this feature has not yet been written.</p>";
      const title = content.querySelector(".help-drawer-article h2")?.textContent?.trim();
      if (heading && title) {
        heading.textContent = title;
      }
    } catch {
      content.innerHTML = "<p class=\"muted\">Help for this feature has not yet been written.</p>";
    }
  };

  const closeDrawer = () => {
    layout.classList.remove("help-open", "help-collapsed");
    setPinned(false);
  };

  window.PlotLineHelpDrawer = {
    isActive() {
      return layout.classList.contains("help-open");
    },
    open(key) {
      return openDrawer(key);
    }
  };

  setWidth(localStorage.getItem(storage.width) || "25%");
  setPinned(readBool(storage.pinned));
  setCollapsed(readBool(storage.collapsed));

  document.addEventListener("plotline:help-more", (event) => {
    const key = event.detail?.key || event.target?.dataset?.helpKey || "";
    window.PlotLineHelpPopovers?.hideAll();
    openDrawer(key);
  });

  document.addEventListener("click", (event) => {
    const opener = event.target instanceof Element ? event.target.closest("[data-help-open]") : null;
    if (!opener) {
      return;
    }

    event.preventDefault();
    window.PlotLineHelpPopovers?.hideAll();
    openDrawer(opener.dataset.helpOpen || "");
  });

  closeButton?.addEventListener("click", closeDrawer);
  collapseButton?.addEventListener("click", () => setCollapsed(!layout.classList.contains("help-collapsed")));
  expandStrip?.addEventListener("click", () => setCollapsed(false));
  expandStrip?.addEventListener("keydown", (event) => {
    if (event.key === "Enter" || event.key === " ") {
      event.preventDefault();
      setCollapsed(false);
    }
  });
  pinButton?.addEventListener("click", () => setPinned(!readBool(storage.pinned)));

  splitter?.addEventListener("pointerdown", (event) => {
    if (!layout.classList.contains("help-open") || layout.classList.contains("help-collapsed")) {
      return;
    }

    event.preventDefault();
    document.body.classList.add("help-resizing");
    splitter.setPointerCapture?.(event.pointerId);

    const onMove = (moveEvent) => {
      const rect = layout.getBoundingClientRect();
      const widthPercent = ((rect.right - moveEvent.clientX) / rect.width) * 100;
      setWidth(widthPercent);
    };

    const onUp = () => {
      document.body.classList.remove("help-resizing");
      document.removeEventListener("pointermove", onMove);
      document.removeEventListener("pointerup", onUp);
    };

    document.addEventListener("pointermove", onMove);
    document.addEventListener("pointerup", onUp);
  });

  const pinnedKey = (() => {
    try {
      return localStorage.getItem(storage.key) || "";
    } catch {
      return "";
    }
  })();
  if (readBool(storage.pinned) && pinnedKey) {
    openDrawer(pinnedKey, { keepCollapsed: true });
  }
})();

(() => {
  const chartRoots = document.querySelectorAll("[data-metric-shape-chart]");
  if (!chartRoots.length) {
    return;
  }

  const colours = [
    "#2f6f63",
    "#b35f3b",
    "#6750a4",
    "#c58b2b",
    "#7d3f52",
    "#33759b",
    "#9c4d8f",
    "#a33d3d",
    "#6f8f3d",
    "#d08a2f",
    "#444b57"
  ];

  const globalDefaultMetrics = new Set(["Overall Intensity", "Tension", "Emotional Weight", "Action"]);
  const metricCharts = [];
  const maxMetricCanvasDimension = 30000;
  const syncMetricCanvasSize = (root) => {
    const frame = root.querySelector(".metric-chart-frame");
    if (!frame) {
      return;
    }

    let naturalWidth = Number(frame.dataset.metricNaturalWidth || 0);
    if (!naturalWidth || frame.dataset.metricCanvasConstrained !== "true") {
      frame.style.width = "";
      naturalWidth = frame.getBoundingClientRect().width;
      if (naturalWidth) {
        frame.dataset.metricNaturalWidth = String(naturalWidth);
      }
    }

    if (!naturalWidth) {
      return;
    }

    const devicePixelRatio = Math.max(window.devicePixelRatio || 1, 1);
    const maxCssWidth = Math.floor(maxMetricCanvasDimension / devicePixelRatio);
    if (naturalWidth > maxCssWidth) {
      frame.style.width = `${maxCssWidth}px`;
      frame.dataset.metricCanvasConstrained = "true";
    } else {
      frame.style.width = "";
      frame.dataset.metricCanvasConstrained = "false";
    }
  };
  const resizeMetricCharts = () => {
    window.requestAnimationFrame(() => {
      metricCharts.forEach(({ chart, root }) => {
        syncMetricCanvasSize(root);
        chart.resize();
      });
    });
  };
  window.PlotLineMetricCharts = { resizeAll: resizeMetricCharts };
  window.addEventListener("plotline:timeline-layout-changed", resizeMetricCharts);

  const chapterGuidePlugin = {
    id: "plotlineChapterGuides",
    afterDraw(chart) {
      const scenes = chart.config.options.plugins.plotlineScenes || [];
      const { ctx, chartArea, scales } = chart;
      if (!scenes.length || !scales.x) {
        return;
      }

      ctx.save();
      ctx.strokeStyle = "rgba(47, 111, 99, 0.16)";
      ctx.fillStyle = "rgba(82, 75, 65, 0.58)";
      ctx.lineWidth = 1;
      ctx.font = "700 10px system-ui, -apple-system, Segoe UI, sans-serif";

      const chapterStarts = [];
      scenes.forEach((scene, index) => {
        if (!index || scene.chapterLabel !== scenes[index - 1].chapterLabel) {
          chapterStarts.push({ scene, index });
        }
      });
      const realChapterStarts = chapterStarts.filter((entry) => !entry.scene.isPlaceholder);
      const labelInterval = realChapterStarts.length > 40 ? 5 : realChapterStarts.length > 24 ? 4 : realChapterStarts.length > 14 ? 2 : 1;

      realChapterStarts.forEach((entry, chapterIndex) => {
        const { scene, index } = entry;
        const x = scales.x.getPixelForValue(index + 0.5);
        ctx.beginPath();
        ctx.moveTo(x, chartArea.top);
        ctx.lineTo(x, chartArea.bottom);
        ctx.stroke();

        const chapterNumber = Number.parseInt(String(scene.chapterLabel).replace(/[^0-9]/g, ""), 10);
        const shouldLabel = chapterIndex === 0 ||
          chapterIndex === realChapterStarts.length - 1 ||
          labelInterval === 1 ||
          (Number.isFinite(chapterNumber) && chapterNumber % labelInterval === 0);
        if (shouldLabel) {
          ctx.fillText(scene.chapterLabel, Math.min(x + 4, chartArea.right - 70), chartArea.bottom + 22);
        }
      });

      ctx.restore();
    }
  };

  if (window.Chart) {
    Chart.register(chapterGuidePlugin);
  }

  chartRoots.forEach((root) => {
    const payloadScript = root.querySelector("script[type='application/json']");
    const canvas = root.querySelector("canvas");
    const toggleHost = root.querySelector("[data-metric-toggles]");
    const fallback = root.querySelector("[data-chart-fallback]");
    const editToolbar = root.querySelector("[data-timeline-metric-edit-toolbar]");
    const editStatus = root.querySelector("[data-edit-metric-status]");
    const canEditTimelineMetric = root.hasAttribute("data-timeline-metric-editor") && editToolbar && editStatus;
    if (!payloadScript || !canvas) {
      return;
    }

    let payload;
    try {
      payload = JSON.parse(payloadScript.textContent || "{}");
    } catch {
      fallback?.classList.remove("d-none");
      return;
    }

    if (!window.Chart || !payload.scenes?.length || !payload.metrics?.length) {
      fallback?.classList.remove("d-none");
      return;
    }

    const defaultMetrics = new Set(payload.defaultMetrics?.length ? payload.defaultMetrics : globalDefaultMetrics);
    const columnCount = payload.scenes.length;
    const tickLabelInterval = columnCount > 80 ? 10 : columnCount > 40 ? 5 : columnCount > 24 ? 4 : columnCount > 14 ? 2 : 1;
    const datasets = payload.metrics.map((metric, metricIndex) => {
      const pointMap = new Map(metric.points.map((point) => [point.sceneId, point.value]));
      const colour = colours[metricIndex % colours.length];
      return {
        label: metric.metricName,
        metricTypeId: metric.metricTypeId,
        minValue: Number.isFinite(metric.minValue) ? metric.minValue : 1,
        maxValue: Number.isFinite(metric.maxValue) ? metric.maxValue : 10,
        data: payload.scenes.map((scene, columnIndex) => {
          const value = pointMap.get(scene.sceneId);
          return value == null
            ? { x: columnIndex + 0.5, y: null, sceneId: scene.sceneId }
            : { x: columnIndex + 0.5, y: value, sceneId: scene.sceneId };
        }),
        borderColor: colour,
        backgroundColor: colour,
        pointBackgroundColor: colour,
        pointBorderColor: "#fff",
        pointBorderWidth: 1.5,
        pointRadius: 3,
        pointHoverRadius: 5,
        borderWidth: 2,
        tension: 0.35,
        spanGaps: true,
        hidden: !defaultMetrics.has(metric.metricName)
      };
    });

    let activeMetricDrag = null;
    let suppressMetricClick = false;
    syncMetricCanvasSize(root);

    const setMetricEditStatus = (message) => {
      if (editStatus) {
        editStatus.textContent = message;
      }
    };

    const getVisibleDatasetIndexes = () => datasets
      .map((_, index) => index)
      .filter((index) => chart?.isDatasetVisible(index));

    const getEditableDatasetIndex = () => {
      const visibleIndexes = getVisibleDatasetIndexes();
      return visibleIndexes.length === 1 ? visibleIndexes[0] : -1;
    };

    const updateMetricEditUi = () => {
      if (!canEditTimelineMetric) {
        return;
      }

      const editableIndex = getEditableDatasetIndex();
      datasets.forEach((dataset, index) => {
        dataset.pointRadius = index === editableIndex ? 5 : 3;
        dataset.pointHoverRadius = index === editableIndex ? 7 : 5;
        dataset.borderWidth = index === editableIndex ? 3 : 2;
      });
      const visibleIndexes = getVisibleDatasetIndexes();
      const canDrag = editableIndex >= 0;
      canvas.classList.toggle("metric-curve-editing", canDrag);
      editStatus.classList.toggle("d-none", visibleIndexes.length === 0);
      if (visibleIndexes.length === 0) {
        setMetricEditStatus("");
      } else if (canDrag) {
        setMetricEditStatus("Drag points up or down to edit this metric.");
      } else {
        setMetricEditStatus("Show only one metric line to edit its curve.");
      }
      chart?.update();
    };

    toggleHost.innerHTML = "";
    datasets.forEach((dataset, index) => {
      const id = `metric-toggle-${Math.random().toString(36).slice(2)}-${index}`;
      const label = document.createElement("label");
      label.className = "metric-toggle-pill";
      label.style.setProperty("--metric-colour", dataset.borderColor);
      label.innerHTML = `<input type="checkbox" id="${id}" ${dataset.hidden ? "" : "checked"}><span>${dataset.label}</span>`;
      toggleHost.appendChild(label);
      label.querySelector("input").addEventListener("change", (event) => {
        chart.setDatasetVisibility(index, event.target.checked);
        updateMetricEditUi();
        chart.update();
      });
    });

    const chart = new Chart(canvas, {
      type: "line",
      data: { datasets },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        interaction: { mode: "nearest", intersect: false },
        scales: {
          y: {
            min: 1,
            max: 10,
            ticks: { stepSize: 1 },
            grid: { color: "rgba(82, 75, 65, 0.10)" }
          },
          x: {
            type: "linear",
            min: 0,
            max: columnCount,
            bounds: "ticks",
            ticks: {
              maxRotation: 0,
              autoSkip: false,
              maxTicksLimit: 16,
              callback(value) {
                const columnIndex = Math.floor(Number(value));
                if (columnIndex < 0 || columnIndex >= columnCount) {
                  return "";
                }

                const scene = payload.scenes[columnIndex];
                if (columnIndex !== 0 && columnIndex !== columnCount - 1 && columnIndex % tickLabelInterval !== 0) {
                  return "";
                }
                return scene?.isPlaceholder ? "" : scene?.axisLabel || "";
              }
            },
            afterBuildTicks(scale) {
              scale.ticks = payload.scenes.map((_, columnIndex) => ({ value: columnIndex + 0.5 }));
            },
            grid: { display: false }
          }
        },
        plugins: {
          legend: { display: false },
          plotlineScenes: payload.scenes,
          tooltip: {
            callbacks: {
              title(items) {
                const columnIndex = Math.floor(items[0].parsed.x);
                const scene = payload.scenes[columnIndex];
                if (scene.isPlaceholder) {
                  return scene.sceneTitle || "No scene";
                }
                return `${scene.chapterLabel}, Scene ${scene.sceneNumber}: ${scene.sceneTitle}`;
              },
              label(item) {
                return `${item.dataset.label}: ${item.formattedValue}`;
              },
              afterBody(items) {
                const columnIndex = Math.floor(items[0].parsed.x);
                const scene = payload.scenes[columnIndex];
                const lines = [];
                if (scene.characterPresence) {
                  lines.push(scene.characterPresence);
                } else if (scene.characterAppears !== undefined) {
                  lines.push(scene.characterAppears ? "Character appears" : "Character does not appear");
                }

                if (scene.bookTitle) {
                  lines.push(scene.bookTitle);
                }

                return lines;
              }
            }
          }
        }
      }
    });

    const getMetricDragPoint = (event) => {
      if (!canEditTimelineMetric) {
        return null;
      }

      const editableIndex = getEditableDatasetIndex();
      if (editableIndex < 0) {
        return null;
      }

      const points = chart.getElementsAtEventForMode(event, "nearest", { intersect: true }, true);
      const match = points.find((point) => point.datasetIndex === editableIndex);
      if (!match) {
        return null;
      }

      const dataset = chart.data.datasets[match.datasetIndex];
      const point = dataset?.data?.[match.index];
      if (!point?.sceneId || point.y == null) {
        return null;
      }

      return {
        datasetIndex: match.datasetIndex,
        pointIndex: match.index,
        previousValue: point.y
      };
    };

    const dragValueFromEvent = (event, dataset) => {
      const rawValue = chart.scales.y.getValueForPixel(event.offsetY);
      const roundedValue = Math.round(rawValue);
      return Math.max(dataset.minValue, Math.min(dataset.maxValue, roundedValue));
    };

    const saveMetricDrag = async (dragState) => {
      const dataset = chart.data.datasets[dragState.datasetIndex];
      const point = dataset?.data?.[dragState.pointIndex];
      if (!dataset || !point?.sceneId) {
        return;
      }

      const token = editToolbar?.querySelector("input[name='__RequestVerificationToken']")?.value || "";
      setMetricEditStatus("Saving...");
      try {
        const response = await fetch(root.dataset.updateUrl || "/SceneMetrics/UpdateSceneMetricValue", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "RequestVerificationToken": token
          },
          body: JSON.stringify({
            sceneId: Number(point.sceneId),
            metricId: Number(dataset.metricTypeId),
            value: Number(point.y)
          })
        });

        const result = await response.json().catch(() => ({}));
        if (!response.ok || !result.success) {
          throw new Error(result.message || "Save failed");
        }

        point.y = result.value;
        setMetricEditStatus("Saved");
        chart.update();
      } catch {
        point.y = dragState.previousValue;
        setMetricEditStatus("Save failed");
        chart.update();
      }
    };

    if (canEditTimelineMetric) {
      updateMetricEditUi();

      canvas.addEventListener("pointerdown", (event) => {
        activeMetricDrag = getMetricDragPoint(event);
        if (!activeMetricDrag) {
          return;
        }

        event.preventDefault();
        canvas.setPointerCapture?.(event.pointerId);
        suppressMetricClick = true;
      });

      canvas.addEventListener("pointermove", (event) => {
        if (!activeMetricDrag) {
          return;
        }

        const dataset = chart.data.datasets[activeMetricDrag.datasetIndex];
        const point = dataset?.data?.[activeMetricDrag.pointIndex];
        if (!dataset || !point) {
          return;
        }

        point.y = dragValueFromEvent(event, dataset);
        chart.update("none");
      });

      canvas.addEventListener("pointerup", async (event) => {
        if (!activeMetricDrag) {
          return;
        }

        const dragState = activeMetricDrag;
        activeMetricDrag = null;
        canvas.releasePointerCapture?.(event.pointerId);
        await saveMetricDrag(dragState);
        window.setTimeout(() => {
          suppressMetricClick = false;
        }, 0);
      });

      canvas.addEventListener("pointercancel", (event) => {
        if (!activeMetricDrag) {
          return;
        }

        const dataset = chart.data.datasets[activeMetricDrag.datasetIndex];
        const point = dataset?.data?.[activeMetricDrag.pointIndex];
        if (point) {
          point.y = activeMetricDrag.previousValue;
        }

        activeMetricDrag = null;
        canvas.releasePointerCapture?.(event.pointerId);
        suppressMetricClick = false;
        chart.update();
      });
    }

    canvas.addEventListener("click", (event) => {
      if (suppressMetricClick) {
        event.preventDefault();
        return;
      }

      const points = chart.getElementsAtEventForMode(event, "nearest", { intersect: true }, true);
      if (!points.length) {
        return;
      }

      const point = chart.data.datasets[points[0].datasetIndex]?.data?.[points[0].index];
      const columnIndex = point ? Math.floor(point.x) : points[0].index;
      const scene = payload.scenes[columnIndex];
      if (scene?.timelineUrl) {
        window.PlotLineTimelineScroll?.save();
        window.location.href = scene.timelineUrl;
      }
    });

    metricCharts.push({ chart, root });
    const resizeObserver = window.ResizeObserver
      ? new ResizeObserver(() => {
          syncMetricCanvasSize(root);
          chart.resize();
        })
      : null;
    if (resizeObserver) {
      resizeObserver.observe(root);
      resizeObserver.observe(root.querySelector(".metric-chart-frame") || root);
    }
  });
})();

(() => {
  const root = document.querySelector("[data-timeline-root]");
  const controls = document.querySelector("[data-timeline-controls]");
  if (!root || !controls) {
    return;
  }

  const storageKey = "plotline.timeline.visibility";
  const defaultState = {
    "scene-cards": true,
    "plot-lines": true,
    assets: true,
    characters: true,
    locations: true,
    warnings: true,
    metrics: true
  };

  const readState = () => {
    try {
      return { ...defaultState, ...JSON.parse(localStorage.getItem(storageKey) || "{}") };
    } catch {
      return { ...defaultState };
    }
  };

  const applyState = (state) => {
    Object.entries(state).forEach(([key, visible]) => {
      root.classList.toggle(`hide-${key}`, !visible);
      document.querySelectorAll(`[data-timeline-section='${key}']`).forEach((section) => {
        section.classList.toggle("timeline-section-hidden", !visible);
      });
    });

    document.querySelectorAll("[data-location-marker]").forEach((marker) => {
      marker.classList.toggle("timeline-section-hidden", !state.locations);
    });
    document.querySelectorAll("[data-warning-marker]").forEach((marker) => {
      marker.classList.toggle("timeline-section-hidden", !state.warnings);
    });

    document.querySelector("[data-visibility-json]")?.setAttribute("value", JSON.stringify(state));
  };

  const state = readState();
  controls.querySelectorAll("[data-timeline-toggle]").forEach((toggle) => {
    const key = toggle.dataset.timelineToggle;
    toggle.checked = state[key] ?? true;
    toggle.addEventListener("change", () => {
      state[key] = toggle.checked;
      localStorage.setItem(storageKey, JSON.stringify(state));
      applyState(state);
    });
  });

  controls.querySelector("[data-timeline-preset-form]")?.addEventListener("submit", () => {
    document.querySelector("[data-visibility-json]")?.setAttribute("value", JSON.stringify(state));
  });

  applyState(state);
})();

(() => {
  const root = document.querySelector("[data-timeline-root]");
  if (!root) {
    return;
  }

  const workspace = root.querySelector(".timeline-workspace") || root;
  const densitySelect = document.querySelector("[data-timeline-density]");
  const zoomSelect = document.querySelector("[data-timeline-zoom]");
  const searchInput = document.querySelector("[data-timeline-search]");
  const minimap = root.querySelector("[data-timeline-minimap]");
  const jumpSelects = document.querySelectorAll("[data-timeline-jump], [data-timeline-warning-jump], [data-timeline-entity-jump]");
  const sceneCards = [...root.querySelectorAll(".timeline-scene-card[data-scene-id]")];
  const collapsedKey = "plotline.timeline.collapsed";
  const densityKey = "plotline.timeline.density";
  const zoomKey = "plotline.timeline.zoom";
  const scrollKey = "plotline.timeline.pending-scroll";
  const navigationDiscoveryKey = "plotdirector.timelineNavigationDiscovered";
  const drawerWidthKey = "plotline.timeline.inspectorWidth";
  const horizontalOverflowTolerance = 8;
  const scrollContainerSelector = [
    ".timeline-shell",
    ".plot-lane-board",
    ".asset-lane-board",
    ".character-lane-board",
    ".metric-graph-stack",
    "[data-timeline-minimap]"
  ].join(",");
  const densityValues = ["compact", "comfortable", "expanded"];
  const zoomValues = ["book", "chapter", "scene-detail", "scene-overview"];
  const inspectorDrawer = root.querySelector("[data-inspector-drawer]");
  const inspectorResizer = root.querySelector("[data-inspector-resizer]");
  const inspectorClose = root.querySelector("[data-inspector-close]");

  const readSet = (key) => {
    try {
      return new Set(JSON.parse(localStorage.getItem(key) || "[]"));
    } catch {
      return new Set();
    }
  };

  const hasCollapsedPreference = localStorage.getItem(collapsedKey) !== null;
  const collapsed = readSet(collapsedKey);

  const getScrollContainers = () => [...root.querySelectorAll(scrollContainerSelector)];

  const setupTimelineNavigationGuidance = () => {
    const card = root.querySelector("[data-timeline-navigation-card]");
    if (!card) {
      return;
    }

    const dismissButton = card.querySelector("[data-timeline-navigation-dismiss]");
    const demoButton = card.querySelector("[data-timeline-navigation-demo]");
    const readDiscovered = () => {
      try {
        return localStorage.getItem(navigationDiscoveryKey) === "true";
      } catch {
        return false;
      }
    };
    const primaryScrollContainer = () => root.querySelector(".timeline-shell");
    const hasHorizontalOverflow = (container) => Boolean(container && container.scrollWidth > container.clientWidth + horizontalOverflowTolerance);
    const maxScrollLeft = (container) => Math.max(0, container.scrollWidth - container.clientWidth);
    const allScrollContainers = () => [workspace, ...getScrollContainers()].filter((container, index, containers) => (
      container && containers.indexOf(container) === index
    ));

    let dismissedForPage = false;
    let isDemoing = false;
    let isArmed = false;
    let dismissTimer = null;
    const knownLeft = new WeakMap();

    const rememberPosition = (container) => knownLeft.set(container, container.scrollLeft);
    const prefersReducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;

    const showCard = () => {
      window.clearTimeout(dismissTimer);
      card.hidden = false;
      card.classList.remove("is-dismissing");
    };

    const hideCard = (animate = false) => {
      window.clearTimeout(dismissTimer);
      if (!animate || prefersReducedMotion() || card.hidden) {
        card.classList.remove("is-dismissing");
        card.hidden = true;
        return;
      }

      card.classList.add("is-dismissing");
      dismissTimer = window.setTimeout(() => {
        card.hidden = true;
        card.classList.remove("is-dismissing");
      }, 1300);
    };

    const updateEdgeIndicators = (container) => {
      const hasOverflow = hasHorizontalOverflow(container);
      const maxLeft = maxScrollLeft(container);
      container.classList.toggle("timeline-horizontal-overflow", hasOverflow);
      container.classList.toggle("has-scroll-left", hasOverflow && container.scrollLeft > horizontalOverflowTolerance);
      container.classList.toggle("has-scroll-right", hasOverflow && container.scrollLeft < maxLeft - horizontalOverflowTolerance);
    };

    const updateGuidanceState = () => {
      const shouldShow = hasHorizontalOverflow(primaryScrollContainer()) && !readDiscovered() && !dismissedForPage;
      if (shouldShow) {
        showCard();
      } else if (!card.classList.contains("is-dismissing")) {
        hideCard(false);
      }
      allScrollContainers().forEach(updateEdgeIndicators);
    };

    const markDiscovered = () => {
      dismissedForPage = true;
      hideCard(true);
      try {
        localStorage.setItem(navigationDiscoveryKey, "true");
      } catch {
        // The card can still disappear for this page view if storage is unavailable.
      }
    };

    const detectHorizontalScroll = (container) => {
      updateEdgeIndicators(container);
      if (!isArmed || isDemoing) {
        rememberPosition(container);
        return;
      }

      const previous = knownLeft.get(container) ?? container.scrollLeft;
      if (Math.abs(container.scrollLeft - previous) > 4 && hasHorizontalOverflow(container)) {
        markDiscovered();
        return;
      }

      rememberPosition(container);
    };

    const containers = allScrollContainers();
    containers.forEach((container) => {
      rememberPosition(container);
      container.addEventListener("wheel", (event) => {
        if (event.defaultPrevented) {
          return;
        }

        if (event.shiftKey && Math.abs(event.deltaY) > Math.abs(event.deltaX) && hasHorizontalOverflow(container)) {
          const previous = container.scrollLeft;
          event.preventDefault();
          container.scrollLeft += event.deltaY;
          window.requestAnimationFrame(() => {
            updateEdgeIndicators(container);
            if (!isDemoing && Math.abs(container.scrollLeft - previous) > 4) {
              markDiscovered();
            }
            rememberPosition(container);
          });
        }
      }, { passive: false });
      container.addEventListener("scroll", () => detectHorizontalScroll(container), { passive: true });
    });

    dismissButton?.addEventListener("click", markDiscovered);
    demoButton?.addEventListener("click", () => {
      const container = primaryScrollContainer();
      if (!hasHorizontalOverflow(container)) {
        updateGuidanceState();
        return;
      }

      const originalLeft = container.scrollLeft;
      const availableRight = maxScrollLeft(container) - originalLeft;
      let distance = Math.min(220, availableRight);
      let direction = 1;
      if (distance < 24 && originalLeft > 0) {
        distance = Math.min(220, originalLeft);
        direction = -1;
      }
      if (distance < 4) {
        return;
      }

      isDemoing = true;
      container.scrollTo({ left: originalLeft + (distance * direction), behavior: "smooth" });
      window.setTimeout(() => {
        container.scrollTo({ left: originalLeft, behavior: "smooth" });
      }, 420);
      window.setTimeout(() => {
        isDemoing = false;
        allScrollContainers().forEach((scrollContainer) => {
          rememberPosition(scrollContainer);
          updateEdgeIndicators(scrollContainer);
        });
        updateGuidanceState();
      }, 950);
    });

    window.setTimeout(() => {
      isArmed = true;
      allScrollContainers().forEach(rememberPosition);
    }, 800);

    window.requestAnimationFrame(updateGuidanceState);
    window.addEventListener("resize", updateGuidanceState);
    window.addEventListener("plotline:timeline-layout-changed", updateGuidanceState);
    densitySelect?.addEventListener("change", () => window.requestAnimationFrame(updateGuidanceState));
    zoomSelect?.addEventListener("change", () => window.requestAnimationFrame(updateGuidanceState));
  };

  const drawerWidthBounds = () => {
    const availableWidth = root.getBoundingClientRect().width || window.innerWidth;
    const minimum = Math.min(420, Math.max(320, availableWidth - 360));
    const maximum = Math.max(minimum, Math.min(availableWidth * 0.65, availableWidth - 420));
    return { minimum, maximum, defaultWidth: Math.max(minimum, Math.min(maximum, availableWidth * 0.4)) };
  };

  const applyDrawerWidth = (width) => {
    if (!inspectorDrawer) {
      return;
    }

    const { minimum, maximum, defaultWidth } = drawerWidthBounds();
    const nextWidth = Math.max(minimum, Math.min(maximum, Number(width) || defaultWidth));
    root.style.setProperty("--timeline-inspector-width", `${Math.round(nextWidth)}px`);
  };

  if (inspectorDrawer) {
    applyDrawerWidth(localStorage.getItem(drawerWidthKey));
    window.addEventListener("resize", () => applyDrawerWidth(localStorage.getItem(drawerWidthKey)));
  }

  setupTimelineNavigationGuidance();

  inspectorResizer?.addEventListener("pointerdown", (event) => {
    event.preventDefault();
    document.body.classList.add("timeline-inspector-resizing");
    inspectorResizer.setPointerCapture(event.pointerId);

    const resize = (moveEvent) => {
      const rootRight = root.getBoundingClientRect().right;
      const nextWidth = rootRight - moveEvent.clientX;
      applyDrawerWidth(nextWidth);
    };

    const finish = () => {
      document.body.classList.remove("timeline-inspector-resizing");
      const width = Math.round(inspectorDrawer?.getBoundingClientRect().width || 0);
      if (width > 0) {
        localStorage.setItem(drawerWidthKey, String(width));
      }
      window.dispatchEvent(new Event("plotline:timeline-layout-changed"));
      inspectorResizer.removeEventListener("pointermove", resize);
      inspectorResizer.removeEventListener("pointerup", finish);
      inspectorResizer.removeEventListener("pointercancel", finish);
    };

    inspectorResizer.addEventListener("pointermove", resize);
    inspectorResizer.addEventListener("pointerup", finish);
    inspectorResizer.addEventListener("pointercancel", finish);
  });

  inspectorClose?.addEventListener("click", (event) => {
    event.preventDefault();
    const url = new URL(window.location.href);
    const focusType = url.searchParams.get("FocusType") || url.searchParams.get("focusType");
    url.searchParams.delete("SelectedSceneID");
    url.searchParams.delete("selectedSceneId");
    url.searchParams.delete("selectedsceneid");
    if (!focusType || focusType.toLowerCase() === "scene") {
      url.searchParams.delete("FocusType");
      url.searchParams.delete("focusType");
      url.searchParams.delete("FocusID");
      url.searchParams.delete("focusId");
    }
    saveScrollPosition();
    window.location.href = url.toString();
  });

  const saveScrollPosition = () => {
    try {
      sessionStorage.setItem(scrollKey, JSON.stringify({
        path: window.location.pathname,
        top: window.scrollY,
        left: window.scrollX,
        workspaceLeft: workspace.scrollLeft,
        workspaceTop: workspace.scrollTop,
        containers: getScrollContainers().map((container, index) => ({
          index,
          left: container.scrollLeft,
          top: container.scrollTop
        }))
      }));
    } catch {
      // Storage can be unavailable in private or restricted browser modes.
    }
  };

  const restoreScrollPosition = () => {
    let scrollPosition = null;
    try {
      scrollPosition = JSON.parse(sessionStorage.getItem(scrollKey) || "null");
      sessionStorage.removeItem(scrollKey);
    } catch {
      try {
        sessionStorage.removeItem(scrollKey);
      } catch {
        // Ignore storage failures; normal navigation should continue.
      }
      return;
    }

    if (!scrollPosition || scrollPosition.path !== window.location.pathname) {
      return;
    }

    window.requestAnimationFrame(() => {
      workspace.scrollLeft = Number(scrollPosition.workspaceLeft) || 0;
      workspace.scrollTop = Number(scrollPosition.workspaceTop) || 0;
      const containers = getScrollContainers();
      (scrollPosition.containers || []).forEach((saved) => {
        const container = containers[saved.index];
        if (container) {
          container.scrollLeft = Number(saved.left) || 0;
          container.scrollTop = Number(saved.top) || 0;
        }
      });
      window.scrollTo(Number(scrollPosition.left) || 0, Number(scrollPosition.top) || 0);
    });
  };

  window.PlotLineTimelineScroll = { save: saveScrollPosition };

  const applyMode = (kind, value, values) => {
    values.forEach((item) => root.classList.remove(`${kind}-${item}`));
    root.classList.add(`${kind}-${value}`);
  };

  const applyDensity = (value) => {
    const nextValue = densityValues.includes(value) ? value : "comfortable";
    applyMode("density", nextValue, densityValues);
    if (densitySelect) {
      densitySelect.value = nextValue;
    }
    localStorage.setItem(densityKey, nextValue);
    window.requestAnimationFrame(renderPlotLineSvg);
  };

  const applyZoom = (value) => {
    const nextValue = zoomValues.includes(value) ? value : "chapter";
    applyMode("zoom", nextValue, zoomValues);
    if (zoomSelect) {
      zoomSelect.value = nextValue;
    }
    localStorage.setItem(zoomKey, nextValue);
    window.requestAnimationFrame(renderPlotLineSvg);
  };

  const getScene = (sceneId) => root.querySelector(`[data-scene-id='${CSS.escape(String(sceneId))}']`);

  const scrollToScene = (sceneId, focus = true) => {
    const target = getScene(sceneId);
    if (!target) {
      return;
    }

    target.classList.add("keyboard-focus");
    target.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
    if (focus) {
      target.focus({ preventScroll: true });
    }
    window.setTimeout(() => target.classList.remove("keyboard-focus"), 1800);
  };

  const scrollActiveChapterIntoView = (behavior = "auto") => {
    const activeChapter = minimap?.querySelector("[data-active-chapter='true']");
    if (!minimap || !activeChapter) {
      return;
    }

    const hostRect = minimap.getBoundingClientRect();
    const activeRect = activeChapter.getBoundingClientRect();
    const activeCenter = activeRect.left + activeRect.width / 2;
    const hostCenter = hostRect.left + hostRect.width / 2;
    if (activeRect.left < hostRect.left || activeRect.right > hostRect.right) {
      minimap.scrollBy({ left: activeCenter - hostCenter, behavior });
    }
  };

  const saveCollapsed = () => localStorage.setItem(collapsedKey, JSON.stringify([...collapsed]));

  const renderPlotLineSvg = () => {
    root.querySelectorAll("[data-plot-line-svg-board]").forEach((board) => {
      const svg = board.querySelector("[data-plot-line-svg]");
      if (!svg) {
        return;
      }
      let healthSvg = board.querySelector("[data-plot-line-health-svg]");
      if (!healthSvg) {
        healthSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        healthSvg.setAttribute("class", "plot-lane-svg-overlay plot-lane-svg-health-overlay");
        healthSvg.setAttribute("data-plot-line-health-svg", "");
        healthSvg.setAttribute("aria-hidden", "true");
        svg.after(healthSvg);
      }

      svg.replaceChildren();
      healthSvg.replaceChildren();
      const boardRect = board.getBoundingClientRect();
      const width = Math.max(board.scrollWidth, boardRect.width);
      const height = Math.max(board.scrollHeight, boardRect.height);
      [svg, healthSvg].forEach((layer) => {
        layer.setAttribute("viewBox", `0 0 ${width} ${height}`);
        layer.setAttribute("width", String(width));
        layer.setAttribute("height", String(height));
      });

      const makeSvg = (name, attributes = {}) => {
        const element = document.createElementNS("http://www.w3.org/2000/svg", name);
        Object.entries(attributes).forEach(([key, value]) => element.setAttribute(key, String(value)));
        return element;
      };

      const rowCenterY = (row) => {
        const rowRect = row.getBoundingClientRect();
        return rowRect.top - boardRect.top + rowRect.height / 2;
      };

      const pointFor = (row, marker) => {
        const slot = marker.closest("[data-plot-line-slot]");
        if (!slot) {
          return null;
        }

        const slotRect = slot.getBoundingClientRect();
        const markerRect = marker.getBoundingClientRect();
        const markerCenterX = markerRect.left - boardRect.left + markerRect.width / 2;
        const markerCenterY = markerRect.top - boardRect.top + markerRect.height / 2;
        return {
          x: markerCenterX,
          y: markerCenterY,
          sceneX: slotRect.left - boardRect.left + slotRect.width / 2,
          radius: Math.max(markerRect.width, markerRect.height) / 2,
          sceneIndex: 0,
          type: (marker.dataset.plotEventType || "Progress").toLowerCase(),
          targetPlotLineId: marker.dataset.targetPlotLineId || "",
          targetPlotLineIds: (marker.dataset.targetPlotLineIds || "")
            .split(",")
            .map((value) => value.trim())
            .filter(Boolean)
        };
      };

      const sceneXs = [...new Set([...board.querySelectorAll("[data-plot-line-slot]")]
        .map((slot) => {
          const slotRect = slot.getBoundingClientRect();
          return Math.round(slotRect.left - boardRect.left + slotRect.width / 2);
        }))]
        .sort((a, b) => a - b);
      const latestSceneIndex = Math.max(sceneXs.length - 1, 0);

      const rowLookup = new Map();
      board.querySelectorAll("[data-plot-line-row]").forEach((row) => {
        if (row.dataset.plotLineId) {
          rowLookup.set(row.dataset.plotLineId, row);
        }
      });

      const rowData = [...rowLookup.values()].map((row) => ({
        row,
        plotLineId: row.dataset.plotLineId,
        colour: row.dataset.plotColour || "#2f6f63",
        points: [...row.querySelectorAll("[data-plot-event-node]")]
          .map((marker) => pointFor(row, marker))
          .filter(Boolean)
          .map((point) => ({
            ...point,
            sceneIndex: Math.max(0, sceneXs.findIndex((x) => x === Math.round(point.sceneX)))
          }))
          .sort((a, b) => a.x - b.x),
        incomingStartXs: []
      }));
      const rowDataById = new Map(rowData.map((item) => [item.plotLineId, item]));

      const healthFor = (points) => {
        const hasResolve = points.some((point) => point.type === "resolve");
        const hasAbandon = points.some((point) => point.type === "abandon");
        const resolvePoint = points.find((point) => point.type === "resolve");
        const abandonPoint = points.find((point) => point.type === "abandon");
        const hasMerge = points.some((point) => point.type === "merge" && point.targetPlotLineId);
        const hasSplit = points.some((point) => point.type === "split" && point.targetPlotLineIds.length >= 2);
        const hasBranchFromResolution = Boolean(resolvePoint) && points.some((point) =>
          point.type === "branch" && point.targetPlotLineId && point.sceneIndex >= resolvePoint.sceneIndex);
        const lastSceneIndex = points.reduce((max, point) => Math.max(max, point.sceneIndex), 0);
        const isNeglected = !hasResolve && !hasAbandon && latestSceneIndex - lastSceneIndex >= 10;
        const isDangling = !hasResolve && !hasAbandon && !hasMerge && !hasSplit && latestSceneIndex >= lastSceneIndex;
        const isQuestionable = hasResolve && !hasMerge && !hasSplit && !hasBranchFromResolution;
        let state = "active";

        if (hasAbandon) {
          state = "abandoned";
        } else if (hasResolve) {
          state = isQuestionable ? "questionable" : "resolved";
        } else if (isNeglected) {
          state = "neglected";
        }

        return {
          state,
          isNeglected,
          isDangling,
          isQuestionable,
          isAbandoned: hasAbandon,
          resolvePoint,
          abandonPoint
        };
      };

      const healthWarningTooltip = (warnings) => warnings.map((warning) => [
        `Warning: ${warning.label}`,
        `Reason: ${warning.reason}`,
        `Suggested Action: ${warning.action}`
      ].join("\n")).join("\n\n");

      const drawHealthMarker = (point, warnings) => {
        const offsetX = Math.max(18, (point.radius || 12) + 8);
        const primaryKind = warnings[0]?.kind || "warning";
        const group = makeSvg("g", { class: `plotline-health-marker plotline-health-marker-${primaryKind}` });
        const title = makeSvg("title");
        title.textContent = healthWarningTooltip(warnings);
        group.append(title);

        const markerX = point.x + offsetX;
        const markerY = point.y - 13;
        if (primaryKind === "questionable") {
          group.append(makeSvg("circle", { cx: markerX, cy: markerY, r: 9 }));
          const text = makeSvg("text", { x: markerX, y: markerY + 4, "text-anchor": "middle" });
          text.textContent = warnings.length > 1 ? String(warnings.length) : "?";
          group.append(text);
        } else {
          group.append(makeSvg("circle", { cx: markerX, cy: markerY, r: 10 }));
          group.append(makeSvg("path", {
            d: `M ${markerX} ${markerY - 6} L ${markerX + 6} ${markerY + 5} L ${markerX - 6} ${markerY + 5} Z`
          }));
          group.append(makeSvg("line", { x1: markerX, y1: markerY - 2, x2: markerX, y2: markerY + 2.5 }));
          group.append(makeSvg("circle", { cx: markerX, cy: markerY + 5.3, r: 1.2 }));
          if (warnings.length > 1) {
            const text = makeSvg("text", { x: markerX + 11, y: markerY - 6, "text-anchor": "middle", class: "plotline-health-count" });
            text.textContent = String(warnings.length);
            group.append(text);
          }
        }

        healthSvg.append(group);
      };

      const drawConnector = (source, targetRow, colour, type) => {
        if (!targetRow) {
          if (window.console?.warn) {
            window.console.warn(`Plot line ${type} connector skipped because target plot line data is missing.`);
          }
          return;
        }

        const target = { x: source.x, y: rowCenterY(targetRow) };
        const flowX = Math.max(28, Math.min(46, Math.abs(target.y - source.y) * 0.28));
        const sourceRadius = Math.max(8, source.radius || 12);
        const startX = source.x + sourceRadius;
        const targetX = startX + flowX;
        const midX = startX + flowX / 2;
        svg.append(makeSvg("path", {
          class: `plot-line-svg-connector plot-line-svg-connector-${type}`,
          d: `M ${startX} ${source.y} C ${midX} ${source.y}, ${midX} ${target.y}, ${targetX} ${target.y}`,
          stroke: colour
        }));
      };

      rowData.forEach((item) => {
        item.points.forEach((point) => {
          if (point.type === "branch" && point.targetPlotLineId) {
            rowDataById.get(point.targetPlotLineId)?.incomingStartXs.push(point.x);
          }

          if (point.type === "split") {
            point.targetPlotLineIds.forEach((targetPlotLineId) => {
              rowDataById.get(targetPlotLineId)?.incomingStartXs.push(point.x);
            });
          }
        });
      });

      rowData.forEach((item) => {
        item.health = healthFor(item.points);
        item.state = item.health.state;
      });

      rowData.forEach((item) => {
        const { colour, points, incomingStartXs, state } = item;
        if (points.length > 0) {
          const explicitStart = points.find((point) => point.type === "start");
          const incomingStartX = incomingStartXs.length ? Math.min(...incomingStartXs) : null;
          const terminatingPoint = points.find((point) => point.type === "split" || point.type === "merge");
          const first = points[0];
          const last = points[points.length - 1];
          const startX = incomingStartX !== null && !(explicitStart && explicitStart.x < incomingStartX)
            ? incomingStartX
            : first.x;
          const endX = terminatingPoint ? terminatingPoint.x : last.x;
          item.lineEndPoint = { x: endX, y: first.y };
          const sortedPoints = [...points].sort((a, b) => a.x - b.x);
          const terminatingIndex = terminatingPoint ? sortedPoints.indexOf(terminatingPoint) : -1;
          const linePoints = terminatingIndex >= 0 ? sortedPoints.slice(0, terminatingIndex + 1) : sortedPoints;
          if (incomingStartX !== null && incomingStartX < linePoints[0].x) {
            linePoints.unshift({ ...linePoints[0], x: incomingStartX, sceneIndex: Math.max(0, linePoints[0].sceneIndex - 1) });
          }

          // Only the segment between two events is dormant when their scene gap crosses the threshold.
          // Thread-level resolved/abandoned/neglected state must not make the entire historical line dotted.
          const dormantGapThreshold = 10;
          linePoints.forEach((point, index) => {
            const nextPoint = linePoints[index + 1];
            if (!nextPoint || nextPoint.x <= point.x) {
              return;
            }
            const isDormantGap = Math.abs(nextPoint.sceneIndex - point.sceneIndex) >= dormantGapThreshold;
            svg.append(makeSvg("path", {
              class: `plot-line-svg-path plot-line-svg-segment ${isDormantGap ? "plot-line-svg-segment-dormant" : ""} plotline-state-${state}`,
              d: `M ${point.x} ${point.y} L ${nextPoint.x} ${nextPoint.y}`,
              stroke: colour
            }));
          });
        }
      });

      rowData.forEach(({ points, colour }) => {
        points.forEach((point) => {
          if (point.type === "branch" || point.type === "merge") {
            drawConnector(point, rowLookup.get(point.targetPlotLineId), colour, point.type);
          }

          if (point.type === "split" && point.targetPlotLineIds.length >= 2) {
            point.targetPlotLineIds.forEach((targetPlotLineId) => {
              drawConnector(point, rowLookup.get(targetPlotLineId), colour, point.type);
            });
          } else if (point.type === "split" && point.targetPlotLineIds.length > 0 && window.console?.warn) {
            window.console.warn("Plot line split connector skipped because fewer than two targets are available.");
          }
        });
      });

      rowData.forEach((item) => {
        const markerPoint = item.lineEndPoint || item.points[item.points.length - 1];
        const healthWarnings = [];
        if (item.health?.isNeglected && markerPoint) {
          healthWarnings.push({
            point: markerPoint,
            kind: "neglected",
            label: "Dormant Thread",
            reason: "No activity for 10+ scenes.",
            action: "Add a Progress, Reveal or Resolve event."
          });
        }

        if (item.health?.isDangling && markerPoint) {
          healthWarnings.push({
            point: markerPoint,
            kind: "dangling",
            label: "Unresolved Thread",
            reason: "Thread contains no Resolve or Abandon event by the end of the current timeline.",
            action: "Review ending scenes and add Resolve or Abandon where appropriate."
          });
        }

        if (item.health?.isQuestionable && item.health.resolvePoint) {
          healthWarnings.push({
            point: item.health.resolvePoint,
            kind: "questionable",
            label: "Missing Closure",
            reason: "Resolved without merge, split, or branch.",
            action: "Verify this thread has sufficient payoff."
          });
        }

        if (item.health?.isAbandoned && item.health.abandonPoint) {
          healthWarnings.push({
            point: item.health.abandonPoint,
            kind: "abandoned",
            label: "Abandoned Thread",
            reason: "This plot line was abandoned.",
            action: "Check this is intentional."
          });
        }

        const groupedWarnings = new Map();
        healthWarnings.forEach((warning) => {
          const key = `${Math.round(warning.point.x)}:${Math.round(warning.point.y)}`;
          if (!groupedWarnings.has(key)) {
            groupedWarnings.set(key, { point: warning.point, warnings: [] });
          }
          groupedWarnings.get(key).warnings.push(warning);
        });
        groupedWarnings.forEach(({ point, warnings }) => drawHealthMarker(point, warnings));
      });
    });
  };

  const setCollapsed = (id, shouldCollapse) => {
    document.querySelectorAll(`[data-collapse-body='${CSS.escape(id)}']`).forEach((body) => {
      body.classList.toggle("timeline-section-collapsed", shouldCollapse);
    });
    document.querySelectorAll(`[data-collapse-toggle='${CSS.escape(id)}']`).forEach((button) => {
      button.classList.toggle("is-collapsed", shouldCollapse);
      button.setAttribute("aria-expanded", String(!shouldCollapse));
    });
  };

  document.querySelectorAll("[data-collapse-toggle]").forEach((button) => {
    const id = button.dataset.collapseToggle;
    if (!hasCollapsedPreference && button.dataset.collapseDefault === "collapsed") {
      collapsed.add(id);
    }

    const isCollapsed = collapsed.has(id);
    setCollapsed(id, isCollapsed);
    button.addEventListener("click", () => {
      const nextCollapsed = !collapsed.has(id);
      if (nextCollapsed) {
        collapsed.add(id);
      } else {
        collapsed.delete(id);
      }
      setCollapsed(id, nextCollapsed);
      saveCollapsed();
      renderPlotLineSvg();
      window.dispatchEvent(new Event("plotline:timeline-layout-changed"));
    });
  });

  densitySelect?.addEventListener("change", () => applyDensity(densitySelect.value));
  zoomSelect?.addEventListener("change", () => applyZoom(zoomSelect.value));
  applyDensity(localStorage.getItem(densityKey) || "comfortable");
  applyZoom(localStorage.getItem(zoomKey) || "chapter");
  const hadPendingScrollRestore = (() => {
    try {
      return sessionStorage.getItem(scrollKey) !== null;
    } catch {
      return false;
    }
  })();
  window.requestAnimationFrame(() => {
    renderPlotLineSvg();
    scrollActiveChapterIntoView("auto");
    if (hadPendingScrollRestore) {
      restoreScrollPosition();
    }
  });

  window.addEventListener("resize", renderPlotLineSvg);
  window.addEventListener("plotline:timeline-layout-changed", renderPlotLineSvg);

  const saveScrollForSceneSelection = (event) => {
    if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
      return;
    }

    const target = event.target instanceof Element ? event.target : null;
    const link = target?.closest("a[href]");
    if (!link || !root.contains(link)) {
      return;
    }

    const url = new URL(link.href, window.location.href);
    if ([...url.searchParams.keys()].some((key) => key.toLowerCase() === "selectedsceneid")) {
      saveScrollPosition();
    }
  };

  root.addEventListener("click", saveScrollForSceneSelection, { capture: true });

  const runSearch = () => {
    const query = searchInput?.value.trim().toLowerCase() || "";
    let firstMatch = null;
    sceneCards.forEach((card) => {
      const matches = !query || (card.dataset.timelineSearchText || "").toLowerCase().includes(query);
      card.classList.toggle("search-hit", Boolean(query && matches));
      card.classList.toggle("search-dim", Boolean(query && !matches));
      if (!firstMatch && query && matches) {
        firstMatch = card;
      }
    });
    return firstMatch;
  };

  searchInput?.addEventListener("input", runSearch);
  searchInput?.addEventListener("keydown", (event) => {
    if (event.key !== "Enter") {
      return;
    }

    const firstMatch = runSearch();
    if (firstMatch) {
      event.preventDefault();
      scrollToScene(firstMatch.dataset.sceneId);
    }
  });

  jumpSelects.forEach((select) => {
    select.addEventListener("change", () => {
      if (select.value) {
        scrollToScene(select.value);
      }
    });
  });

  document.querySelectorAll("[data-jump-scene]").forEach((button) => {
    button.addEventListener("click", () => {
      if (button.dataset.jumpScene) {
        scrollToScene(button.dataset.jumpScene);
      }
    });
  });

  document.addEventListener("keydown", (event) => {
    const activeTag = document.activeElement?.tagName?.toLowerCase();
    const isTyping = ["input", "select", "textarea"].includes(activeTag) || document.activeElement?.isContentEditable;

    if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "f" && searchInput) {
      event.preventDefault();
      searchInput.focus();
      searchInput.select();
      return;
    }

    if (event.key === "Escape") {
      const url = new URL(window.location.href);
      const hadFocus = url.searchParams.has("FocusType") || url.searchParams.has("FocusID");
      url.searchParams.delete("FocusType");
      url.searchParams.delete("FocusID");
      if (hadFocus) {
        window.location.href = url.toString();
      } else if (searchInput?.value) {
        searchInput.value = "";
        runSearch();
      }
      return;
    }

    if (isTyping) {
      return;
    }

    if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
      event.preventDefault();
      workspace.scrollBy({ left: event.key === "ArrowLeft" ? -260 : 260, behavior: "smooth" });
      return;
    }

    if (event.key === "+" || event.key === "=" || event.key === "-") {
      const currentIndex = zoomValues.indexOf(zoomSelect?.value || "chapter");
      const direction = event.key === "-" ? -1 : 1;
      const nextIndex = Math.min(zoomValues.length - 1, Math.max(0, currentIndex + direction));
      applyZoom(zoomValues[nextIndex]);
    }
  });
})();

(() => {
  const root = document.querySelector("[data-timeline-root]");
  const panel = document.querySelector("[data-drag-drop-panel]");
  if (!panel) {
    return;
  }

  const forms = new Map([...document.querySelectorAll("[data-drag-form]")].map((form) => [form.dataset.dragForm, form]));
  const title = panel.querySelector("[data-drag-panel-title]");
  const kicker = panel.querySelector("[data-drag-panel-kicker]");
  const summary = panel.querySelector("[data-drag-panel-summary]");
  const sceneMoveModalElement = document.querySelector("[data-scene-move-modal]");
  const sceneMoveModal = sceneMoveModalElement && window.bootstrap?.Modal
    ? window.bootstrap.Modal.getOrCreateInstance(sceneMoveModalElement)
    : null;
  const sceneMoveTitle = sceneMoveModalElement?.querySelector("[data-scene-move-title]");
  const sceneMoveSummary = sceneMoveModalElement?.querySelector("[data-scene-move-summary]");
  const sceneMoveSubmit = sceneMoveModalElement?.querySelector("[data-scene-move-submit]");
  const scrollKey = "plotline.timeline.scroll";
  let currentDrag = null;
  let suppressSceneClickUntil = 0;

  const setValue = (form, name, value) => {
    const field = form?.querySelector(`[name='${CSS.escape(name)}']`);
    if (field) {
      field.value = value ?? "";
    }
  };

  const selectText = (select) => select?.selectedOptions?.[0]?.textContent?.trim() || "";
  const quoteScene = (label) => `\u201c${label || "this scene"}\u201d`;
  const buildSceneMoveSummary = (payload, sourceChapter, targetChapter, position, anchorLabel) => {
    if (sourceChapter && targetChapter && sourceChapter === targetChapter) {
      return `Move ${quoteScene(payload.label)} to this new position within ${targetChapter}?`;
    }

    if (sourceChapter && targetChapter) {
      return `Move ${quoteScene(payload.label)} from ${sourceChapter} to ${targetChapter}?`;
    }

    return `Move ${quoteScene(payload.label)} ${position.toLowerCase()} ${anchorLabel}?`;
  };

  const showForm = (key, heading, body, actionLabel) => {
    if (key === "scene") {
      panel.classList.add("d-none");
      panel.querySelectorAll("[data-drag-form]").forEach((form) => form.classList.add("d-none"));
      forms.get("scene")?.classList.remove("d-none");
      if (sceneMoveTitle) {
        sceneMoveTitle.textContent = heading;
      }
      if (sceneMoveSummary) {
        sceneMoveSummary.textContent = body;
      }
      if (sceneMoveSubmit) {
        sceneMoveSubmit.classList.toggle("d-none", heading.toLowerCase().startsWith("cannot") || heading.toLowerCase().startsWith("scene already"));
      }
      if (sceneMoveModal) {
        sceneMoveModal.show();
      } else {
        panel.classList.remove("d-none");
      }
      return;
    }

    forms.forEach((form, formKey) => form.classList.toggle("d-none", formKey !== key));
    panel.classList.remove("d-none");
    title.textContent = heading;
    kicker.textContent = actionLabel;
    summary.textContent = body;
    panel.scrollIntoView({ behavior: "smooth", block: "center" });
  };

  const hidePanel = () => {
    panel.classList.add("d-none");
    forms.forEach((form) => form.classList.add("d-none"));
  };

  const clearDropHighlights = () => {
    document
      .querySelectorAll(".drop-target-active, .drop-before, .drop-after, .chapter-drop-active, .drop-chapter-end")
      .forEach((target) => target.classList.remove("drop-target-active", "drop-before", "drop-after", "chapter-drop-active", "drop-chapter-end"));
  };

  const saveTimelineScroll = () => {
    const workspace = root?.querySelector(".timeline-shell");
    try {
      sessionStorage.setItem(scrollKey, JSON.stringify({ left: workspace?.scrollLeft || 0, top: window.scrollY || 0 }));
    } catch {
      // Scroll restoration is a convenience; blocked storage should not stop the move.
    }
  };

  try {
    const storedScroll = JSON.parse(sessionStorage.getItem(scrollKey) || "null");
    if (storedScroll) {
      sessionStorage.removeItem(scrollKey);
      requestAnimationFrame(() => {
        const workspace = root?.querySelector(".timeline-shell");
        if (workspace) {
          workspace.scrollLeft = storedScroll.left || 0;
        }
        window.scrollTo({ top: storedScroll.top || 0 });
      });
    }
  } catch {
    sessionStorage.removeItem(scrollKey);
  }

  const readPayload = (event) => {
    try {
      const payload = JSON.parse(event.dataTransfer.getData("application/json") || "{}");
      return payload.type ? payload : currentDrag || {};
    } catch {
      return currentDrag || {};
    }
  };

  const writePayload = (event, element) => {
    const payload = {
      type: element.dataset.dragType,
      id: element.dataset.dragId,
      label: element.dataset.dragLabel || element.textContent.trim()
    };
    event.dataTransfer.effectAllowed = "copyMove";
    event.dataTransfer.setData("application/json", JSON.stringify(payload));
    event.dataTransfer.setData("text/plain", `${payload.type}:${payload.id}`);
    currentDrag = payload;
    document.body.classList.add("drag-active", `dragging-${payload.type}`);
    element.classList.add("is-dragging");
  };

  document.querySelectorAll("[data-drag-type][draggable='true']").forEach((item) => {
    item.addEventListener("dragstart", (event) => writePayload(event, item));
    item.addEventListener("dragend", () => {
      item.classList.remove("is-dragging");
      document.body.classList.remove("drag-active", "dragging-scene", "dragging-character", "dragging-asset", "dragging-location");
      currentDrag = null;
      clearDropHighlights();
    });
  });

  panel.querySelectorAll("[data-drag-cancel]").forEach((button) => button.addEventListener("click", hidePanel));
  forms.get("scene")?.addEventListener("submit", saveTimelineScroll);

  root?.querySelectorAll(".timeline-scene-card[data-scene-id]").forEach((card) => {
    card.addEventListener("click", (event) => {
      if (Date.now() < suppressSceneClickUntil) {
        event.preventDefault();
        event.stopPropagation();
      }
    });

    card.addEventListener("dragover", (event) => {
      const payload = readPayload(event);
      if (["scene", "character", "asset"].includes(payload.type)) {
        event.preventDefault();
        event.stopPropagation();
        event.dataTransfer.dropEffect = payload.type === "scene" ? "move" : "copy";
        card.classList.add("drop-target-active");
        card.classList.toggle("drop-before", payload.type === "scene" && event.offsetX < card.clientWidth / 2);
        card.classList.toggle("drop-after", payload.type === "scene" && event.offsetX >= card.clientWidth / 2);
      }
    });

    card.addEventListener("dragleave", () => card.classList.remove("drop-target-active", "drop-before", "drop-after"));

    card.addEventListener("drop", (event) => {
      const payload = readPayload(event);
      if (!["scene", "character", "asset"].includes(payload.type)) {
        return;
      }

      event.preventDefault();
      event.stopPropagation();
      suppressSceneClickUntil = Date.now() + 800;
      card.classList.remove("drop-target-active", "drop-before", "drop-after");
      const sceneId = card.dataset.sceneId;

      if (payload.type === "scene") {
        if (payload.id === sceneId) {
          showForm("scene", "Cannot move scene", "Drop a scene before or after a different scene.", "Scene move");
          return;
        }

        const form = forms.get("scene");
        const position = event.offsetX < card.clientWidth / 2 ? "Before" : "After";
        const sourceCard = root.querySelector(`.timeline-scene-card[data-scene-id='${CSS.escape(payload.id)}']`);
        const sourceChapter = sourceCard?.dataset.chapterLabel || "";
        const targetChapter = card.dataset.chapterLabel || "";
        setValue(form, "SceneID", payload.id);
        setValue(form, "AnchorSceneID", sceneId);
        setValue(form, "TargetChapterID", "");
        setValue(form, "Position", position);
        showForm("scene", "Move Scene?", buildSceneMoveSummary(payload, sourceChapter, targetChapter, position, card.dataset.dragLabel), "Scene move");
        return;
      }

      if (payload.type === "character") {
        const form = forms.get("character");
        setValue(form, "SceneID", sceneId);
        setValue(form, "CharacterID", payload.id);
        showForm("character", "Add character to scene", `Add ${payload.label} to ${card.dataset.dragLabel}.`, "Character drop");
        return;
      }

      const form = forms.get("asset-event");
      setValue(form, "SceneID", sceneId);
      setValue(form, "StoryAssetID", payload.id);
      setValue(form, "EventTitle", "");
      showForm("asset-event", "What happens to this asset?", `Add an event for ${payload.label} in ${card.dataset.dragLabel}.`, "Asset drop");
    });
  });

  root?.querySelectorAll(".timeline-scenes[data-chapter-drop-target]").forEach((chapterBody) => {
    const chapter = chapterBody.closest("[data-chapter-drop-target]");
    const chapterId = chapterBody.dataset.chapterId || chapter?.dataset.chapterId;
    const chapterLabel = chapterBody.dataset.chapterLabel || chapter?.dataset.chapterLabel || "this chapter";

    chapterBody.addEventListener("dragover", (event) => {
      const payload = readPayload(event);
      if (payload.type !== "scene" || !chapterId) {
        return;
      }

      if (event.target.closest(".timeline-scene-card[data-scene-id]")) {
        return;
      }

      event.preventDefault();
      event.dataTransfer.dropEffect = "move";
      chapterBody.classList.add("drop-target-active", "drop-chapter-end");
      chapter?.classList.add("chapter-drop-active");
    });

    chapterBody.addEventListener("dragleave", (event) => {
      if (chapterBody.contains(event.relatedTarget)) {
        return;
      }

      chapterBody.classList.remove("drop-target-active", "drop-chapter-end");
      chapter?.classList.remove("chapter-drop-active");
    });

    chapterBody.addEventListener("drop", (event) => {
      const payload = readPayload(event);
      if (payload.type !== "scene" || !chapterId) {
        return;
      }

      if (event.target.closest(".timeline-scene-card[data-scene-id]")) {
        return;
      }

      event.preventDefault();
      event.stopPropagation();
      suppressSceneClickUntil = Date.now() + 800;
      clearDropHighlights();
      const sourceCard = root.querySelector(`.timeline-scene-card[data-scene-id='${CSS.escape(payload.id)}']`);
      if (sourceCard?.dataset.chapterId === chapterId && chapterBody.lastElementChild?.dataset?.sceneId === payload.id) {
        showForm("scene", "Scene already here", `${payload.label} is already at the end of ${chapterLabel}.`, "Scene move");
        return;
      }

      const form = forms.get("scene");
      setValue(form, "SceneID", payload.id);
      setValue(form, "AnchorSceneID", "");
      setValue(form, "TargetChapterID", chapterId);
      setValue(form, "Position", "End");
      showForm("scene", "Move Scene?", buildSceneMoveSummary(payload, sourceCard?.dataset.chapterLabel || "", chapterLabel, "After", chapterLabel), "Scene move");
    });
  });

  const assetDependencyForm = forms.get("asset-dependency");
  const updateAssetPreview = () => {
    const source = assetDependencyForm?.dataset.sourceLabel || "Source asset";
    const target = assetDependencyForm?.dataset.targetLabel || "Target asset";
    const dependency = selectText(assetDependencyForm?.querySelector("[name='AssetDependencyTypeID']")) || "Requires";
    const sourceState = selectText(assetDependencyForm?.querySelector("[name='SourceRequiredStateID']")) || "the required state";
    const targetState = selectText(assetDependencyForm?.querySelector("[name='TargetBlockedStateID']")) || "the blocked state";
    const preview = panel.querySelector("[data-asset-dependency-preview]");
    if (preview) {
      preview.textContent = `${source} ${dependency.toLowerCase()} ${target}. ${source} should be ${sourceState} before ${target} can be ${targetState}.`;
    }
  };

  assetDependencyForm?.querySelectorAll("select").forEach((select) => select.addEventListener("change", updateAssetPreview));

  document.querySelectorAll("[data-asset-drop-target], .drag-chip.asset").forEach((target) => {
    target.addEventListener("dragover", (event) => {
      const payload = readPayload(event);
      if (payload.type === "asset") {
        event.preventDefault();
        event.dataTransfer.dropEffect = "copy";
        target.classList.add("drop-target-active");
      }
    });
    target.addEventListener("dragleave", () => target.classList.remove("drop-target-active"));
    target.addEventListener("drop", (event) => {
      const payload = readPayload(event);
      const targetId = target.dataset.assetId || target.dataset.dragId;
      const targetLabel = target.dataset.assetLabel || target.dataset.dragLabel || target.textContent.trim();
      if (payload.type !== "asset" || !targetId) {
        return;
      }

      event.preventDefault();
      target.classList.remove("drop-target-active");
      if (payload.id === targetId) {
        showForm("asset-dependency", "Cannot create dependency", "Drop one asset onto a different asset.", "Asset dependency");
        return;
      }

      setValue(assetDependencyForm, "SourceAssetID", payload.id);
      setValue(assetDependencyForm, "TargetAssetID", targetId);
      assetDependencyForm.dataset.sourceLabel = payload.label;
      assetDependencyForm.dataset.targetLabel = targetLabel;
      updateAssetPreview();
      showForm("asset-dependency", "Create asset dependency", `${payload.label} -> ${targetLabel}`, "Asset dependency");
    });
  });

  document.querySelectorAll("[data-drag-type='location']").forEach((target) => {
    target.addEventListener("dragover", (event) => {
      const payload = readPayload(event);
      if (payload.type === "location") {
        event.preventDefault();
        event.dataTransfer.dropEffect = "copy";
        target.classList.add("drop-target-active");
      }
    });
    target.addEventListener("dragleave", () => target.classList.remove("drop-target-active"));
    target.addEventListener("drop", (event) => {
      const payload = readPayload(event);
      const targetId = target.dataset.dragId;
      const targetLabel = target.dataset.dragLabel || target.textContent.trim();
      if (payload.type !== "location") {
        return;
      }

      event.preventDefault();
      target.classList.remove("drop-target-active");
      if (payload.id === targetId) {
        showForm("location-relationship", "Cannot create relationship", "Drop one location onto a different location.", "Location relationship");
        return;
      }

      const form = forms.get("location-relationship");
      setValue(form, "FromLocationID", payload.id);
      setValue(form, "ToLocationID", targetId);
      showForm("location-relationship", "Create location relationship", `${payload.label} -> ${targetLabel}`, "Location drop");
    });
  });
})();
