(() => {

  // IGblue DEBUG v36: set IG_DBG_ON=0 or put `return;` at top of dbg() to disable all logs.
  const IG_DBG_ON = 1;
  function dbg(fn, loc, line, value) {
    // return; // DEBUG OFF: uncomment this one line later.
    if (!IG_DBG_ON) return;
    try {
      const clip = (v) => {
        const seen = new WeakSet();
        let s = JSON.stringify(v, (k, val) => {
          if (typeof val === "string") return val.length > 500 ? val.slice(0, 500) + `...<${val.length}>` : val;
          if (val && typeof val === "object") {
            if (seen.has(val)) return "[Circular]";
            seen.add(val);
          }
          return val;
        });
        if (s && s.length > 2200) s = s.slice(0, 2200) + `...<${s.length}>`;
        return s || "";
      };
      const payload = { ts: new Date().toISOString(), file: "side.js", fn, loc, line, href: location.href, value };
      const out = clip(payload);
      const key = `IG_dbg/side.js/${fn}/${loc}/${line}`.replace(/\s+/g, "_");
      localStorage.setItem(key, out);
      localStorage.setItem("IG_dbg/side.js/_last", out);
      console.log(`[IG_DBG][side.js][${fn}][${loc}:${line}]`, value);
    } catch (e) {}
  }
  function dbgMedia(m){
    return m ? {
      code:m.code, username:m.username, media_type:m.media_type, has_img_origin:!!m.img_origin,
      has_video_url:!!m.video_url, has_carousel:!!(typeof m.carousel_media === "string" && m.carousel_media.trim()),
      carousel_count: typeof m.carousel_media === "string" ? m.carousel_media.split("\n").filter(Boolean).length : 0,
      like_count:m.like_count, comment_count:m.comment_count, media_repost_count:m.media_repost_count,
      share_count:m.share_count, play_count:m.play_count, created_at:m.created_at,
      video_url:m.video_url ? String(m.video_url).slice(0,180) : "",
      img_origin:m.img_origin ? String(m.img_origin).slice(0,180) : ""
    } : null;
  }

  const IGBLUE_BUILD_VERSION = (() => { try { const m = chrome.runtime.getManifest(); return String(m.version_name || m.version || "2.3.8.170"); } catch (_) { return "2.3.8.170"; } })();
  const IGBLUE_BUILD_TAG = (() => { const m = String(IGBLUE_BUILD_VERSION || "").match(/(\d+(?:a\d*)?)$/i); return m ? ("v" + m[1]) : String(IGBLUE_BUILD_VERSION || "v170"); })();
  const IGBLUE_PACKAGE_VERSION = "v239";
  const IGBLUE_CYCLE_SCHEMA = "ig_auto_cycle_state_v127";
  function teBuildCycleId(suffix) { return `${IGBLUE_BUILD_TAG}_${Date.now()}_${String(suffix || "cycle")}`; }

  (() => {
    "use strict";
    var e = window.IGBLUE_EXCELJS || window.ExcelJS;
    var t = () => e;
    if (!e) console.warn("[IG] side_xlsx_export.js not loaded; export_xlsx unavailable");
    function teCopyTextTripleEnter(text) {
        const payload = String(text == null ? "" : text).trimEnd() + "\n\n\n";
        try {
          if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(payload).catch(()=>{});
            return true;
          }
        } catch (_) {}
        try {
          const ta = document.createElement("textarea");
          ta.value = payload;
          ta.style.position = "fixed";
          ta.style.left = "-9999px";
          document.body.appendChild(ta);
          ta.focus();
          ta.select();
          document.execCommand("copy");
          document.body.removeChild(ta);
          return true;
        } catch (_) { return false; }
      }
    function n(e) {
        const msg = String(e == null ? "" : e);
        if (!msg.trim()) return;
        if (/상태\s*전송은\s*최신값만\s*유지하여\s*중복전송을\s*생략/.test(msg)) return;
        var t = document.createElement("div");
        ((t.textContent = msg),
          (t.title = "클릭하면 클립보드에 복사됩니다."),
          (t.style.visibility = "hidden"),
          (t.style.minWidth = "220px"),
          (t.style.maxWidth = "calc(100vw - 24px)"),
          (t.style.backgroundColor = "#1f2937"),
          (t.style.color = "#fff"),
          (t.style.textAlign = "center"),
          (t.style.borderRadius = "10px"),
          (t.style.padding = "10px 12px"),
          (t.style.position = "fixed"),
          (t.style.zIndex = "2147483647"),
          (t.style.left = "50%"),
          (t.style.bottom = "8px"),
          (t.style.transform = "translateX(-50%)"),
          (t.style.fontSize = "13px"),
          (t.style.lineHeight = "1.35"),
          (t.style.cursor = "copy"),
          (t.style.boxShadow = "0 8px 18px rgba(0,0,0,0.22)"),
          (t.style.transition = "all 0.25s ease"),
          t.addEventListener("click", function(){ teCopyTextTripleEnter(msg); }),
          document.body.appendChild(t),
          setTimeout(function () {
            ((t.style.visibility = "visible"),
              (t.style.opacity = "1"),
              (t.style.bottom = "12px"));
          }, 50),
          setTimeout(function () {
            ((t.style.opacity = "0"),
              (t.style.visibility = "hidden"),
              setTimeout(function () {
                try { document.body.removeChild(t); } catch (_) {}
              }, 300));
          }, 3200));
      }
      const i = ["auto_swipe", "auto_swipe_10", "auto_swipe_30", "auto_top"],
        s = ["export_xlsx", "refresh"],
        o = ["down_all_medias", "down_filtered_medias"],
        a = "bulk-download",
        l = new Set();
      let c = null,
        u = !1,
        h = 0,
        f = 0,
        d = 0,
        p = null,
        teProgressState = { visible: !1, done: !1, file: "", file_path: "", file_paths: [], no_label: "", loaded: 0, total: 0, percent: 0, message: "", owner_key: "" };
      let teAutoPyStats = { active: false, run_seq: 0, start_ts: 0, current_no: 0, current_no_label: "", max_rank: 0, mp4_count: 0, jpg_count: 0, mp4_total_ms: 0, jpg_total_ms: 0, mp4_max_ms: 0, jpg_max_ms: 0, mp4_min_ms: 0, jpg_min_ms: 0, cycle_count: 0, cycle_total_ms: 0, cycle_max_ms: 0, cycle_min_ms: 0, last_reason: "" };
      let teAutoPyLastDoneText = "";
      let teAutoPyStatsTimer = 0;
      let teAutoPyStatsLastMainPush = 0;
      const m = "ndy_ig_overlay_mode",
        b = ["detail", "download", "none"],
        g = "detail",
        y = "ndy_ig_er_weights",
        v = Object.freeze(["like", "comment", "repost"]),
        w = Object.freeze({ like: 1, comment: 4, repost: 4 });
      let _ = g,
        x = { ...w };
      function k(e) {
        return b.includes(e);
      }
      function S() {
        return new Promise((e) => {
          chrome.storage.local.get(m, (t = {}) => {
            const r = t[m],
              n = k(r) ? r : g;
            ((_ = n),
              k(t[m]) ||
                chrome.storage.local.set({ [m]: n }, () => {
                  chrome.runtime.lastError;
                }),
              e(n));
          });
        });
      }
      function C(e) {
        const t = { ...w };
        return e && "object" == typeof e
          ? (v.forEach((r) => {
              const n = Number(e[r]);
              Number.isFinite(n) && n >= 0 && (t[r] = n);
            }),
            t)
          : t;
      }
      function E(e) {
        return (
          !(!e || "object" != typeof e) &&
          v.every((t) => {
            const r = Number(e[t]);
            return Number.isFinite(r) && r >= 0;
          })
        );
      }
      function M() {
        return new Promise((e) => {
          chrome.storage.local.get(y, (t = {}) => {
            const r = t[y],
              n = E(r) ? C(r) : { ...w };
            ((x = n),
              E(r) ||
                chrome.storage.local.set({ [y]: n }, () => {
                  chrome.runtime.lastError;
                }),
              e(n));
          });
        });
      }

      // IGblue local patch v21: editable 5-step ER emoji badges.
      const teErEmojiStoreKey = "visual/er_emoji_settings";
      function teErEmojiDefaults() {
        return {
          enabled: true,
          minViews: 10000,
          levels: [
            { enabled: true, pct: 0, emoji: "😭", label: "약함" },
            { enabled: true, pct: 1, emoji: "😐", label: "보통" },
            { enabled: true, pct: 3, emoji: "🙂", label: "괜찮음" },
            { enabled: true, pct: 5, emoji: "❤️", label: "충성도 좋음" },
            { enabled: true, pct: 10, emoji: "🔥", label: "팬덤급" }
          ]
        };
      }
      function teErEmojiSettings() {
        const e = teErEmojiDefaults();
        try {
          const t = JSON.parse(teLocalGet(teErEmojiStoreKey, ""));
          if (t && typeof t === "object") {
            e.enabled = t.enabled !== false;
            const r = Number(t.minViews);
            Number.isFinite(r) && r >= 0 && (e.minViews = r);
            Array.isArray(t.levels) && t.levels.forEach((t, r) => {
              if (!e.levels[r]) return;
              "enabled" in t && (e.levels[r].enabled = t.enabled !== false);
              const n = Number(t.pct);
              Number.isFinite(n) && n >= 0 && (e.levels[r].pct = n);
              null != t.emoji && (e.levels[r].emoji = String(t.emoji).slice(0, 8) || e.levels[r].emoji);
              null != t.label && (e.levels[r].label = String(t.label).slice(0, 24) || e.levels[r].label);
            });
          }
        } catch {}
        return e;
      }
      function teSaveErEmojiSettings(e) {
        const t = teErEmojiDefaults();
        try {
          const r = {
            enabled: e?.enabled !== false,
            minViews: Math.max(0, Number(e?.minViews) || 0),
            levels: (Array.isArray(e?.levels) ? e.levels : t.levels).slice(0, 5).map((e, r) => ({
              enabled: e?.enabled !== false,
              pct: Math.max(0, Number(e?.pct) || 0),
              emoji: String(e?.emoji || t.levels[r]?.emoji || "").slice(0, 8),
              label: String(e?.label || t.levels[r]?.label || "").slice(0, 24)
            }))
          };
          teLocalSet(teErEmojiStoreKey, JSON.stringify(r));
        } catch {}
      }
      function teErPercent(e) {
        if (null != e?.engagement_rate) {
          const t = 100 * Number(e.engagement_rate);
          return Number.isFinite(t) ? t : null;
        }
        const t = Number(e?.play_count);
        if (!Number.isFinite(t) || t <= 0) return null;
        const r = (Number(e?.like_count) || 0) * (Number(x?.like) || 1) +
          (Number(e?.comment_count) || 0) * (Number(x?.comment) || 4) +
          (Number(e?.media_repost_count) || 0) * (Number(x?.repost) || 4);
        return 100 * r / t;
      }
      function teErEmojiForMedia(e) {
        const t = teErEmojiSettings();
        if (!t.enabled) return null;
        const r = Number(e?.play_count);
        if (!Number.isFinite(r) || r < Number(t.minViews || 0)) return null;
        const n = teErPercent(e);
        if (null == n) return null;
        const i = (t.levels || []).filter((e) => e && e.enabled !== false).sort((e, t) => Number(t.pct) - Number(e.pct));
        for (const t of i) {
          if (n >= Number(t.pct || 0)) return { emoji: t.emoji || "", percent: n, label: t.label || "", threshold: Number(t.pct || 0) };
        }
        return null;
      }
      function teCreateErEmojiSettings() {
        const e = teErEmojiSettings();
        const t = document.createElement("div");
        t.style.display = "flex";
        t.style.flexDirection = "column";
        t.style.gap = "10px";
        t.style.borderTop = "1px solid #e5e7eb";
        t.style.paddingTop = "12px";
        const r = document.createElement("div");
        r.textContent = "ER emoji 5-step";
        r.style.fontSize = "14px";
        r.style.fontWeight = "700";
        const n = document.createElement("label");
        n.style.display = "flex";
        n.style.alignItems = "center";
        n.style.gap = "6px";
        n.style.fontSize = "12px";
        const i = document.createElement("input");
        i.type = "checkbox";
        i.checked = e.enabled !== false;
        n.appendChild(i);
        n.appendChild(document.createTextNode("Show ER emoji badge"));
        n.appendChild(teMakeDefaultButton("ER emoji", teErEmojiStoreKey));
        const s = document.createElement("label");
        s.style.display = "flex";
        s.style.alignItems = "center";
        s.style.gap = "6px";
        s.style.fontSize = "12px";
        s.textContent = "Min views";
        const o = document.createElement("input");
        o.type = "number";
        o.min = "0";
        o.step = "1000";
        o.value = e.minViews;
        o.style.width = "90px";
        o.style.textAlign = "right";
        o.style.padding = "4px 6px";
        o.style.border = "1px solid #ccc";
        o.style.borderRadius = "4px";
        s.appendChild(o);
        const a = document.createElement("div");
        a.style.display = "grid";
        a.style.gridTemplateColumns = "42px 74px 52px 1fr";
        a.style.gap = "5px";
        a.style.alignItems = "center";
        a.style.fontSize = "12px";
        ["Use", "ER %", "Emoji", "Label"].forEach((e) => {
          const t = document.createElement("div");
          t.textContent = e;
          t.style.fontWeight = "700";
          a.appendChild(t);
        });
        const l = [];
        (e.levels || teErEmojiDefaults().levels).slice(0, 5).forEach((e) => {
          const t = document.createElement("input");
          t.type = "checkbox";
          t.checked = e.enabled !== false;
          const r = document.createElement("input");
          r.type = "number";
          r.min = "0";
          r.step = "0.1";
          r.value = e.pct;
          r.style.width = "62px";
          r.style.textAlign = "right";
          r.style.padding = "4px 5px";
          const n = document.createElement("input");
          n.type = "text";
          n.value = e.emoji || "";
          n.style.width = "42px";
          n.style.textAlign = "center";
          n.style.padding = "4px 5px";
          const i = document.createElement("input");
          i.type = "text";
          i.value = e.label || "";
          i.style.padding = "4px 5px";
          [r, n, i].forEach((e) => { e.style.border = "1px solid #ccc"; e.style.borderRadius = "4px"; });
          a.appendChild(t);
          a.appendChild(r);
          a.appendChild(n);
          a.appendChild(i);
          l.push({ enabled: t, pct: r, emoji: n, label: i });
        });
        const c = document.createElement("div");
        c.style.fontSize = "12px";
        c.style.padding = "8px";
        c.style.borderRadius = "8px";
        c.style.background = "#f4faff";
        c.style.border = "1px solid #dbeafe";
        const u = () => {
          const e = l.map((e) => ({ enabled: e.enabled.checked, pct: Number(e.pct.value) || 0, emoji: e.emoji.value || "", label: e.label.value || "" }))
            .filter((e) => e.enabled)
            .sort((e, t) => Number(t.pct) - Number(e.pct));
          const t = e[0] || { emoji: "🔥", pct: 10, label: "팬덤급" };
          c.textContent = `Sample: ${t.emoji} ${Number(t.pct).toFixed(1).replace(/\.0$/, "")}% · ${t.label}`;
        };
        [i, o, ...l.flatMap((e) => [e.enabled, e.pct, e.emoji, e.label])].forEach((e) => e.addEventListener("input", u));
        u();
        t.appendChild(r);
        t.appendChild(n);
        t.appendChild(s);
        t.appendChild(a);
        t.appendChild(c);
        return {
          element: t,
          getValues: () => ({
            enabled: i.checked,
            minViews: Number(o.value) || 0,
            levels: l.map((e) => ({ enabled: e.enabled.checked, pct: Number(e.pct.value) || 0, emoji: e.emoji.value || "", label: e.label.value || "" }))
          })
        };
      }

      // IGblue local patch v27: simple metric underline bars + configurable date borders.
      const teMetricBarStoreKey = "visual/metric_bar_settings";
      const teDateBorderStoreKey = "visual/date_border_settings";
      const teRankBadgeStoreKey = "visual/rank_badge_settings";
      const teActiveDownloadStoreKey = "visual/active_download_settings";
      const teThumbWidthStoreKey = "visual/thumb_min_width";
      function teThumbWidthDefault() { return 200; }
      function teThumbWidthValue() {
        const e = Number(teLocalGet(teThumbWidthStoreKey, teThumbWidthDefault()));
        return Math.max(90, Math.min(420, Number.isFinite(e) ? Math.floor(e) : teThumbWidthDefault()));
      }
      function teApplyThumbWidth(e) {
        const t = Math.max(90, Math.min(420, Number(e) || teThumbWidthDefault()));
        document.documentElement.style.setProperty("--igblue-thumb-min-width", `${Math.floor(t)}px`);
        teLocalSet(teThumbWidthStoreKey, Math.floor(t));
        return Math.floor(t);
      }
      function teMetricBarDefaults() {
        return {
          enabled: true,
          barHeight: 3,
          itemGap: 4,
          textBarGap: 2,
          itemHeight: 18,
          items: {
            views: { enabled: true, side: true, main: true, label: "Views", max: 100000000, color: "#0d6efd" },
            likes: { enabled: true, side: true, main: true, label: "Likes", max: 1000000, color: "#ef4444" },
            comments: { enabled: true, side: true, main: true, label: "Comments", max: 10000, color: "#22c55e" },
            shares: { enabled: true, side: true, main: true, label: "Reposts", max: 100000, color: "#f97316" }
          }
        };
      }
      function teSafeHexColor(e, t) {
        const r = String(e || "").trim();
        return /^#[0-9a-fA-F]{6}$/.test(r) ? r : t;
      }
      function teHexToRgba(e, a, fallback) {
        const h = teSafeHexColor(e, fallback || "#ff2d2d").replace("#", "");
        const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
        const o = Math.max(0, Math.min(1, Number(a)));
        return `rgba(${r}, ${g}, ${b}, ${Number.isFinite(o) ? o : 1})`;
      }
      function teSampleGradientBackground() {
        return "linear-gradient(135deg,#f8fafc 0%,#facc15 18%,#22c55e 42%,#2563eb 68%,#111827 100%)";
      }
      function teSampleCardWidthValue(e) {
        const t = Number(e);
        return Math.max(160, Math.min(420, Number.isFinite(t) ? Math.floor(t) : teThumbWidthValue()));
      }
      function teSampleMetricIcon(e) {
        const common = 'width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"';
        if (e === "likes") return `<svg ${common}><title>Likes</title><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>`;
        if (e === "comments") return `<svg ${common}><title>Comments</title><path d="M3 11.5a8.38 8.38 0 0 0 .9 3.8 8.5 8.5 0 0 0 7.6 4.7 8.38 8.38 0 0 0 3.8-.9L21 21l-1.9-5.7a8.38 8.38 0 0 0 .9-3.8 8.5 8.5 0 0 0-4.7-7.6 8.38 8.38 0 0 0-3.8-.9h-.5a8.48 8.48 0 0 0-8 8v.5z"></path></svg>`;
        if (e === "shares") return `<svg width="16" height="16" viewBox="0 0 24 24"><title>Repost</title><path d="M4.51617 6.9986C6.13179 4.58593 8.88099 2.99979 11.9995 2.99979C15.7267 2.99979 18.9259 5.26459 20.2927 8.49676C20.5079 9.00543 21.0946 9.24341 21.6033 9.0283C22.1119 8.81318 22.3499 8.22644 22.1348 7.71777C20.466 3.7716 16.5582 0.999786 11.9995 0.999786C8.27776 0.999786 4.9897 2.84823 2.99988 5.67416V2.9986C2.99988 2.44631 2.55216 1.9986 1.99988 1.9986C1.44759 1.9986 0.999878 2.44631 0.999878 2.9986V7.9986C0.999878 8.55088 1.44759 8.9986 1.99988 8.9986H6.99988C7.55216 8.9986 7.99988 8.55088 7.99988 7.9986C7.99988 7.44631 7.55216 6.9986 6.99988 6.9986H4.51617Z" fill="currentColor"></path><path d="M2.39572 14.9713C2.90439 14.7562 3.49113 14.9942 3.70625 15.5029C5.07309 18.735 8.27228 20.9998 11.9995 20.9998C15.118 20.9998 17.8672 19.4137 19.4828 17.001H16.9991C16.4468 17.001 15.9991 16.5533 15.9991 16.001C15.9991 15.4487 16.4468 15.001 16.9991 15.001H21.9991C22.5514 15.001 22.9991 15.4487 22.9991 16.001V21.001C22.9991 21.5533 22.5514 22.001 21.9991 22.001C21.4468 22.001 20.9991 21.5533 20.9991 21.001V18.3255C19.0093 21.1514 15.7212 22.9998 11.9995 22.9998C7.44077 22.9998 3.53298 20.228 1.86419 16.2818C1.64908 15.7732 1.88705 15.1864 2.39572 14.9713Z" fill="currentColor"></path></svg>`;
        if (e === "date") return `<svg ${common}><title>Date</title><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>`;
        if (e === "views") return `<svg ${common}><title>Views</title><path d="M23.441 11.819C23.413 11.74 20.542 4 12 4S.587 11.74.559 11.819a1 1 0 0 0 1.881.677 10.282 10.282 0 0 1 19.12 0 1 1 0 0 0 1.881-.677Zm-7.124 2.368a3.359 3.359 0 0 1-1.54-.1 3.56 3.56 0 0 1-2.365-2.362 3.35 3.35 0 0 1-.103-1.542.99.99 0 0 0-1.134-1.107 5.427 5.427 0 0 0-3.733 2.34 5.5 5.5 0 0 0 8.446 6.97 5.402 5.402 0 0 0 1.536-3.09.983.983 0 0 0-1.107-1.109Z" fill-rule="evenodd"></path></svg>`;
        return `<span></span>`;
      }
      function teSampleCardHtml(inner, width) {
        const w = teSampleCardWidthValue(width);
        return `<div style="font-weight:700;margin-bottom:7px">Sample thumbnail preview</div><div style="width:${w}px;max-width:100%;aspect-ratio:1/1.2;border-radius:12px;overflow:hidden;background:${teSampleGradientBackground()};color:#fff;position:relative;box-shadow:0 4px 16px rgba(15,23,42,.18);margin-bottom:6px"><div class="overlay">${inner}</div></div>`;
      }
      function teMetricBarSettings() {
        const e = teMetricBarDefaults();
        try {
          const t = JSON.parse(teLocalGet(teMetricBarStoreKey, ""));
          if (t && typeof t === "object") {
            "enabled" in t && (e.enabled = t.enabled !== false);
            ["barHeight", "itemGap", "textBarGap", "itemHeight"].forEach((r) => {
              const n = Number(t[r]);
              Number.isFinite(n) && n >= 0 && (e[r] = n);
            });
            if (t.items && typeof t.items === "object") {
              Object.keys(e.items).forEach((r) => {
                const n = t.items[r];
                if (!n || typeof n !== "object") return;
                "enabled" in n && (e.items[r].enabled = n.enabled !== false);
                "side" in n && (e.items[r].side = n.side !== false);
                e.items[r].main = e.items[r].side;
                const i = Number(n.max);
                Number.isFinite(i) && i >= 1 && (e.items[r].max = Math.floor(i));
                e.items[r].color = teSafeHexColor(n.color, e.items[r].color);
                null != n.label && (e.items[r].label = String(n.label).slice(0, 20));
              });
            }
          }
        } catch {}
        e.barHeight = Math.max(1, Math.min(12, Number(e.barHeight) || 3));
        e.itemGap = Math.max(0, Math.min(20, Number(e.itemGap) || 4));
        e.textBarGap = Math.max(0, Math.min(20, Number(e.textBarGap) || 2));
        e.itemHeight = Math.max(0, Math.min(60, Number(e.itemHeight) || 18));
        return e;
      }
      function teSaveMetricBarSettings(e) {
        const t = teMetricBarDefaults(), r = { enabled: e?.enabled !== false, barHeight: 3, itemGap: 4, textBarGap: 2, itemHeight: 18, items: {} };
        r.barHeight = Math.max(1, Math.min(12, Math.floor(Number(e?.barHeight) || t.barHeight)));
        r.itemGap = Math.max(0, Math.min(20, Math.floor(Number(e?.itemGap) || t.itemGap)));
        r.textBarGap = Math.max(0, Math.min(20, Math.floor(Number(e?.textBarGap) || t.textBarGap)));
        r.itemHeight = Math.max(0, Math.min(60, Math.floor(Number(e?.itemHeight) || t.itemHeight)));
        Object.keys(t.items).forEach((n) => {
          const i = e?.items?.[n] || t.items[n];
          r.items[n] = {
            enabled: i?.enabled !== false,
            side: i?.side !== false,
            main: i?.side !== false,
            label: String(i?.label || t.items[n].label).slice(0, 20),
            max: Math.max(1, Math.floor(Number(i?.max) || t.items[n].max)),
            color: teSafeHexColor(i?.color, t.items[n].color)
          };
        });
        teLocalSet(teMetricBarStoreKey, JSON.stringify(r));
      }
      function teMetricBarPercent(e, t) {
        const r = Math.max(0, Number(e) || 0), n = Math.max(1, Number(t) || 1);
        if (r <= 0) return 0;
        return Math.max(0, Math.min(100, (r / n) * 100));
      }
      function teClearMetricBar(e) {
        if (!e) return;
        e.classList.remove("igblue-metric-bar-on");
        ["--igblue-stat-bar-color", "--igblue-stat-bar-width", "--igblue-stat-bar-height", "--igblue-stat-bar-gap", "--igblue-stat-item-gap", "--igblue-stat-item-height"].forEach((t) => e.style.removeProperty(t));
      }
      function teApplyMetricBarToStat(e, t, r, n) {
        if (!e) return;
        teClearMetricBar(e);
        if (!t.enabled || !t.side || !Number.isFinite(Number(r)) || Number(r) <= 0) return;
        const i = teMetricBarPercent(r, t.max);
        e.style.setProperty("--igblue-stat-bar-color", teSafeHexColor(t.color, "#0d6efd"));
        e.style.setProperty("--igblue-stat-bar-width", `${i.toFixed(2)}%`);
        e.style.setProperty("--igblue-stat-bar-height", `${Math.max(1, Math.min(12, Number(n.barHeight) || 3))}px`);
        e.style.setProperty("--igblue-stat-bar-gap", `${Math.max(0, Math.min(20, Number(n.textBarGap) || 2))}px`);
        e.style.setProperty("--igblue-stat-item-gap", `${Math.max(0, Math.min(20, Number(n.itemGap) || 4))}px`);
        e.style.setProperty("--igblue-stat-item-height", `${Math.max(0, Math.min(60, Number(n.itemHeight) || 18))}px`);
        e.classList.add("igblue-metric-bar-on");
        e.title = `${t.label || "metric"}: ${Number(r).toLocaleString()} / max ${Number(t.max || 0).toLocaleString()}`;
      }
      function teApplyMetricBars(e, t) {
        if (!e) return;
        const r = teMetricBarSettings(), n = r.items || {}, overlay = e[0]?.closest?.(".overlay");
        overlay && overlay.style.setProperty("--igblue-overlay-item-gap", `${Math.max(0, Math.min(20, Number(r.itemGap) || 4))}px`);
        [e[1], e[2], e[3], e[5]].forEach(teClearMetricBar);
        if (!r.enabled) return;
        teApplyMetricBarToStat(e[5], n.views || {}, teMetricNumber(t?.play_count), r);
        teApplyMetricBarToStat(e[1], n.likes || {}, teMetricNumber(t?.like_count), r);
        teApplyMetricBarToStat(e[2], n.comments || {}, teMetricNumber(t?.comment_count), r);
        teApplyMetricBarToStat(e[3], n.shares || {}, teMetricNumber(t?.media_repost_count), r);
      }
      function teDateBorderDefaults() {
        return {
          enabled: true,
          borderWidth: 2,
          levels: {
            today: { enabled: true, side: true, main: true, days: 0, label: "오늘", color: "#ff2d2d" },
            week: { enabled: true, side: true, main: true, days: 7, label: "1주", color: "#22c55e" },
            month: { enabled: true, side: true, main: true, days: 31, label: "1개월", color: "#eab308" },
            quarter: { enabled: false, side: true, main: true, days: 90, label: "3개월", color: "#9ca3af" }
          }
        };
      }
      function teDateBorderSettings() {
        const e = teDateBorderDefaults();
        try {
          const t = JSON.parse(teLocalGet(teDateBorderStoreKey, ""));
          if (t && typeof t === "object") {
            "enabled" in t && (e.enabled = t.enabled !== false);
            const r = Number(t.borderWidth);
            Number.isFinite(r) && r >= 1 && (e.borderWidth = r);
            if (t.levels && typeof t.levels === "object") {
              Object.keys(e.levels).forEach((r) => {
                const n = t.levels[r];
                if (!n || typeof n !== "object") return;
                "enabled" in n && (e.levels[r].enabled = n.enabled !== false);
                "side" in n && (e.levels[r].side = n.side !== false);
                e.levels[r].main = e.levels[r].side;
                e.levels[r].color = teSafeHexColor(n.color, e.levels[r].color);
              });
            }
          }
        } catch {}
        e.borderWidth = Math.max(1, Math.min(9, Number(e.borderWidth) || 2));
        return e;
      }
      function teSaveDateBorderSettings(e) {
        const t = teDateBorderDefaults(), r = { enabled: e?.enabled !== false, borderWidth: Math.max(1, Math.min(9, Math.floor(Number(e?.borderWidth) || t.borderWidth))), levels: {} };
        Object.keys(t.levels).forEach((n) => {
          const i = e?.levels?.[n] || t.levels[n];
          r.levels[n] = {
            enabled: i?.enabled !== false,
            side: i?.side !== false,
            main: i?.side !== false,
            color: teSafeHexColor(i?.color, t.levels[n].color)
          };
        });
        teLocalSet(teDateBorderStoreKey, JSON.stringify(r));
      }
      function teRankBadgeDefaults() {
        return { enabled: true, side: true, main: true, borderColor: "#ff6666", borderWidth: 4, fontSize: 11, bold: false, borderOpacity: 0.55, textOpacity: 0.82 };
      }
      function teRankBadgeSettings() {
        const e = teRankBadgeDefaults();
        try {
          const t = JSON.parse(teLocalGet(teRankBadgeStoreKey, ""));
          if (t && typeof t === "object") {
            "enabled" in t && (e.enabled = t.enabled !== false);
            "side" in t && (e.side = t.side !== false);
            e.main = e.side;
            e.borderColor = teSafeHexColor(t.borderColor, e.borderColor);
            const r = Number(t.borderWidth); Number.isFinite(r) && (e.borderWidth = r);
            const n = Number(t.fontSize); Number.isFinite(n) && (e.fontSize = n);
            "bold" in t && (e.bold = t.bold === true);
            const oldOpacity = Number(t.opacity);
            const bo = Number(t.borderOpacity); Number.isFinite(bo) ? (e.borderOpacity = bo) : Number.isFinite(oldOpacity) && (e.borderOpacity = oldOpacity);
            const to = Number(t.textOpacity); Number.isFinite(to) ? (e.textOpacity = to) : Number.isFinite(oldOpacity) && (e.textOpacity = oldOpacity);
          }
        } catch {}
        e.borderWidth = Math.max(0, Math.min(8, Math.floor(Number(e.borderWidth) || 0)));
        e.fontSize = Math.max(8, Math.min(24, Math.floor(Number(e.fontSize) || 11)));
        { const v = Number(e.borderOpacity); e.borderOpacity = Math.max(0, Math.min(1, Number.isFinite(v) ? v : 0.55)); }
        { const v = Number(e.textOpacity); e.textOpacity = Math.max(0, Math.min(1, Number.isFinite(v) ? v : 0.82)); }
        return e;
      }
      function teSaveRankBadgeSettings(e) {
        const t = teRankBadgeDefaults();
        const r = {
          enabled: e?.enabled !== false,
          side: e?.side !== false,
          main: e?.side !== false,
          borderColor: teSafeHexColor(e?.borderColor, t.borderColor),
          borderWidth: Math.max(0, Math.min(8, Math.floor(Number(e?.borderWidth) || t.borderWidth))),
          fontSize: Math.max(8, Math.min(24, Math.floor(Number(e?.fontSize) || t.fontSize))),
          bold: e?.bold === true,
          borderOpacity: Math.max(0, Math.min(1, Number.isFinite(Number(e?.borderOpacity)) ? Number(e?.borderOpacity) : t.borderOpacity)),
          textOpacity: Math.max(0, Math.min(1, Number.isFinite(Number(e?.textOpacity)) ? Number(e?.textOpacity) : t.textOpacity))
        };
        teLocalSet(teRankBadgeStoreKey, JSON.stringify(r));
      }
      function teApplyRankBadgeVars(e, t) {
        if (!e) return;
        const boRaw = Number(t.borderOpacity), toRaw = Number(t.textOpacity);
        const bo = Math.max(0, Math.min(1, Number.isFinite(boRaw) ? boRaw : 0.55));
        const to = Math.max(0, Math.min(1, Number.isFinite(toRaw) ? toRaw : 0.82));
        e.style.setProperty("--igblue-rank-border-color", teSafeHexColor(t.borderColor, "#ff6666"));
        e.style.setProperty("--igblue-rank-border-rgba", teHexToRgba(t.borderColor, bo, "#ff6666"));
        e.style.setProperty("--igblue-rank-border-width", `${Math.max(0, Math.min(8, Number(t.borderWidth) || 0))}px`);
        e.style.setProperty("--igblue-rank-font-size", `${Math.max(8, Math.min(24, Number(t.fontSize) || 11))}px`);
        e.style.setProperty("--igblue-rank-font-weight", t.bold ? "800" : "500");
        e.style.setProperty("--igblue-rank-text-opacity", String(to));
        e.style.setProperty("--igblue-rank-opacity", String(to));
      }
      function teClearRankBadgeVars(e) {
        if (!e) return;
        ["--igblue-rank-border-color", "--igblue-rank-border-rgba", "--igblue-rank-border-width", "--igblue-rank-font-size", "--igblue-rank-font-weight", "--igblue-rank-opacity", "--igblue-rank-text-opacity"].forEach((t) => e.style.removeProperty(t));
      }
      function teActiveDownloadDefaults() {
        // v179a37: 기본은 인스타 화면을 덜 가리도록 확대/강조 OFF. 자동 클릭 좌표는 DOM rect payload로 유지한다.
        return { enabled: false, color: "#ff0000", borderWidth: 4, radius: 3, glow: false, glowOpacity: 0.45, scale: 1.0 };
      }
      function teActiveDownloadSettings() {
        const e = teActiveDownloadDefaults();
        try {
          const t = JSON.parse(teLocalGet(teActiveDownloadStoreKey, ""));
          if (t && typeof t === "object") {
            "enabled" in t && (e.enabled = t.enabled !== false);
            e.color = teSafeHexColor(t.color, e.color);
            const bw = Number(t.borderWidth); Number.isFinite(bw) && (e.borderWidth = bw);
            const rr = Number(t.radius); Number.isFinite(rr) && (e.radius = rr);
            "glow" in t && (e.glow = t.glow !== false);
            const go = Number(t.glowOpacity); Number.isFinite(go) && (e.glowOpacity = go);
            const sc = Number(t.scale); Number.isFinite(sc) && (e.scale = sc);
          }
        } catch {}
        e.borderWidth = Math.max(0, Math.min(12, Math.floor(Number(e.borderWidth) || 0)));
        e.radius = Math.max(0, Math.min(999, Math.floor(Number(e.radius) || 0)));
        e.glowOpacity = Math.max(0, Math.min(1, Number(e.glowOpacity) || 0));
        e.scale = Math.max(1, Math.min(1.6, Number(e.scale) || 1));
        return e;
      }
      function teSaveActiveDownloadSettings(e) {
        const t = teActiveDownloadDefaults();
        const r = {
          enabled: e?.enabled !== false,
          color: teSafeHexColor(e?.color, t.color),
          borderWidth: Math.max(0, Math.min(12, Math.floor(Number(e?.borderWidth) || t.borderWidth))),
          radius: Math.max(0, Math.min(999, Math.floor(Number(e?.radius) || t.radius))),
          glow: e?.glow !== false,
          glowOpacity: Math.max(0, Math.min(1, Number.isFinite(Number(e?.glowOpacity)) ? Number(e?.glowOpacity) : t.glowOpacity)),
          scale: Math.max(1, Math.min(1.6, Number.isFinite(Number(e?.scale)) ? Number(e?.scale) : t.scale))
        };
        teLocalSet(teActiveDownloadStoreKey, JSON.stringify(r));
      }
      function teLocalRemove(e) {
        try { localStorage.removeItem(teLocalKey(e)); } catch {}
      }
      function teResetSettingsSection(e, t) {
        if (!confirm(`${e} 기본값으로 되돌릴까요?`)) return;
        (Array.isArray(t) ? t : [t]).forEach(teLocalRemove);
        n(`${e} 기본값 적용`);
        const r = document.getElementById("settingsModal");
        r && r.remove();
        Array.isArray(oe) && ee(oe);
        setTimeout(T, 0);
      }
      function teMakeDefaultButton(e, t) {
        const r = document.createElement("button");
        r.type = "button";
        r.textContent = "기본값";
        r.style.padding = "3px 8px";
        r.style.border = "1px solid #cfd8e3";
        r.style.borderRadius = "999px";
        r.style.background = "#f4faff";
        r.style.cursor = "pointer";
        r.style.fontSize = "12px";
        r.addEventListener("click", () => teResetSettingsSection(e, t));
        return r;
      }
      function teCreateSettingsTabs(e) {
        const t = document.createElement("div"), r = document.createElement("div"), n = document.createElement("div");
        t.style.display = "flex"; t.style.flexDirection = "column"; t.style.gap = "12px";
        r.style.display = "flex"; r.style.flexWrap = "wrap"; r.style.gap = "6px"; r.style.borderBottom = "1px solid #e5e7eb"; r.style.padding = "8px 0 7px"; r.style.position = "sticky"; r.style.top = "0"; r.style.background = "#fff"; r.style.zIndex = "3";
        n.style.display = "flex"; n.style.flexDirection = "column"; n.style.gap = "12px";
        e.forEach((e, i) => {
          const s = document.createElement("button");
          s.type = "button"; s.textContent = e.title;
          s.style.padding = "6px 10px"; s.style.border = "1px solid #cfd8e3"; s.style.borderRadius = "999px"; s.style.background = i ? "#fff" : "#e7f1ff"; s.style.cursor = "pointer";
          const o = document.createElement("div");
          o.style.display = i ? "none" : "flex"; o.style.flexDirection = "column"; o.style.gap = "12px";
          o.appendChild(e.element);
          s.addEventListener("click", () => {
            Array.from(r.children).forEach((e) => e.style.background = "#fff");
            Array.from(n.children).forEach((e) => e.style.display = "none");
            s.style.background = "#e7f1ff"; o.style.display = "flex";
          });
          r.appendChild(s); n.appendChild(o);
        });
        t.appendChild(r); t.appendChild(n);
        return { element: t };
      }

      function teDateLevelByAge(e, t) {
        if (e === 0) return t.today;
        if (e >= 1 && e <= 7) return t.week;
        if (e >= 8 && e <= 31) return t.month;
        if (e >= 32 && e <= 90) return t.quarter;
        return null;
      }
      function teApplyDateFreshClass(e, t) {
        if (!e) return;
        e.classList.remove("igblue-date-today", "igblue-date-week", "igblue-date-month", "igblue-date-quarter", "igblue-date-border");
        e.style.removeProperty("--igblue-date-color");
        e.style.removeProperty("--igblue-date-border-width");
        const r = Number(t?.created_at || 0);
        if (!Number.isFinite(r) || r <= 0) return;
        const n = teDateBorderSettings();
        if (!n.enabled) return;
        const i = new Date(), s = new Date(1e3 * r);
        i.setHours(0, 0, 0, 0);
        s.setHours(0, 0, 0, 0);
        const o = Math.floor((i.getTime() - s.getTime()) / 86400000), a = teDateLevelByAge(o, n.levels || {});
        if (!a || !a.enabled || !a.side) return;
        e.classList.add("igblue-date-border");
        e.style.setProperty("--igblue-date-color", teSafeHexColor(a.color, "#22c55e"));
        e.style.setProperty("--igblue-date-border-width", `${Math.max(1, Math.min(9, Number(n.borderWidth) || 2))}px`);
      }
      // Old top-card view highlight is intentionally disabled in v27.
      function teViewVisualSettings() { return { enabled: false, applySide: false, applyMain: false, border: false, bar: false, thumbWidth: teThumbWidthValue() }; }
      function teViewLevelForMedia() { return null; }
      function teViewBarPercent() { return 0; }
      function teEnsureViewBar(e) {
        let t = e?.querySelector?.(".igblue-view-bar-wrap");
        if (!t && e) {
          t = document.createElement("div");
          t.className = "igblue-view-bar-wrap";
          const r = document.createElement("div");
          r.className = "igblue-view-bar";
          t.appendChild(r);
          e.appendChild(t);
        }
        return t;
      }
      function teApplyViewVisual(e) {
        if (!e) return;
        e.classList.remove("igblue-view-border", "igblue-view-bar-on");
        ["--igblue-view-color", "--igblue-view-glow", "--igblue-view-bar-width", "--igblue-view-bar-height", "--igblue-view-border-width"].forEach((t) => e.style.removeProperty(t));
      }
      function teSaveViewVisualSettings(e) {
        teSaveMetricBarSettings(e?.metric || {});
        teSaveDateBorderSettings(e?.date || {});
      }
      function teCreateMetricBarSettings() {
        const e = teMetricBarSettings(), t = document.createElement("div");
        t.style.display = "flex"; t.style.flexDirection = "column"; t.style.gap = "10px";
        const r = document.createElement("div");
        r.textContent = "Metric underline bars"; r.style.fontSize = "14px"; r.style.fontWeight = "700";
        const n = document.createElement("div");
        n.style.display = "grid"; n.style.gridTemplateColumns = "repeat(auto-fit, minmax(95px, 1fr))"; n.style.gap = "8px";
        const mkCheck = (label, checked) => { const e = document.createElement("label"), t = document.createElement("input"); e.style.fontSize = "12px"; e.style.display = "flex"; e.style.gap = "5px"; e.style.alignItems = "center"; t.type = "checkbox"; t.checked = checked !== false; e.appendChild(t); e.appendChild(document.createTextNode(label)); return { label: e, input: t }; };
        const enabled = mkCheck("사용", e.enabled);
        n.appendChild(enabled.label);
        n.appendChild(teMakeDefaultButton("Metric bars", [teMetricBarStoreKey, teThumbWidthStoreKey]));
        const o = document.createElement("div");
        o.style.display = "grid"; o.style.gridTemplateColumns = "80px 80px 92px 98px 92px"; o.style.gap = "6px"; o.style.alignItems = "center"; o.style.fontSize = "12px";
        const mkNum = (label, value, min, max) => { const e = document.createElement("label"), t = document.createElement("input"); e.textContent = label; e.style.display = "flex"; e.style.flexDirection = "column"; e.style.gap = "3px"; t.type = "number"; t.min = String(min); t.max = String(max); t.step = "1"; t.value = value; t.style.width = label.includes("최대") ? "98px" : "70px"; t.style.textAlign = "right"; t.style.padding = "4px 5px"; t.style.border = "1px solid #ccc"; t.style.borderRadius = "4px"; e.appendChild(t); return { label: e, input: t }; };
        const barHeight = mkNum("막대 px", e.barHeight, 1, 12), itemGap = mkNum("항목간 px", e.itemGap, 0, 20), textBarGap = mkNum("글자-바 px", e.textBarGap, 0, 20), itemHeight = mkNum("항목높이 px", e.itemHeight, 0, 60), thumbWidth = mkNum("썸네일 px", teThumbWidthValue(), 90, 420);
        [barHeight, itemGap, textBarGap, itemHeight, thumbWidth].forEach((e) => o.appendChild(e.label));
        const a = document.createElement("div");
        a.style.display = "grid"; a.style.gridTemplateColumns = "38px 38px 42px 82px 112px 70px"; a.style.gap = "5px"; a.style.alignItems = "center"; a.style.fontSize = "12px";
        ["Use", "Side", "Main", "색상", "최대값", "항목"].forEach((e) => { const t = document.createElement("div"); t.textContent = e; t.style.fontWeight = "700"; a.appendChild(t); });
        const rows = {}, labels = { views: "Views", likes: "Likes", comments: "Comments", shares: "Repost" };
        Object.keys(labels).forEach((t) => {
          const r = e.items[t];
          const n = document.createElement("input"); n.type = "checkbox"; n.checked = r.enabled !== false;
          const i = document.createElement("input"); i.type = "checkbox"; i.checked = r.side !== false;
          const s = document.createElement("input"); s.type = "checkbox"; s.checked = r.main === true;
          const o = document.createElement("input"); o.type = "color"; o.value = teSafeHexColor(r.color, "#0d6efd"); o.style.width = "72px";
          const l = document.createElement("input"); l.type = "number"; l.min = "1"; l.step = "100"; l.value = r.max; l.style.width = "104px"; l.style.textAlign = "right"; l.style.padding = "4px 5px";
          const c = document.createElement("div"); c.textContent = labels[t]; c.style.fontWeight = "600";
          [l].forEach((e) => { e.style.border = "1px solid #ccc"; e.style.borderRadius = "4px"; });
          a.appendChild(n); a.appendChild(i); a.appendChild(s); a.appendChild(o); a.appendChild(l); a.appendChild(c);
          rows[t] = { enabled: n, side: i, main: s, color: o, max: l, label: labels[t] };
        });
        const sample = document.createElement("div"); sample.style.border = "1px solid #dbeafe"; sample.style.borderRadius = "10px"; sample.style.background = "#f4faff"; sample.style.padding = "8px"; sample.style.fontSize = "12px";
        const updateSample = () => {
          const bh = Math.max(1, Math.min(12, Number(barHeight.input.value) || 3)), gap = Math.max(0, Math.min(20, Number(textBarGap.input.value) || 2)), itemH = Math.max(0, Math.min(60, Number(itemHeight.input.value) || 18));
          const sampleValues = { views: 72000000, likes: 380000, comments: 1800, shares: 2500 };
          const rowHtml = Object.keys(rows).map((k) => {
            const row = rows[k], max = Math.max(1, Number(row.max.value) || 1), val = sampleValues[k] || Math.round(max * .4);
            const pct = Math.max(0, Math.min(100, val / max * 100));
            const disabled = !row.enabled.checked || !row.side.checked;
            return `<div class="stat igblue-metric-bar-on" style="--igblue-stat-bar-height:${bh}px;--igblue-stat-bar-gap:${gap}px;--igblue-stat-item-height:${itemH}px;--igblue-stat-item-gap:${Number(itemGap.input.value)||0}px;--igblue-stat-bar-color:${row.color.value};--igblue-stat-bar-width:${disabled ? 0 : pct}%;opacity:${disabled ? .38 : 1}">${teSampleMetricIcon(k)}<span>${val.toLocaleString()}</span></div>`;
          }).join("\n");
          sample.innerHTML = teSampleCardHtml(rowHtml, thumbWidth.input.value);
        };
        [enabled.input, barHeight.input, itemGap.input, textBarGap.input, itemHeight.input, thumbWidth.input, ...Object.values(rows).flatMap((e) => [e.enabled, e.side, e.main, e.color, e.max])].forEach((e) => e.addEventListener("input", updateSample));
        const applyThumbNow = () => { thumbWidth.input.value = String(teApplyThumbWidth(thumbWidth.input.value)); };
        thumbWidth.input.addEventListener("blur", applyThumbNow); thumbWidth.input.addEventListener("change", applyThumbNow);
        updateSample();
        t.appendChild(r); t.appendChild(n); t.appendChild(o); t.appendChild(a); t.appendChild(sample);
        return { element: t, getValues: () => ({ enabled: enabled.input.checked, barHeight: Number(barHeight.input.value) || 3, itemGap: Number(itemGap.input.value) || 0, textBarGap: Number(textBarGap.input.value) || 0, itemHeight: Number(itemHeight.input.value) || 0, thumbWidth: Number(thumbWidth.input.value) || teThumbWidthDefault(), items: Object.fromEntries(Object.entries(rows).map(([k, v]) => [k, { enabled: v.enabled.checked, side: v.side.checked, main: v.main.checked, color: v.color.value, max: Number(v.max.value) || 1, label: v.label }])) }) };
      }
      function teCreateDateBorderSettings() {
        const e = teDateBorderSettings(), t = document.createElement("div");
        t.style.display = "flex"; t.style.flexDirection = "column"; t.style.gap = "10px"; t.style.borderTop = "1px solid #e5e7eb"; t.style.paddingTop = "12px";
        const r = document.createElement("div"); r.textContent = "Date border"; r.style.fontSize = "14px"; r.style.fontWeight = "700";
        const n = document.createElement("div"); n.style.display = "grid"; n.style.gridTemplateColumns = "repeat(auto-fit, minmax(95px, 1fr))"; n.style.gap = "8px";
        const mkCheck = (label, checked) => { const e = document.createElement("label"), t = document.createElement("input"); e.style.fontSize = "12px"; e.style.display = "flex"; e.style.gap = "5px"; e.style.alignItems = "center"; t.type = "checkbox"; t.checked = checked !== false; e.appendChild(t); e.appendChild(document.createTextNode(label)); return { label: e, input: t }; };
        const enabled = mkCheck("사용", e.enabled); n.appendChild(enabled.label); n.appendChild(teMakeDefaultButton("Date border", teDateBorderStoreKey));
        const borderWidth = document.createElement("input"); borderWidth.type = "number"; borderWidth.min = "1"; borderWidth.max = "9"; borderWidth.value = e.borderWidth; borderWidth.style.width = "70px"; borderWidth.style.textAlign = "right"; borderWidth.style.padding = "4px 5px"; borderWidth.style.border = "1px solid #ccc"; borderWidth.style.borderRadius = "4px";
        const bwLabel = document.createElement("label"); bwLabel.textContent = "테두리 px"; bwLabel.style.fontSize = "12px"; bwLabel.style.display = "flex"; bwLabel.style.flexDirection = "column"; bwLabel.style.gap = "3px"; bwLabel.appendChild(borderWidth); n.appendChild(bwLabel);
        const a = document.createElement("div"); a.style.display = "grid"; a.style.gridTemplateColumns = "38px 38px 42px 82px 1fr"; a.style.gap = "5px"; a.style.alignItems = "center"; a.style.fontSize = "12px";
        ["Use", "Side", "Main", "색상", "기간"].forEach((e) => { const t = document.createElement("div"); t.textContent = e; t.style.fontWeight = "700"; a.appendChild(t); });
        const labels = { today: "오늘", week: "1주", month: "1개월", quarter: "3개월" }, rows = {};
        Object.keys(labels).forEach((t) => {
          const r = e.levels[t];
          const n = document.createElement("input"); n.type = "checkbox"; n.checked = r.enabled !== false;
          const i = document.createElement("input"); i.type = "checkbox"; i.checked = r.side !== false;
          const s = document.createElement("input"); s.type = "checkbox"; s.checked = r.main === true;
          const o = document.createElement("input"); o.type = "color"; o.value = teSafeHexColor(r.color, "#22c55e"); o.style.width = "72px";
          const l = document.createElement("div"); l.textContent = labels[t]; l.style.fontWeight = "600";
          a.appendChild(n); a.appendChild(i); a.appendChild(s); a.appendChild(o); a.appendChild(l);
          rows[t] = { enabled: n, side: i, main: s, color: o, label: labels[t] };
        });
        const sample = document.createElement("div"); sample.style.border = "1px solid #dbeafe"; sample.style.borderRadius = "10px"; sample.style.background = "#f4faff"; sample.style.padding = "8px"; sample.style.fontSize = "12px";
        const updateSample = () => {
          sample.innerHTML = `<div style="font-weight:700;margin-bottom:5px">Sample date border</div>` + Object.keys(rows).map((k) => `<span style="display:inline-block;margin:3px;border:${Number(borderWidth.value)||2}px solid ${rows[k].color.value};border-radius:999px;padding:1px 6px;background:#111;color:#fff">${rows[k].label}</span>`).join("\n");
        };
        [enabled.input, borderWidth, ...Object.values(rows).flatMap((e) => [e.enabled, e.side, e.main, e.color])].forEach((e) => e.addEventListener("input", updateSample));
        updateSample();
        t.appendChild(r); t.appendChild(n); t.appendChild(a); t.appendChild(sample);
        return { element: t, getValues: () => ({ enabled: enabled.input.checked, borderWidth: Number(borderWidth.value) || 2, levels: Object.fromEntries(Object.entries(rows).map(([k, v]) => [k, { enabled: v.enabled.checked, side: v.side.checked, main: v.main.checked, color: v.color.value }])) }) };
      }
      function teCreateRankBadgeSettings() {
        const e = teRankBadgeSettings(), t = document.createElement("div");
        t.style.display = "flex"; t.style.flexDirection = "column"; t.style.gap = "10px"; t.style.borderTop = "1px solid #e5e7eb"; t.style.paddingTop = "12px";
        const r = document.createElement("div"); r.textContent = "Rank badge / No 000"; r.style.fontSize = "14px"; r.style.fontWeight = "700";
        const n = document.createElement("div"); n.style.display = "grid"; n.style.gridTemplateColumns = "repeat(auto-fit, minmax(95px, 1fr))"; n.style.gap = "8px";
        const mkCheck = (label, checked) => { const e = document.createElement("label"), t = document.createElement("input"); e.style.fontSize = "12px"; e.style.display = "flex"; e.style.gap = "5px"; e.style.alignItems = "center"; t.type = "checkbox"; t.checked = checked !== false; e.appendChild(t); e.appendChild(document.createTextNode(label)); return { label: e, input: t }; };
        const enabled = mkCheck("사용", e.enabled), side = mkCheck("Side", e.side), main = mkCheck("Main", e.main), bold = mkCheck("Bold", e.bold);
        [enabled.label, teMakeDefaultButton("Rank badge", teRankBadgeStoreKey), side.label, main.label, bold.label].forEach((e) => n.appendChild(e));
        const grid = document.createElement("div"); grid.style.display = "grid"; grid.style.gridTemplateColumns = "110px 80px 80px 96px 96px"; grid.style.gap = "8px"; grid.style.alignItems = "end"; grid.style.fontSize = "12px";
        const mkNum = (label, value, min, max, step) => { const e = document.createElement("label"), t = document.createElement("input"); e.textContent = label; e.style.display = "flex"; e.style.flexDirection = "column"; e.style.gap = "3px"; t.type = "number"; t.min = String(min); t.max = String(max); t.step = String(step || 1); t.value = value; t.style.width = "76px"; t.style.textAlign = "right"; t.style.padding = "4px 5px"; t.style.border = "1px solid #ccc"; t.style.borderRadius = "4px"; e.appendChild(t); return { label: e, input: t }; };
        const colorLabel = document.createElement("label"); colorLabel.textContent = "테두리색"; colorLabel.style.display = "flex"; colorLabel.style.flexDirection = "column"; colorLabel.style.gap = "3px";
        const borderColor = document.createElement("input"); borderColor.type = "color"; borderColor.value = teSafeHexColor(e.borderColor, "#ff2d2d"); borderColor.style.width = "90px"; colorLabel.appendChild(borderColor);
        const borderWidth = mkNum("테두리 px", e.borderWidth, 0, 8, 1), fontSize = mkNum("글자 px", e.fontSize, 8, 24, 1), borderOpacity = mkNum("테두리 투명", e.borderOpacity, 0, 1, 0.05), textOpacity = mkNum("글자 투명", e.textOpacity, 0, 1, 0.05);
        [colorLabel, borderWidth.label, fontSize.label, borderOpacity.label, textOpacity.label].forEach((e) => grid.appendChild(e));
        const sample = document.createElement("div"); sample.style.border = "1px solid #dbeafe"; sample.style.borderRadius = "10px"; sample.style.background = "#f4faff"; sample.style.padding = "8px"; sample.style.fontSize = "12px";
        const updateSample = () => {
          const fw = bold.input.checked ? 800 : 500;
          const bo = Math.max(0, Math.min(1, Number(borderOpacity.input.value) || 0));
          const to = Math.max(0, Math.min(1, Number(textOpacity.input.value) || 0));
          const badge = `<div class="stat igblue-rank-badge" style="--igblue-rank-border-rgba:${teHexToRgba(borderColor.value, bo, "#ff2d2d")};--igblue-rank-border-width:${Number(borderWidth.input.value)||0}px;--igblue-rank-font-size:${Number(fontSize.input.value)||11}px;--igblue-rank-font-weight:${fw};--igblue-rank-text-opacity:${to}"><span>No 0001</span></div>`;
          const body = `${enabled.input.checked && side.input.checked ? badge : ""}<div class="stat">${teSampleMetricIcon("likes")}<span>118</span></div><div class="stat">${teSampleMetricIcon("comments")}<span>23</span></div><div class="stat">${teSampleMetricIcon("shares")}<span>6</span></div><div class="stat">${teSampleMetricIcon("date")}<span>2026-06-15</span></div><div class="stat">${teSampleMetricIcon("views")}<span>37,819</span></div><div class="stat"><span>ER 0.62%</span></div>`;
          sample.innerHTML = teSampleCardHtml(body, teThumbWidthValue());
        };
        [enabled.input, side.input, main.input, bold.input, borderColor, borderWidth.input, fontSize.input, borderOpacity.input, textOpacity.input].forEach((e) => e.addEventListener("input", updateSample));
        updateSample();
        t.appendChild(r); t.appendChild(n); t.appendChild(grid); t.appendChild(sample);
        return { element: t, getValues: () => ({ enabled: enabled.input.checked, side: side.input.checked, main: main.input.checked, bold: bold.input.checked, borderColor: borderColor.value, borderWidth: Number(borderWidth.input.value) || 0, fontSize: Number(fontSize.input.value) || 11, borderOpacity: Number(borderOpacity.input.value) || 0, textOpacity: Number(textOpacity.input.value) || 0 }) };
      }

      function teCreateActiveDownloadSettings() {
        const e = teActiveDownloadSettings(), t = document.createElement("div");
        t.style.display = "flex"; t.style.flexDirection = "column"; t.style.gap = "10px"; t.style.borderTop = "1px solid #e5e7eb"; t.style.paddingTop = "12px";
        const r = document.createElement("div"); r.textContent = "다운버튼 크게 표시"; r.style.fontSize = "14px"; r.style.fontWeight = "700";
        const desc = document.createElement("div"); desc.textContent = "체크하면 실제 MP4/JPG 다운로드 버튼을 크게 표시합니다. 체크를 해제하면 버튼 가로폭을 50%로 줄여 표시하며 기능은 그대로 유지됩니다."; desc.style.fontSize = "12px"; desc.style.color = "#64748b";
        const n = document.createElement("div"); n.style.display = "grid"; n.style.gridTemplateColumns = "repeat(auto-fit, minmax(92px, 1fr))"; n.style.gap = "8px"; n.style.alignItems = "end"; n.style.fontSize = "12px";
        const mkCheck = (label, checked) => { const e = document.createElement("label"), t = document.createElement("input"); e.style.fontSize = "12px"; e.style.display = "flex"; e.style.gap = "5px"; e.style.alignItems = "center"; t.type = "checkbox"; t.checked = checked !== false; e.appendChild(t); e.appendChild(document.createTextNode(label)); return { label: e, input: t }; };
        const mkNum = (label, value, min, max, step) => { const e = document.createElement("label"), t = document.createElement("input"); e.textContent = label; e.style.display = "flex"; e.style.flexDirection = "column"; e.style.gap = "3px"; t.type = "number"; t.min = String(min); t.max = String(max); t.step = String(step || 1); t.value = value; t.style.width = "76px"; t.style.textAlign = "right"; t.style.padding = "4px 5px"; t.style.border = "1px solid #ccc"; t.style.borderRadius = "4px"; e.appendChild(t); return { label: e, input: t }; };
        const enabled = mkCheck("다운버튼 크게 표시", e.enabled), glow = mkCheck("Glow", e.glow);
        const colorLabel = document.createElement("label"); colorLabel.textContent = "색상"; colorLabel.style.display = "flex"; colorLabel.style.flexDirection = "column"; colorLabel.style.gap = "3px";
        const color = document.createElement("input"); color.type = "color"; color.value = teSafeHexColor(e.color, "#ffffff"); color.style.width = "90px"; colorLabel.appendChild(color);
        const borderWidth = mkNum("테두리 px", e.borderWidth, 0, 12, 1), radius = mkNum("radius", e.radius, 0, 999, 1), glowOpacity = mkNum("Glow 투명", e.glowOpacity, 0, 1, 0.05), scale = mkNum("확대", e.scale, 1, 1.6, 0.01);
        [enabled.label, teMakeDefaultButton("다운버튼 크기", teActiveDownloadStoreKey), glow.label, colorLabel, borderWidth.label, radius.label, glowOpacity.label, scale.label].forEach((e) => n.appendChild(e));
        const sample = document.createElement("div"); sample.style.border = "1px solid #dbeafe"; sample.style.borderRadius = "10px"; sample.style.background = "#0f172a"; sample.style.padding = "14px"; sample.style.fontSize = "12px"; sample.style.minHeight = "52px"; sample.style.display = "flex"; sample.style.alignItems = "center"; sample.style.justifyContent = "center";
        const updateSample = () => {
          const c = teSafeHexColor(color.value, "#ffffff");
          const bo = Math.max(0, Math.min(12, Number(borderWidth.input.value) || 0));
          const rr = Math.max(0, Math.min(999, Number(radius.input.value) || 0));
          const go = Math.max(0, Math.min(1, Number(glowOpacity.input.value) || 0));
          const sc = Math.max(1, Math.min(1.6, Number(scale.input.value) || 1));
          const gl = glow.input.checked ? `0 0 18px 8px ${teHexToRgba(c, go, "#ffffff")}` : "none";
          sample.innerHTML = `<span style="display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:${bo}px solid ${c};border-radius:${rr}px;box-shadow:0 0 0 4px rgba(0,0,0,.55),${gl};transform:scale(${sc});color:#fff;background:rgba(255,255,255,.08)">▶</span>`;
        };
        [enabled.input, glow.input, color, borderWidth.input, radius.input, glowOpacity.input, scale.input].forEach((e) => e.addEventListener("input", updateSample));
        updateSample();
        t.appendChild(r); t.appendChild(desc); t.appendChild(n); t.appendChild(sample);
        return { element: t, getValues: () => ({ enabled: enabled.input.checked, glow: glow.input.checked, color: color.value, borderWidth: Number(borderWidth.input.value) || 0, radius: Number(radius.input.value) || 0, glowOpacity: Number(glowOpacity.input.value) || 0, scale: Number(scale.input.value) || 1 }) };
      }

      function teCreateViewVisualSettings() {
        const e = document.createElement("div");
        e.style.display = "flex"; e.style.flexDirection = "column"; e.style.gap = "12px"; e.style.borderTop = "1px solid #e5e7eb"; e.style.paddingTop = "12px";
        const t = document.createElement("div"); t.textContent = "Visual settings"; t.style.fontSize = "15px"; t.style.fontWeight = "800";
        const r = document.createElement("div"); r.style.fontSize = "12px"; r.style.color = "#475569"; r.textContent = "Side/Main 체크는 사이드패널과 인스타 메인 오버레이 표시 대상을 나눕니다.";
        const n = teCreateMetricBarSettings(), i = teCreateDateBorderSettings();
        e.appendChild(t); e.appendChild(r); e.appendChild(n.element); e.appendChild(i.element);
        return { element: e, getValues: () => ({ metric: n.getValues(), date: i.getValues(), thumbWidth: Number(n.getValues().thumbWidth) || teThumbWidthDefault() }) };
      }
      function teVisualPayload(metricValue, dateValue, rankValue, activeValue) {
        return { metric: metricValue || teMetricBarSettings(), date: dateValue || teDateBorderSettings(), rank: rankValue || teRankBadgeSettings(), active: activeValue || teActiveDownloadSettings() };
      }
      function teSendVisualSettingsToMain(metricValue, dateValue, rankValue, activeValue) {
        const payload = teVisualPayload(metricValue, dateValue, rankValue, activeValue);
        try { chrome.storage.local.set({ ndy_ig_visual_settings: payload }, () => { chrome.runtime.lastError; }); } catch {}
        try { teSendToTargetTab({ type: "ndy_ig_visual_settings_changed", value: payload }, ""); } catch {}
      }
      function T() {
        const e = document.getElementById("settingsModal");
        e && e.remove();
        const { modal: t, dialog: r } = (function () {
            const e = document.createElement("div");
            ((e.id = "settingsModal"),
              (e.style.position = "fixed"),
              (e.style.inset = "0"),
              (e.style.display = "flex"),
              (e.style.alignItems = "center"),
              (e.style.justifyContent = "center"),
              (e.style.backgroundColor = "rgba(0, 0, 0, 0.4)"),
              (e.style.zIndex = "2100"));
            const t = document.createElement("div");
            return (
              (t.style.backgroundColor = "#fff"),
              (t.style.borderRadius = "8px"),
              (t.style.boxShadow = "0 10px 30px rgba(0,0,0,0.2)"),
              (t.style.padding = "20px 24px"),
              (t.style.width = "760px"),
              (t.style.maxWidth = "96%"),
              (t.style.maxHeight = "90vh"),
              (t.style.overflow = "auto"),
              (t.style.display = "flex"),
              (t.style.flexDirection = "column"),
              (t.style.gap = "20px"),
              { modal: e, dialog: t }
            );
          })(),
          { header: i, closeButton: s } = (function (e) {
            const t = document.createElement("div");
            ((t.style.display = "flex"),
              (t.style.justifyContent = "space-between"),
              (t.style.alignItems = "center"),
              (t.style.position = "sticky"),
              (t.style.top = "0"),
              (t.style.background = "#fff"),
              (t.style.zIndex = "4"),
              (t.style.paddingBottom = "8px"));
            const r = document.createElement("h3");
            ((r.textContent = "Settings"),
              (r.style.margin = "0"),
              (r.style.fontSize = "18px"),
              (r.style.fontWeight = "600"));
            const n = document.createElement("button");
            return (
              (n.type = "button"),
              (n.textContent = "×"),
              (n.style.border = "none"),
              (n.style.background = "transparent"),
              (n.style.fontSize = "24px"),
              (n.style.cursor = "pointer"),
              (n.style.lineHeight = "1"),
              n.setAttribute("aria-label", "Close"),
              n.addEventListener("click", e),
              t.appendChild(r),
              t.appendChild(n),
              { header: t, closeButton: n }
            );
          })(() => t.remove()),
          o = (function (e) {
            const t = document.createElement("label");
            ((t.style.display = "flex"),
              (t.style.flexDirection = "column"),
              (t.style.gap = "6px"),
              (t.style.fontSize = "14px"),
              (t.style.fontWeight = "600"),
              (t.textContent = "Overlay display"));
            const r = document.createElement("select");
            ((r.style.padding = "6px 8px"),
              (r.style.borderRadius = "4px"),
              (r.style.border = "1px solid #ccc"),
              (r.style.fontSize = "14px"));
            const n = {
              detail: "Detail (stats + downloads)",
              download: "Down icon only",
              none: "None (hide overlay)",
            };
            return (
              b.forEach((e) => {
                const t = document.createElement("option");
                ((t.value = e), (t.textContent = n[e] || e), r.appendChild(t));
              }),
              (r.value = e),
              t.appendChild(r),
              {
                element: t,
                getValue: () => r.value,
                setValue: (e) => {
                  k(e) && (r.value = e);
                },
              }
            );
          })(_),
          a = (function (e) {
            const t = document.createElement("div");
            ((t.style.display = "flex"),
              (t.style.flexDirection = "column"),
              (t.style.gap = "12px"));
            const r = document.createElement("div");
            ((r.textContent = "Engagement formula"),
              (r.style.fontSize = "14px"),
              (r.style.fontWeight = "600"));
            const n = document.createElement("div");
            ((n.textContent =
              "Adjust how likes and comments contribute to ER."),
              (n.style.fontSize = "12px"),
              (n.style.color = "#555"));
            const i = document.createElement("div");
            ((i.style.display = "grid"),
              (i.style.gridTemplateColumns =
                "repeat(auto-fit, minmax(140px, 1fr))"),
              (i.style.gap = "10px"));
            const s = {},
              o = (e, t, r) => {
                const n = document.createElement("label");
                ((n.style.display = "flex"),
                  (n.style.flexDirection = "column"),
                  (n.style.gap = "4px"),
                  (n.style.fontSize = "12px"),
                  (n.textContent = e));
                const i = document.createElement("input");
                return (
                  (i.type = "number"),
                  (i.min = "0"),
                  (i.step = "0.1"),
                  (i.placeholder = r),
                  (i.style.padding = "6px 8px"),
                  (i.style.borderRadius = "4px"),
                  (i.style.border = "1px solid #ccc"),
                  n.appendChild(i),
                  (s[t] = i),
                  n
                );
              };
            (i.appendChild(o("Like weight", "like", "1")),
              i.appendChild(o("Comment weight", "comment", "4")),
              i.appendChild(o("Repost weight", "repost", "4")));
            const a = document.createElement("div");
            ((a.style.fontSize = "12px"),
              (a.style.color = "#333"),
              (a.style.backgroundColor = "#f5f5f5"),
              (a.style.borderRadius = "4px"),
              (a.style.padding = "8px"));
            const l = () => ({
                like: s.like.valueAsNumber,
                comment: s.comment.valueAsNumber,
                repost: s.repost.valueAsNumber,
              }),
              c = () => {
                const e = l(),
                  t = Number.isFinite(e.like) ? e.like : "?",
                  r = Number.isFinite(e.comment) ? e.comment : "?",
                  n = Number.isFinite(e.repost) ? e.repost : "?";
                a.textContent = `ER = (Likes × ${t} + Comments × ${r} + Reposts × ${n}) ÷ Plays count`;
              };
            Object.values(s).forEach((e) => {
              e.addEventListener("input", c);
            });
            const u = (e) => {
              const t = C(e);
              ((s.like.value = t.like),
                (s.comment.value = t.comment),
                (s.repost.value = t.repost),
                c());
            };
            return (
              u(e),
              t.appendChild(r),
              t.appendChild(n),
              t.appendChild(i),
              t.appendChild(a),
              { element: t, getValues: l, setValues: u }
            );
          })(x),
          teErUi = teCreateErEmojiSettings(),
          teViewUi = teCreateViewVisualSettings(),
          teRankUi = teCreateRankBadgeSettings(),
          teActiveUi = teCreateActiveDownloadSettings();
        let l;
        const tePreviewCurrentSettings = () => {
          const teErValue = teErUi.getValues(), teViewValue = teViewUi.getValues(), teRankValue = teRankUi.getValues(), teActiveValue = teActiveUi.getValues();
          teSaveErEmojiSettings(teErValue);
          teSaveViewVisualSettings(teViewValue);
          teSaveRankBadgeSettings(teRankValue);
          teSaveActiveDownloadSettings(teActiveValue);
          teViewValue && teApplyThumbWidth(teViewValue.thumbWidth);
          teSendVisualSettingsToMain(teViewValue.metric, teViewValue.date, teRankValue, teActiveValue);
          Array.isArray(oe) && ee(oe);
          n("미리보기 적용");
        };
        const c = (function (onCancel, onPreview, onSave) {
          const r = document.createElement("div");
          ((r.style.display = "flex"),
            (r.style.justifyContent = "flex-end"),
            (r.style.gap = "8px"),
            (r.style.position = "sticky"),
            (r.style.bottom = "0"),
            (r.style.background = "#fff"),
            (r.style.paddingTop = "8px"),
            (r.style.zIndex = "4"));
          const n = document.createElement("button");
          ((n.type = "button"),
            (n.textContent = "Cancel"),
            (n.style.padding = "6px 12px"),
            (n.style.border = "1px solid #ccc"),
            (n.style.borderRadius = "4px"),
            (n.style.backgroundColor = "#f4f4f4"),
            n.addEventListener("click", onCancel));
          const pBtn = document.createElement("button");
          ((pBtn.type = "button"),
            (pBtn.textContent = "미리보기"),
            (pBtn.style.padding = "6px 12px"),
            (pBtn.style.border = "1px solid #93c5fd"),
            (pBtn.style.borderRadius = "4px"),
            (pBtn.style.backgroundColor = "#e7f1ff"),
            (pBtn.style.color = "#1d4ed8"),
            (pBtn.style.cursor = "pointer"),
            pBtn.addEventListener("click", (ev) => { ev.preventDefault(); ev.stopPropagation(); onPreview(); }));
          const i = document.createElement("button");
          return (
            (i.type = "button"),
            (i.textContent = "Save"),
            (i.style.padding = "6px 12px"),
            (i.style.border = "none"),
            (i.style.borderRadius = "4px"),
            (i.style.backgroundColor = "#f7941e"),
            (i.style.color = "#fff"),
            (i.style.cursor = "pointer"),
            i.addEventListener("click", onSave),
            r.appendChild(n),
            r.appendChild(pBtn),
            r.appendChild(i),
            { element: r, saveButton: i, previewButton: pBtn }
          );
        })(
          () => t.remove(),
          tePreviewCurrentSettings,
          () => {
            const e = o.getValue(),
              r = a.getValues(),
              teErValue = teErUi.getValues(),
              teViewValue = teViewUi.getValues(),
              teRankValue = teRankUi.getValues(),
              teActiveValue = teActiveUi.getValues();
            if (!k(e)) return void n("Invalid overlay mode");
            if (!E(r)) return void n("Invalid ER weights");
            const i = C(r);
            var s, c;
            ((l.disabled = !0),
              Promise.all([
                ((c = e),
                new Promise((e, t) => {
                  chrome.storage.local.set({ [m]: c }, () => {
                    chrome.runtime.lastError
                      ? t(new Error(chrome.runtime.lastError.message))
                      : ((_ = c), e());
                  });
                })),
                ((s = i),
                new Promise((e, t) => {
                  const r = C(s);
                  chrome.storage.local.set({ [y]: r }, () => {
                    chrome.runtime.lastError
                      ? t(new Error(chrome.runtime.lastError.message))
                      : ((x = r), e());
                  });
                })),
              ])
                .then(() => {
                  (!(function (e) {
                    chrome.tabs.query(
                      { url: ["*://www.instagram.com/*"] },
                      (t) => {
                        chrome.runtime.lastError ||
                          (t &&
                            0 !== t.length &&
                            t.forEach((t) => {
                              chrome.tabs.sendMessage(
                                t.id,
                                {
                                  type: "ndy_ig_overlay_mode_changed",
                                  value: e,
                                },
                                () => {
                                  chrome.runtime.lastError &&
                                    chrome.runtime.lastError.message?.includes(
                                      "Receiving end does not exist",
                                    );
                                },
                              );
                            }));
                      },
                    );
                  })(e),
                    (function (e) {
                      const t = C(e || x);
                      chrome.tabs.query(
                        { url: ["*://www.instagram.com/*"] },
                        (e) => {
                          chrome.runtime.lastError ||
                            (e &&
                              0 !== e.length &&
                              e.forEach((e) => {
                                chrome.tabs.sendMessage(
                                  e.id,
                                  {
                                    type: "ndy_ig_er_weights_changed",
                                    value: t,
                                  },
                                  () => {
                                    chrome.runtime.lastError &&
                                      chrome.runtime.lastError.message?.includes(
                                        "Receiving end does not exist",
                                      );
                                  },
                                );
                              }));
                        },
                      );
                    })(i),
                    teSaveErEmojiSettings(teErValue),
                    teSaveViewVisualSettings(teViewValue),
                    teSaveRankBadgeSettings(teRankValue),
                    teSaveActiveDownloadSettings(teActiveValue),
                    teViewValue && teApplyThumbWidth(teViewValue.thumbWidth),
                    teSendVisualSettingsToMain(teViewValue.metric, teViewValue.date, teRankValue, teActiveValue),
                    Array.isArray(oe) && ee(oe),
                    n("Settings updated"),
                    t.remove());
                })
                .catch((e) => {
                  n("Failed to save, please retry");
                })
                .finally(() => {
                  l.disabled = !1;
                }));
          },
        );
        const teTabs = teCreateSettingsTabs([
          { title: "기본", element: o.element },
          { title: "ER", element: (() => { const e = document.createElement("div"); e.style.display = "flex"; e.style.flexDirection = "column"; e.style.gap = "12px"; e.appendChild(a.element); e.appendChild(teErUi.element); return e; })() },
          { title: "막대/날짜", element: teViewUi.element },
          { title: "순위", element: (() => { const e = document.createElement("div"); e.style.display = "flex"; e.style.flexDirection = "column"; e.style.gap = "12px"; e.appendChild(teRankUi.element); e.appendChild(teActiveUi.element); return e; })() }
        ]);
        ((l = c.saveButton),
          r.appendChild(i),
          r.appendChild(teTabs.element),
          r.appendChild(c.element),
          t.appendChild(r),
          document.body.appendChild(t),
          S().then((e) => {
            o.setValue(e);
          }),
          M().then((e) => {
            a.setValues(e);
          }),
          setTimeout(() => {
            s.focus();
          }, 0));
      }
      function A() {
        n("Down limit reached");
        const e = document.getElementById("down_progress");
        e && (e.textContent = "Status: Down limit reached");
        teProgressState.visible = !1;
        teProgressState.done = !1;
        teUpdateProgressBox();
      }
      function R(e) {
        return new Promise((t) => setTimeout(t, e));
      }
      function O() {
        return {
          all: Array.isArray(se) ? se.length : 0,
          filtered: Array.isArray(oe) ? oe.length : 0,
        };
      }
      function j(e, t, r) {
        const n = document.getElementById(e);
        return n
          ? t !== r
            ? ((n.textContent = r),
              n.classList.add("highlight"),
              setTimeout(() => n.classList.remove("highlight"), 500),
              r)
            : (n.textContent !== String(r) && (n.textContent = r), t)
          : r;
      }
      function I(e, t) {
        const r = {
          filtered: Number.isFinite(e) ? e : O().filtered,
          all: Number.isFinite(t) ? t : O().all,
        };
        ((h = j("page_info_all", h, r.all)),
          (f = j("page_info_filtered", f, r.filtered)),
          N(),
          P());
      }
      // IGblue local patch v15: side-panel progress box for bulk downloads.
      function teFormatBytes(e) {
        const t = Number(e) || 0;
        return t >= 1073741824 ? `${(t / 1073741824).toFixed(2)} GB` : t >= 1048576 ? `${(t / 1048576).toFixed(1)} MB` : t >= 1024 ? `${(t / 1024).toFixed(1)} KB` : `${t} B`;
      }
      function teExtractNoLabelFromText(...values) {
        try {
          const joined = values.map((v) => String(v || "")).filter(Boolean).join("\n");
          if (!joined) return "";
          const m = joined.match(/(?:^|[^A-Za-z0-9])No\s*0*(\d{1,6})(?=$|[^A-Za-z0-9])/i);
          if (!m) return "";
          const n = Number(m[1] || 0) || 0;
          return n > 0 ? ("No" + String(n).padStart(3, "0")) : "";
        } catch (_) { return ""; }
      }
      function teProgressCurrentNoLabel(extra) {
        return teProgressState.no_label || teExtractNoLabelFromText(extra || "", teProgressState.file_path, teProgressState.file, teProgressState.message, teProgressState.shortcode);
      }
      function tePad2(n) { return String(Math.max(0, Number(n) || 0)).padStart(2, "0"); }
      function teFormatShortDateTime(ts) {
        if (!ts) return "-- --:--";
        const d = new Date(Number(ts));
        const ap = d.getHours() >= 12 ? "PM" : "AM";
        const hh = d.getHours() % 12 || 12;
        return `${tePad2(d.getMonth() + 1)}-${tePad2(d.getDate())} ${ap}${tePad2(hh)}:${tePad2(d.getMinutes())}`;
      }
      function teFormatElapsed(ms) {
        const s = Math.max(0, Math.floor((Number(ms) || 0) / 1000));
        const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
        return `${tePad2(h)}:${tePad2(m)}:${tePad2(sec)}`;
      }
      function teFormatSec(ms) {
        const v = Math.max(0, Number(ms) || 0) / 1000;
        return v >= 10 ? `${Math.round(v)}초` : `${v.toFixed(1)}초`;
      }
      function teFormatSecRange(minMs, maxMs) {
        return `${teFormatSec(minMs || 0).replace(/초$/, "")}~${teFormatSec(maxMs || 0)}`;
      }
      function teProgressAllPaths() {
        try {
          const raw = [];
          if (teProgressState.file_path) raw.push(teProgressState.file_path);
          if (teProgressState.file) raw.push(teProgressState.file);
          if (Array.isArray(teProgressState.file_paths)) raw.push.apply(raw, teProgressState.file_paths);
          const seen = new Set();
          const out = [];
          for (const pth of raw) {
            const v = String(pth || "").trim();
            if (!v || seen.has(v)) continue;
            seen.add(v);
            out.push(v);
          }
          return out;
        } catch (_) { return []; }
      }
      function teProgressPrimaryPath() {
        const paths = teProgressAllPaths();
        return paths.length ? paths[0] : "";
      }
      function teProgressMp4Path() {
        try {
          for (const pth of teProgressAllPaths()) {
            const v = String(pth || "");
            if (/\.mp4(?:$|[?#])/i.test(v)) return v;
          }
        } catch (_) {}
        return "";
      }
      function teProgressAssetLabel() {
        const k = String(teProgressState.asset_kind || "").toLowerCase();
        if (k === "video" || /\.mp4(?:$|[?#])/i.test(teProgressPrimaryPath())) return "mp4";
        if (k === "image" || /\.(jpg|jpeg|png|webp)(?:$|[?#])/i.test(teProgressPrimaryPath())) return "jpg";
        return k || "파일";
      }
      function teProgressResetForContext(reason) {
        try {
          teProgressState.visible = false;
          teProgressState.done = false;
          teProgressState.file = "";
          teProgressState.file_path = "";
          teProgressState.file_paths = [];
          teProgressState.no_label = "";
          teProgressState.loaded = 0;
          teProgressState.total = 0;
          teProgressState.percent = 0;
          teProgressState.message = String(reason || "");
          teProgressState.shortcode = "";
          teProgressState.asset_kind = "";
          teProgressState.owner_key = teActiveFolderAccountName ? teActiveFolderAccountName() : "";
          localStorage.removeItem("IGBLUE/recent_download_state");
          teUpdateProgressBox && teUpdateProgressBox();
        } catch (_) {}
      }
      function teRecentDownloadSummaryText() {
        try {
          const no = teProgressCurrentNoLabel ? teProgressCurrentNoLabel() : "";
          const pct = Math.max(0, Math.min(100, Number(teProgressState.percent || 0) || 0));
          const bytes = teProgressState.total ? ` · ${teFormatBytes(teProgressState.total)}` : "";
          const file = String(teProgressState.file_path || teProgressState.file || "");
          const name = file ? file.split(/[\/]/).pop() : "";
          const sc = teProgressState.shortcode ? ` · ${teProgressState.shortcode}` : "";
          if (!no && !file && !teProgressState.shortcode) return "";
          return `최근다운 ${no || "No---"} ${pct}%${bytes}${sc}${name ? " · " + name : ""}`;
        } catch (_) { return ""; }
      }
      function teFmtProgressPct(done, total) {
        const t = Math.max(0, Number(total || 0) || 0);
        const d = Math.max(0, Number(done || 0) || 0);
        if (!t) return 0;
        return Math.max(0, Math.min(100, Math.round((d / t) * 100)));
      }
      function teFormatEtaHhMm(ms) {
        const totalMin = Math.max(0, Math.ceil((Number(ms) || 0) / 60000));
        const h = Math.floor(totalMin / 60);
        const m = totalMin % 60;
        return `${String(h).padStart(2, "0")}시간 ${String(m).padStart(2, "0")}분`;
      }
      function teEstimateEtaMs(done, total, startTs) {
        const t = Math.max(0, Number(total || 0) || 0);
        const d = Math.max(0, Number(done || 0) || 0);
        const remain = Math.max(0, t - d);
        if (!remain) return 0;
        const elapsed = Math.max(0, Date.now() - (Number(startTs || 0) || Date.now()));
        const avg = d > 0 ? Math.max(500, elapsed / d) : Math.max(1000, elapsed || 0);
        const delay = Math.max(0, Number(teDownloadDelaySec && teDownloadDelaySec() || 0) || 0) * 1000;
        return remain * (avg + delay);
      }
      function teUnifiedProgressText() {
        try {
          let currentLine = "현재 주소  대기  0%";
          let overallLine = "전체 주소 목록 -  0%  종료예상 00시간 00분";
          const totalDownLine = (v) => `총 : ${String(Math.max(0, Number(v || 0) || 0)).padStart(3, "0")} 개 다운`;
          const acct = (typeof teAutoPyAccountQueueStatus === "function" ? teAutoPyAccountQueueStatus() : null);
          const acctTotal = acct && acct.has ? Math.max(0, Number(acct.total || 0) || 0) : 0;
          const acctIndex = acct && acct.has ? Math.max(0, Number(acct.index || 0) || 0) : 0;
          if (p) {
            const total = Math.max(0, Number(p.totalFiles || p.totalPosts || 0) || 0);
            const done = Math.max(0, Number(p.completedFiles || 0) || 0);
            const pct = teFmtProgressPct(done, total);
            const cur = teProgressCurrentNoLabel(p.currentFileLabel || teProgressState.no_label || "") || `${done}/${total || 0}`;
            currentLine = `현재 주소  ${cur}  ${pct}%`;
            const eta = teEstimateEtaMs(done, total, p.start_ts || teProgressState.start_ts || 0);
            if (acctTotal > 0) {
              const base = Math.max(0, acctIndex - 1);
              const overallPct = Math.max(0, Math.min(100, Math.round(((base + pct / 100) / acctTotal) * 100)));
              overallLine = `전체 주소 목록 ${acctIndex}/${acctTotal}  ${overallPct}%  종료예상 ${teFormatEtaHhMm(eta)}`;
            } else {
              overallLine = `전체 주소 목록 -  ${pct}%  종료예상 ${teFormatEtaHhMm(eta)}`;
            }
            const downLine = totalDownLine(done);
            if ((p.statusLabel === "Completed" || teProgressState.done) && total > 0) {
              teAutoPyLastDoneText = "자동다운로드 완료\n" + currentLine + "\n" + overallLine + "\n" + downLine;
              return teAutoPyLastDoneText;
            }
            return currentLine + "\n" + overallLine + "\n" + downLine;
          }
          const st = teAutoPyStats || {};
          if (st.start_ts || st.active) {
            const target = Math.max(1, Number(teAutoPyTargetCount && teAutoPyTargetCount() || teTopNCount && teTopNCount() || 1) || 1);
            const curNo = Math.max(0, Number(st.current_no || teAutoPyReadLastRankTextValue && teAutoPyReadLastRankTextValue() || 0) || 0);
            const done = Math.max(0, Math.min(target, curNo));
            const pct = teFmtProgressPct(done, target);
            currentLine = `현재 주소  ${curNo ? teAutoPyNoLabel(curNo) : "No---"}  ${pct}%`;
            const eta = teEstimateEtaMs(done, target, st.start_ts || Date.now());
            if (acctTotal > 0) {
              const base = Math.max(0, acctIndex - 1);
              const overallPct = Math.max(0, Math.min(100, Math.round(((base + pct / 100) / acctTotal) * 100)));
              overallLine = `전체 주소 목록 ${acctIndex}/${acctTotal}  ${overallPct}%  종료예상 ${teFormatEtaHhMm(eta)}`;
            } else {
              overallLine = `전체 주소 목록 -  ${pct}%  종료예상 ${teFormatEtaHhMm(eta)}`;
            }
            const downLine = totalDownLine((Number(st.mp4_count || 0) || 0) + (Number(st.jpg_count || 0) || 0));
            return currentLine + "\n" + overallLine + "\n" + downLine;
          }
          if (teAutoPyLastDoneText) return teAutoPyLastDoneText;
          return "자동현황 대기\n다운로드 진행 없음\n" + totalDownLine(0);
        } catch (_) { return "자동현황 대기\n다운로드 진행 없음"; }
      }
      function teRenderUnifiedStatus(reason) {
        try {
          const el = document.getElementById("igblue_auto_py_stats");
          if (!el) return;
          const text = teUnifiedProgressText();
          el.textContent = text;
          el.title = text + "\n현재 주소 진행률 / 전체 주소 목록 진행률 / 종료예상입니다.";
          el.classList.toggle("is-idle", !p && !(teAutoPyStats && (teAutoPyStats.start_ts || teAutoPyStats.active)));
        } catch (_) {}
      }
      function teAutoPyStatsText() {
        return teUnifiedProgressText();
      }
      function teAutoPyStatsRender() {
        teRenderUnifiedStatus("auto_stats");
      }
      function teAutoPyStatsPushToMain(reason, force) {
        try {
          const now = Date.now();
          if (!force && now - teAutoPyStatsLastMainPush < 900) return;
          teAutoPyStatsLastMainPush = now;
          const text = teAutoPyStatsText();
          try { localStorage.setItem("IGBLUE/auto_py_stats_last", JSON.stringify(Object.assign({}, teAutoPyStats, { text, reason: String(reason || ""), updated_at: new Date().toISOString() }))); } catch (_) {}
          teSendToTargetTab({ type: "igblue_auto_status_update", value: { text, reason: String(reason || ""), active: !!(teAutoPyStats && teAutoPyStats.active), no_label: teAutoPyStats && teAutoPyStats.current_no_label || "", ts: now } }, "").catch(() => {});
        } catch (_) {}
      }
      function teAutoPyStatsTouch(reason, force) {
        teAutoPyStatsRender();
        teAutoPyStatsPushToMain(reason, !!force);
      }
      function teAutoPyStatsStart(seq, startNo, maxRank, reason) {
        const n = Number(startNo || 0) || 0;
        if (!(teAutoPyStats && teAutoPyStats.active && Number(teAutoPyStats.run_seq || 0) === Number(seq || 0))) {
          teAutoPyStats = { active: true, run_seq: Number(seq || 0) || 0, start_ts: Date.now(), current_no: n, current_no_label: n > 0 ? teAutoPyNoLabel(n) : "No---", max_rank: Number(maxRank || 0) || 0, mp4_count: 0, jpg_count: 0, mp4_total_ms: 0, jpg_total_ms: 0, mp4_max_ms: 0, jpg_max_ms: 0, mp4_min_ms: 0, jpg_min_ms: 0, cycle_count: 0, cycle_total_ms: 0, cycle_max_ms: 0, cycle_min_ms: 0, last_reason: String(reason || "start") };
        } else {
          teAutoPyStats.current_no = n || teAutoPyStats.current_no || 0;
          teAutoPyStats.current_no_label = n > 0 ? teAutoPyNoLabel(n) : (teAutoPyStats.current_no_label || "No---");
          teAutoPyStats.max_rank = Number(maxRank || teAutoPyStats.max_rank || 0) || 0;
          teAutoPyStats.last_reason = String(reason || teAutoPyStats.last_reason || "update");
        }
        if (!teAutoPyStatsTimer) teAutoPyStatsTimer = setInterval(() => { try { if (teAutoPyStats && teAutoPyStats.start_ts) teAutoPyStatsTouch("tick", false); } catch (_) {} }, 1000);
        teAutoPyStatsTouch(reason || "start", true);
      }
      function teAutoPyStatsSetRank(no, reason) {
        const n = Number(no || 0) || 0;
        if (n > 0) {
          teAutoPyStats.current_no = n;
          teAutoPyStats.current_no_label = teAutoPyNoLabel(n);
        }
        teAutoPyStats.last_reason = String(reason || teAutoPyStats.last_reason || "rank");
        teAutoPyStatsTouch(reason || "rank", false);
      }
      function teAutoPyStatsRecordAsset(kind, elapsedMs) {
        const k = String(kind || "").toLowerCase() === "jpg" ? "jpg" : "mp4";
        const ms = Math.max(0, Number(elapsedMs) || 0);
        if (k === "jpg") {
          teAutoPyStats.jpg_count += 1;
          teAutoPyStats.jpg_total_ms += ms;
          teAutoPyStats.jpg_max_ms = Math.max(teAutoPyStats.jpg_max_ms || 0, ms);
          teAutoPyStats.jpg_min_ms = teAutoPyStats.jpg_min_ms ? Math.min(teAutoPyStats.jpg_min_ms, ms) : ms;
        } else {
          teAutoPyStats.mp4_count += 1;
          teAutoPyStats.mp4_total_ms += ms;
          teAutoPyStats.mp4_max_ms = Math.max(teAutoPyStats.mp4_max_ms || 0, ms);
          teAutoPyStats.mp4_min_ms = teAutoPyStats.mp4_min_ms ? Math.min(teAutoPyStats.mp4_min_ms, ms) : ms;
        }
        teAutoPyStatsTouch("asset_" + k, true);
      }
      function teAutoPyStatsRecordCycle(startMs, reason) {
        const ms = Math.max(0, Date.now() - (Number(startMs) || Date.now()));
        if (!ms) return;
        teAutoPyStats.cycle_count += 1;
        teAutoPyStats.cycle_total_ms += ms;
        teAutoPyStats.cycle_max_ms = Math.max(teAutoPyStats.cycle_max_ms || 0, ms);
        teAutoPyStats.cycle_min_ms = teAutoPyStats.cycle_min_ms ? Math.min(teAutoPyStats.cycle_min_ms, ms) : ms;
        teAutoPyStatsTouch(reason || "cycle", true);
      }
      function teAutoPyStatsFinish(reason) {
        try {
          if (teAutoPyStats && teAutoPyStats.start_ts) {
            teAutoPyStats.active = false;
            teAutoPyStats.last_reason = String(reason || "finished");
            teAutoPyLastDoneText = "자동다운로드 완료\n" + teUnifiedProgressText();
            teAutoPyStatsTouch(reason || "finished", true);
            try { n("자동다운로드 완료"); } catch (_) {}
          }
          if (teAutoPyStatsTimer) { clearInterval(teAutoPyStatsTimer); teAutoPyStatsTimer = 0; }
        } catch (_) {}
      }
      function teProgressEls() {
        return {
          box: document.getElementById("igblue_download_progress"),
          bar: document.getElementById("igblue_progress_bar"),
          summary: document.getElementById("igblue_progress_summary"),
          detail: document.getElementById("igblue_progress_detail"),
          ok: document.getElementById("igblue_progress_ok"),
          stop: document.getElementById("igblue_progress_stop"),
          resume: document.getElementById("igblue_progress_resume"),
          openFile: document.getElementById("igblue_progress_open_file"),
          openFolder: document.getElementById("igblue_progress_open_folder"),
          title: document.getElementById("igblue_progress_title")
        };
      }
      function teIsDownloadRunning() {
        return !!(p && p.statusLabel && !["Completed", "Stopped", "No downloadable medias"].includes(p.statusLabel));
      }
      function teUpdateDownloadControlButtons() {
        const e = teProgressEls();
        const running = teIsDownloadRunning();
        if (e.stop) e.stop.disabled = !running || teDownloadStopRequested;
        if (e.resume) e.resume.disabled = !(teDownloadResumeState && !running);
      }
      function teRequestStopDownload() {
        if (!teIsDownloadRunning()) return;
        teDownloadStopRequested = !0;
        teProgressState.message = "Stop requested. Current file will finish first.";
        p && (p.statusLabel = "Stopping...");
        N();
      }
      function teResumeStoppedDownload() {
        if (!teDownloadResumeState || teIsDownloadRunning()) return;
        const e = teDownloadResumeState;
        teDownloadResumeState = null;
        X(e.scope, [], e);
      }
      function teCloseProgressBox() {
        teProgressState.visible = !1;
        teProgressState.done = !1;
        teProgressState.file = "";
        teProgressState.loaded = 0;
        teProgressState.total = 0;
        teProgressState.percent = 0;
        teProgressState.message = "";
        teProgressState.file_path = "";
        teProgressState.file_paths = [];
        teProgressState.shortcode = "";
        teProgressState.asset_kind = "";
        teProgressState.media_index = 0;
        teProgressState.json_path = "";
        teProgressState.start_ts = 0;
        p = null;
        teDownloadStopRequested = !1;
        teUpdateProgressBox();
      }
      function teUpdateProgressBox() {
        const e = teProgressEls();
        if (!e.box) return;
        if (e.ok && !e.ok.dataset.igblueBound) {
          e.ok.dataset.igblueBound = "1";
          e.ok.addEventListener("click", teCloseProgressBox);
        }
        if (e.stop && !e.stop.dataset.igblueBound) {
          e.stop.dataset.igblueBound = "1";
          e.stop.addEventListener("click", () => {
            teRequestStopDownload();
            try { if (teAutoPyStats && teAutoPyStats.active) teAutoPyStop("progress_stop"); } catch (_) {}
          });
        }
        if (e.resume && !e.resume.dataset.igblueBound) {
          e.resume.dataset.igblueBound = "1";
          e.resume.addEventListener("click", () => { teResumeStoppedDownload(); });
        }
        const openLocal = (kind) => {
          const path = kind === "folder" ? (teProgressMp4Path() || teProgressState.file_path || teProgressState.file || "") : teProgressMp4Path();
          if (!path) return;
          try {
            chrome.runtime.sendMessage({type: kind === "folder" ? "IGBLUE_OPEN_LOCAL_FOLDER" : "IGBLUE_OPEN_LOCAL_FILE", path}, (res) => {
              if (chrome.runtime.lastError) n("로컬 열기 실패");
              else if (!res || res.ok === false) n((res && (res.message || res.error)) || "로컬 열기 실패");
              else n(kind === "folder" ? "폴더 열기 요청" : "mp4 열기 요청");
            });
          } catch (_) { n("로컬 열기 예외"); }
        };
        if (e.openFile && !e.openFile.dataset.igblueBound) {
          e.openFile.dataset.igblueBound = "1";
          e.openFile.addEventListener("click", () => openLocal("file"));
        }
        if (e.openFolder && !e.openFolder.dataset.igblueBound) {
          e.openFolder.dataset.igblueBound = "1";
          e.openFolder.addEventListener("click", () => openLocal("folder"));
        }
        const mp4Path = teProgressMp4Path();
        const hasMp4 = !!mp4Path;
        const hasAny = !!(teProgressState.file_path || teProgressState.file || (Array.isArray(teProgressState.file_paths) && teProgressState.file_paths.length));
        if (e.openFile) { e.openFile.disabled = !hasMp4; e.openFile.textContent = "mp4열기"; }
        if (e.openFolder) e.openFolder.disabled = !(hasMp4 || hasAny);
        if (!p && !teProgressState.visible && !hasAny) {
          e.box.classList.add("igblue-progress-hidden");
          return;
        }
        e.box.classList.remove("igblue-progress-hidden");
        const t = p || { statusLabel: teProgressState.done ? "Completed" : "Downing...", currentFileLabel: teProgressState.file };
        const i = Math.max(0, Math.min(100, Number(teProgressState.percent) || 0));
        const done = t.statusLabel === "Completed" || teProgressState.done;
        const pct = done ? 100 : i;
        const progressNoLabel = teProgressCurrentNoLabel(t.currentFileLabel || teProgressState.no_label || "");
        const size = teProgressState.total > 0 ? teFormatBytes(teProgressState.total) : (teProgressState.loaded > 0 ? teFormatBytes(teProgressState.loaded) : "");
        const sc = String(teProgressState.shortcode || "");
        if (e.title) e.title.textContent = `다운로드 진행${progressNoLabel ? ` · ${progressNoLabel}` : ""}`;
        if (e.bar) e.bar.style.width = `${pct}%`;
        if (e.ok) e.ok.style.display = "none";
        teUpdateDownloadControlButtons();
        if (e.summary) e.summary.textContent = [progressNoLabel || "No---", `${pct}%`, sc].filter(Boolean).join(" · ");
        if (e.detail) {
          const allPaths = teProgressAllPaths();
          const primaryPath = teProgressPrimaryPath();
          const assetLabel = teProgressAssetLabel();
          const pathLines = allPaths.length
            ? allPaths.slice(0, 6).map((pth, idx) => `${idx === 0 ? "전체 저장 주소" : "추가 저장 주소 " + (idx + 1)}: ${pth}`)
            : [`${assetLabel}: 대기중`];
          e.detail.textContent = [
            progressNoLabel ? `No: ${progressNoLabel}` : "",
            sc ? `shortcode: ${sc}` : "",
            assetLabel ? `asset: ${assetLabel}${Number(teProgressState.media_index || 0) ? `[${teProgressState.media_index}]` : ""}` : "",
            primaryPath ? `${assetLabel}: ${primaryPath}` : "",
            ...pathLines,
            teProgressState.json_path ? `json: ${teProgressState.json_path}` : "",
            teProgressState.message ? `상태: ${teProgressState.message}` : ""
          ].filter(Boolean).join("\n");
        }
        teRenderUnifiedStatus("progress_box");
      }
      function teHandleDownProgress(e) {
        if (e && e.feature_check_id) {
          teAutoPyFeatureCheck(
            e.feature_check_id,
            e.feature_check_status || "[체크완료]",
            e.feature_check_message_ko || e.message || "",
            {
              cycle_id: e.feature_cycle_id || e.cycle_id || "",
              rank_no: Number(e.rank_no || 0) || 0,
              rank_label: String(e.rank_label || e.no_label || ""),
              shortcode: String(e.shortcode || ""),
              asset_kind: String(e.asset_kind || ""),
              error_code: String(e.error_code || ""),
              next_check: String(e.next_check || "")
            }
          );
          if (String(e.status || "") === "feature_check") return;
        }
        try {
          const videoOnly = !!(document.getElementById("download_video_only") && document.getElementById("download_video_only").checked);
          const rawPath0 = String(e && (e.file_path || e.file || "") || "");
          const kind0 = String(e && (e.asset_kind || "") || "").toLowerCase();
          if (videoOnly && (kind0 === "image" || /\.(jpg|jpeg|png|webp)(?:$|[?#])/i.test(rawPath0))) return;
        } catch (_) {}
        teProgressState.visible = !0;
        teProgressState.file = e.file || teProgressState.file || "";
        teProgressState.file_path = e.file_path || teProgressState.file_path || (String(e.file || "").includes(":\\") ? e.file : "");
        teProgressState.file_paths = Array.isArray(e.file_paths) ? e.file_paths : teProgressState.file_paths || [];
        teProgressState.shortcode = e.shortcode || teProgressState.shortcode || (e.meta && e.meta.shortcode) || "";
        teProgressState.no_label = e.no_label || teExtractNoLabelFromText(e.file_path, e.file, e.prefix, e.filename, e.message, teProgressState.file_path, teProgressState.file) || teProgressState.no_label || "";
        teProgressState.asset_kind = e.asset_kind || teProgressState.asset_kind || "";
        teProgressState.media_index = e.media_index != null ? e.media_index : (teProgressState.media_index || 0);
        teProgressState.json_path = e.json_path || teProgressState.json_path || "";
        teProgressState.loaded = Number(e.loaded) || 0;
        teProgressState.total = Number(e.total) || 0;
        teProgressState.percent = Math.max(0, Math.min(100, Number(e.percent) || 0));
        teProgressState.message = e.message || "";
        teProgressState.owner_key = teActiveFolderAccountName ? teActiveFolderAccountName() : teProgressState.owner_key || "";
        if ("done" === e.status || "already_done" === e.status) teProgressState.percent = 100;
        if ("error" === e.status) teProgressState.message = e.message || "Down error";
        try {
          const recent = {
            ts: new Date().toISOString(),
            status: e.status || "",
            instagram_url: teCurrentTargetUrl(),
            shortcode: teProgressState.shortcode || "",
            no_label: teProgressState.no_label || teProgressCurrentNoLabel(),
            asset_kind: teProgressState.asset_kind || "",
            media_index: teProgressState.media_index || 0,
            file_path: teProgressState.file_path || teProgressState.file || "",
            file_paths: teProgressState.file_paths || [],
            json_path: teProgressState.json_path || "",
            message: teProgressState.message || ""
          };
          localStorage.setItem("IGBLUE/recent_download_state", JSON.stringify(recent));
        } catch (_) {}
        teUpdateProgressBox();
      }
      function N() {
        const e = document.getElementById("down_progress");
        if (!e) return;
        if (!p) {
          e.textContent = d > 0 ? `Status: Queue ${d}` : "Status: Idle";
          teUpdateProgressBox();
          teRenderUnifiedStatus("status_idle");
          return;
        }
        e.textContent = p.statusLabel ? `Status: ${p.statusLabel}` : "Status: Running";
        if (p.statusLabel === "Completed") {
          teProgressState.visible = !0;
          teProgressState.done = !0;
          teProgressState.percent = 100;
        }
        teUpdateProgressBox();
        teRenderUnifiedStatus("status_running");
      }
      function P() {
        const hasIgTarget = !!u || (typeof teHasInstagramTarget !== "undefined" && !!teHasInstagramTarget);
        const e = document.getElementById("goto");
        // IGblue v86: Goto Instagram은 인스타 대상 존재 여부와 무관하게 항상 누를 수 있어야 한다.
        // 인스타 페이지가 아니거나 대상 탭이 끊긴 상태에서도 사용자가 직접 Instagram으로 이동해야 한다.
        e && (e.disabled = false);
        const t = (e, t) => {
          e.forEach((e) => {
            const r = document.getElementById(e);
            r && (r.disabled = t);
          });
        };
        if (!hasIgTarget) return (t([...i, ...s, ...o], !0), void N());
        const busy = l.size > 0;
        t(["refresh"], !1);
        t(["export_xlsx"], !u);
        t(i, busy);
        const n = O();
        (o.forEach((e) => {
          const t = document.getElementById(e);
          t &&
            (busy
              ? (t.disabled = !0)
              : "down_all_medias" === e
                ? (t.disabled = n.all <= 0)
                : "down_filtered_medias" === e &&
                  (t.disabled = n.filtered <= 0));
        }),
          N());
      }
      function B(e) {
        (l.add(e), P());
      }
      function D(e) {
        l.delete(e) && P();
      }
      function F() {
        ((c = null),
          // IGblue local patch v9: do not clear the UI merely because Instagram changed URL inside the SPA.
          // Clear only when there is no active Instagram target.
          (typeof teHasInstagramTarget !== "undefined" && teHasInstagramTarget)
            ? P()
            : u &&
              ((u = !1),
              void 0 !== se && (se = null),
              void 0 !== oe && (oe = []),
              ee([]),
              ie(0, 0),
              (p = null),
              (d = 0),
              D(a),
              I(0, 0),
              P()));
      }
      function L(e, t, r) {
        let n = { type: "getl" };
        try {
          chrome.runtime.sendMessage(n, function (i) {
            chrome.runtime.lastError ||
              (-1 == i.status
                ? e(r ? -1 : t)
                : ((n = { type: "down" }),
                  chrome.runtime.sendMessage(n, function (r) {
                    chrome.runtime.lastError || (r.status < 4 ? e(t) : A());
                  })));
          });
        } catch {}
      }
      function z() {
        n("Upgrade link disabled in local build");
      }
      function U() {
        L($, 3, !0);
      }
      function $(e) {
        oe && oe.length > 0
          ? (async function (e) {
              const r = new (t().Workbook)(),
                n = r.addWorksheet("Posts"),
                i = Array.from(
                  new Set(
                    e.flatMap((e) =>
                      Object.keys(e).filter(
                        (e) =>
                          "img_b64" !== e && "code" !== e && "username" !== e,
                      ),
                    ),
                  ),
                ).sort((e, t) => e.localeCompare(t)),
                s = ["Image", "Code", "username", ...i];
              (n.addRow(s), (n.views = [{ state: "frozen", ySplit: 1 }]));
              for (let t = 0; t < e.length; t++) {
                const s = e[t],
                  o = [
                    "",
                    "",
                    "",
                    ...i.map((e) =>
                      "created_at" === e && s[e]
                        ? ce(new Date(1e3 * s[e]))
                        : (s[e] ?? ""),
                    ),
                  ],
                  a = n.addRow(o);
                a.height = 78;
                const l = n.getCell(`B${t + 2}`);
                if (
                  ((l.value = {
                    text: s.code,
                    hyperlink: `https://www.instagram.com/p/${s.code}`,
                  }),
                  (l.font = { color: { argb: "FF0000FF" }, underline: !0 }),
                  s?.username)
                ) {
                  const e = n.getCell(`C${t + 2}`);
                  ((e.value = {
                    text: s.username,
                    hyperlink: `https://www.instagram.com/${s.username}`,
                  }),
                    (e.font = { color: { argb: "FF0000FF" }, underline: !0 }));
                }
                if (
                  (i.forEach((e, t) => {
                    const r = a.getCell(4 + t),
                      n = s[e];
                    n &&
                      ("audio_collection" === e &&
                        ((r.value = {
                          text: n,
                          hyperlink: `https://www.instagram.com/reels/audio/${n}/`,
                        }),
                        (r.font = {
                          color: { argb: "FF0000FF" },
                          underline: !0,
                        })),
                      "original_sound_username" === e &&
                        ((r.value = {
                          text: n,
                          hyperlink: `https://www.instagram.com/${n}`,
                        }),
                        (r.font = {
                          color: { argb: "FF0000FF" },
                          underline: !0,
                        })));
                  }),
                  s.img_b64?.startsWith("data:image"))
                ) {
                  const { width: e, height: i } = await le(s.img_b64),
                    o = 100,
                    a = o * (e / i),
                    l = r.addImage({
                      base64: s.img_b64,
                      extension: s.img_b64.includes("image/png")
                        ? "png"
                        : "jpeg",
                      hyperlink: {
                        hyperlink: `https://www.instagram.com/p/${s.code}`,
                        tooltip: "View on Instagram",
                      },
                    });
                  n.addImage(l, {
                    tl: { col: 0, row: t + 1 },
                    ext: { width: a, height: o },
                  });
                }
              }
              n.columns = [
                { width: 13 },
                { width: 12 },
                { width: 12 },
                ...i.map(() => ({ width: 12 })),
              ];
              const o = await r.xlsx.writeBuffer(),
                a = new Blob([o], {
                  type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                }),
                l = document.createElement("a");
              ((l.href = URL.createObjectURL(a)),
                (l.download =
                  "ig-sorter-" +
                  ((c = new Date()),
                  `${c.getFullYear()}${String(c.getMonth() + 1).padStart(2, "0")}${String(c.getDate()).padStart(2, "0")}-${String(c.getHours()).padStart(2, "0")}${String(c.getMinutes()).padStart(2, "0")}${String(c.getSeconds()).padStart(2, "0")}.xlsx`)),
                l.click());
              var c;
            })(oe)
          : n("empty list");
      }
      function teMetricCountEl() {
        return document.getElementById("top_metric_count");
      }
      function teSetTopMetricCountValue(value, title) {
        const el = teMetricCountEl();
        const n = Math.max(0, Math.floor(Number(value || 0) || 0));
        if (el) {
          el.dataset.count = String(n);
          el.textContent = String(n);
          el.title = title || "조회수 기준에 맞는 실제 게시물 개수";
        }
        teLocalSet("filter/metric_count", String(n));
        teLocalSet("top_metric_count", String(n));
        return n;
      }
      function teGetTopMetricCountValue() {
        const el = teMetricCountEl();
        const raw = el ? (el.dataset.count || el.textContent || "0") : teLocalGetAny(["filter/metric_count", "top_metric_count"], "0");
        const n = Number.parseInt(raw, 10);
        return Number.isFinite(n) && n > 0 ? n : 0;
      }
      function teTopNCount() {
        // v179a12: Top sorted medias 개수는 사용자가 입력하는 input이 아니라 계산된 span(data-count) 값이다.
        return teGetTopMetricCountValue();
      }
      function teAutoPyTargetCount() {
        // v179a2: Auto PY 종료 목표. 0/빈값이면 조건에 맞는 전체(top_metric_count)를 사용한다.
        const el = document.getElementById("igblue_auto_py_target_count");
        const raw = el ? Number.parseInt(el.value, 10) : 0;
        const n = Number.isFinite(raw) && raw > 0 ? raw : 0;
        return n > 0 ? n : teTopNCount();
      }
      function teAutoPyTargetCountSave() {
        const el = document.getElementById("igblue_auto_py_target_count");
        if (el) teLocalSet("download/auto_py_target_count", el.value || "");
      }

      const IGBLUE_ACCOUNT_RESERVED_PATHS = new Set(["", "p", "reel", "reels", "explore", "explorer", "stories", "accounts", "direct", "graphql", "api", "oauth", "developer", "about", "privacy", "terms", "legal", "emails", "challenge", "search", "home", "instagram"]);
      const IGBLUE_ACCOUNT_DELETED_PREFIX = "🗑️";
      function teAccountQueueKey() { return "download/auto_py_account_urls"; }
      function teAccountQueueIndexKey() { return "download/auto_py_account_index"; }
      function teAccountQueueHeightKey() { return "download/auto_py_account_textarea_height_px"; }
      function teAccountQueueDisabledKey() { return "download/auto_py_account_queue_disabled"; }
      function teAutoPyAccountQueueDisabled() { return teLocalGet(teAccountQueueDisabledKey(), "0") === "1"; }
      function teAutoPySetAccountQueueDisabled(on) {
        teLocalSet(teAccountQueueDisabledKey(), on ? "1" : "0");
        if (on) teAutoPySetAccountQueueIndex(0);
        teAutoPyUpdateAccountQueueStatus();
      }
      function teAutoPyDeletedLine(raw) {
        return String(raw || "").trim().startsWith(IGBLUE_ACCOUNT_DELETED_PREFIX);
      }
      function teAutoPyStripDeletedPrefix(raw) {
        return String(raw || "").trim().replace(/^🗑️\s*/, "").trim();
      }
      function teAutoPyNormalizeAccountUsername(raw) {
        try {
          let value = teAutoPyStripDeletedPrefix(raw).replace(/^@+/, "").trim();
          if (!value) return "";
          if (/^(?:https?:\/\/|www\.|instagram\.com\/)/i.test(value)) {
            const url = /^https?:\/\//i.test(value) ? value : "https://" + value;
            const parsed = new URL(url);
            if (parsed.hostname.replace(/^www\./i, "").toLowerCase() !== "instagram.com") return "";
            const parts = String(parsed.pathname || "/").split("/").filter(Boolean);
            const first = String(parts[0] || "").trim();
            if (!first || IGBLUE_ACCOUNT_RESERVED_PATHS.has(first.toLowerCase())) return "";
            if (parts.length > 1 && String(parts[1] || "").toLowerCase() !== "reels") return "";
            value = first;
          } else if (value.includes("/")) return "";
          value = value.toLowerCase();
          if (!value || value.length > 30 || IGBLUE_ACCOUNT_RESERVED_PATHS.has(value)) return "";
          if (!/^[a-z0-9._]+$/.test(value)) return "";
          return value;
        } catch (_) { return ""; }
      }
      function teAutoPyAccountUrlFromUsername(raw) {
        const username = teAutoPyNormalizeAccountUsername(raw);
        return username ? `https://www.instagram.com/${username}/` : "";
      }
      function teAutoPyNormalizeAccountUrl(raw) {
        return teAutoPyAccountUrlFromUsername(raw);
      }
      function teAccountNameFromAccountUrl(value) {
        const username = teAutoPyNormalizeAccountUsername(value);
        return username ? "@" + username : "";
      }
      function teAutoPySameAccountUrl(a, b) {
        const ua = teAutoPyNormalizeAccountUsername(a), ub = teAutoPyNormalizeAccountUsername(b);
        return !!(ua && ub && ua === ub);
      }
      function teAutoPyAccountQueueEntries() {
        const el = document.getElementById("igblue_account_url_list");
        const raw = el ? String(el.value || "") : teLocalGet(teAccountQueueKey(), "");
        const byUsername = new Map();
        for (const line of String(raw || "").split(/\r?\n|\r/)) {
          const username = teAutoPyNormalizeAccountUsername(line);
          if (!username) continue;
          const deleted = teAutoPyDeletedLine(line);
          const prev = byUsername.get(username);
          byUsername.set(username, { username, deleted: deleted || !!(prev && prev.deleted), enabled: true });
        }
        return Array.from(byUsername.values());
      }
      function teAutoPyAccountQueueUsernames(includeDisabled) {
        if (!includeDisabled && teAutoPyAccountQueueDisabled()) return [];
        return teAutoPyAccountQueueEntries().filter((entry) => !entry.deleted && entry.enabled !== false).map((entry) => entry.username);
      }
      function teAutoPyAccountQueueList(includeDisabled) {
        return teAutoPyAccountQueueUsernames(includeDisabled).map((username) => teAutoPyAccountUrlFromUsername(username)).filter(Boolean);
      }
      function teAutoPyStoredAccountQueueList(entries) {
        const list = Array.isArray(entries) ? entries : teAutoPyAccountQueueEntries();
        return list.map((entry) => entry.deleted ? `${IGBLUE_ACCOUNT_DELETED_PREFIX} ${entry.username}` : entry.username);
      }
      function teAutoPyAccountQueueIndex() {
        const n = Number.parseInt(teLocalGet(teAccountQueueIndexKey(), "0"), 10);
        return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
      }
      function teAutoPySetAccountQueueIndex(n) {
        const v = Math.max(0, Math.floor(Number(n || 0) || 0));
        teLocalSet(teAccountQueueIndexKey(), String(v));
        const input = document.getElementById("igblue_account_queue_index_input");
        if (input && String(input.value || "") !== String(v)) input.value = String(v);
        teAutoPyUpdateAccountQueueStatus();
      }
      function teAutoPyFindAccountQueueIndex(urls, currentUrl) {
        if (!Array.isArray(urls) || !urls.length) return -1;
        for (let i = 0; i < urls.length; i++) if (teAutoPySameAccountUrl(urls[i], currentUrl)) return i;
        return -1;
      }
      function teAutoPyQueueUrlIndexFromUiIndex(uiIndex, urls) {
        const n = Math.max(0, Math.floor(Number(uiIndex || 0) || 0));
        if (n <= 0) return -1;
        return Math.min(n - 1, Math.max(0, (Array.isArray(urls) ? urls.length : 0) - 1));
      }
      let teAccountQueueDirty = false;
      function teAutoPySetAccountQueueDirty(dirty) {
        teAccountQueueDirty = !!dirty;
        const saveBtn = document.getElementById("igblue_account_url_save");
        if (saveBtn) {
          saveBtn.classList.toggle("is-dirty", teAccountQueueDirty);
          saveBtn.textContent = teAccountQueueDirty ? "💾 저장*" : "💾 저장";
          saveBtn.title = teAccountQueueDirty ? "SQLite에 저장하지 않은 수집계정 변경사항이 있습니다." : "수집계정 목록을 Analyze8780 SQLite에 저장합니다.";
        }
      }
      function teAutoPyRenderAccountQueueEntries(entries, cache) {
        const el = document.getElementById("igblue_account_url_list");
        if (!el) return false;
        const lines = teAutoPyStoredAccountQueueList(entries);
        el.value = lines.join("\n");
        if (cache) teLocalSet(teAccountQueueKey(), lines.join("\n"));
        teAutoPyUpdateAccountQueueStatus();
        return true;
      }
      async function teAutoPySaveAccountQueue() {
        const el = document.getElementById("igblue_account_url_list");
        if (!el) return { ok: false, error: "account_queue_missing" };
        const entries = teAutoPyAccountQueueEntries();
        const items = entries.map((entry, index) => ({ username: entry.username, position: index + 1, enabled: !entry.deleted, is_deleted: !!entry.deleted }));
        try {
          const response = await fetch("http://127.0.0.1:8780/api/collection-accounts", {
            method: "PUT",
            headers: { "Content-Type": "application/json" },
            cache: "no-store",
            body: JSON.stringify({ items })
          });
          const payload = await response.json().catch(() => ({}));
          if (!response.ok || !payload || payload.ok !== true) throw new Error(String(payload && payload.error || `HTTP ${response.status}`));
          const savedEntries = (Array.isArray(payload.items) ? payload.items : items).map((item) => ({
            username: teAutoPyNormalizeAccountUsername(item && item.username),
            deleted: !!(item && (item.is_deleted || item.deleted)),
            enabled: item && item.enabled !== false
          })).filter((item) => item.username);
          teAutoPyRenderAccountQueueEntries(savedEntries, true);
          teAutoPySetAccountQueueDirty(false);
          teAutoPySetAccountQueueIndex(Math.min(teAutoPyAccountQueueIndex(), teAutoPyAccountQueueList().length));
          return { ok: true, total: savedEntries.length };
        } catch (error) {
          teAutoPySetAccountQueueDirty(true);
          return { ok: false, error: String(error && error.message || error || "save_failed") };
        }
      }
      function teAutoPyAccountQueueStatus() {
        const disabled = teAutoPyAccountQueueDisabled();
        const usernames = teAutoPyAccountQueueUsernames();
        const urls = usernames.map((username) => teAutoPyAccountUrlFromUsername(username));
        const uiIndex = disabled ? 0 : teAutoPyAccountQueueIndex();
        if (!urls.length) return { has: false, disabled, index: uiIndex, ui_index: uiIndex, url_index: -1, total: 0, url: "", account: "" };
        if (uiIndex <= 0) {
          const curUrl = teCurrentTargetUrl ? String(teCurrentTargetUrl() || "") : "";
          const curAccount = teAccountNameFromAccountUrl(curUrl);
          return { has: true, index: 0, ui_index: 0, url_index: -1, total: urls.length, url: curUrl, account: curAccount, current_page: true };
        }
        const urlIdx = teAutoPyQueueUrlIndexFromUiIndex(uiIndex, urls);
        return { has: true, index: uiIndex, ui_index: uiIndex, url_index: urlIdx, total: urls.length, url: urls[urlIdx] || "", account: usernames[urlIdx] ? "@" + usernames[urlIdx] : "", current_page: false };
      }
      function teAutoPyAccountQueueLabel() {
        const st = teAutoPyAccountQueueStatus();
        if (!(st && st.has)) return "";
        return st.current_page ? `계정 0/${st.total} 현재페이지`.trim() : `계정 ${st.index}/${st.total} ${st.account || ""}`.trim();
      }
      function teAutoPyUpdateAccountQueueStatus() {
        const disabled = teAutoPyAccountQueueDisabled();
        const entries = teAutoPyAccountQueueEntries();
        const activeEntries = entries.filter((entry) => !entry.deleted && entry.enabled !== false);
        const deletedCount = entries.length - activeEntries.length;
        const urls = disabled ? [] : activeEntries.map((entry) => teAutoPyAccountUrlFromUsername(entry.username));
        let uiIndex = disabled ? 0 : teAutoPyAccountQueueIndex();
        if (uiIndex > urls.length) {
          uiIndex = urls.length;
          teLocalSet(teAccountQueueIndexKey(), String(uiIndex));
        }
        const input = document.getElementById("igblue_account_queue_index_input");
        const total = document.getElementById("igblue_account_queue_total");
        const detail = document.getElementById("igblue_account_queue_detail");
        const box = document.getElementById("igblue_account_queue_box");
        const dis = document.getElementById("igblue_account_queue_disabled");
        if (box) box.classList.toggle("is-account-queue-disabled", !!disabled);
        if (dis && dis.checked !== !!disabled) dis.checked = !!disabled;
        if (input) {
          input.disabled = !!disabled;
          if (String(input.value || "") !== String(uiIndex)) input.value = String(uiIndex);
        }
        if (total) total.textContent = String(urls.length || 0);
        if (detail) {
          const deletedText = deletedCount ? ` · 삭제 ${deletedCount}개 제외` : "";
          if (disabled) detail.textContent = `목록 미사용 · SQLite 목록 ${entries.length}개 보존 · 이번 실행 표시는 0/0${deletedText}`;
          else if (!urls.length) detail.textContent = `수집계정 없음 · 0=현재 웹페이지${deletedText}`;
          else if (uiIndex <= 0) detail.textContent = `현재 페이지부터 시작 · 다음은 목록 1/${urls.length}${deletedText}`;
          else {
            const idx = teAutoPyQueueUrlIndexFromUiIndex(uiIndex, urls);
            detail.textContent = `목록 ${uiIndex}/${urls.length} · @${activeEntries[idx] && activeEntries[idx].username || ""}${deletedText}`;
          }
        }
      }
      let teAnalyze8780DeletedPollTimer = null;
      async function teAutoPySyncDeletedAccountsFromAnalyze8780() {
        try {
          const response = await fetch("http://127.0.0.1:8780/api/deleted/accounts", { cache: "no-store" });
          if (!response.ok) return false;
          const payload = await response.json();
          const deleted = new Set((Array.isArray(payload && payload.items) ? payload.items : []).map((item) => teAutoPyNormalizeAccountUsername(item && item.username)).filter(Boolean));
          if (!deleted.size) return true;
          const el = document.getElementById("igblue_account_url_list");
          if (!el || teAccountQueueDirty) return false;
          const entries = teAutoPyAccountQueueEntries();
          let changed = false;
          entries.forEach((entry) => {
            if (deleted.has(entry.username) && !entry.deleted) { entry.deleted = true; entry.enabled = false; changed = true; }
          });
          if (changed) {
            teAutoPyRenderAccountQueueEntries(entries, true);
            teAutoPySetAccountQueueDirty(false);
            teAutoPySetAccountQueueIndex(Math.min(teAutoPyAccountQueueIndex(), teAutoPyAccountQueueList().length));
          }
          return true;
        } catch (_) { return false; }
      }
      function teAutoPyStartAnalyze8780DeletedPolling() {
        if (teAnalyze8780DeletedPollTimer) return;
        teAutoPySyncDeletedAccountsFromAnalyze8780();
        teAnalyze8780DeletedPollTimer = setInterval(teAutoPySyncDeletedAccountsFromAnalyze8780, 30000);
      }
      async function teAutoPyRestoreAccountQueue() {
        const el = document.getElementById("igblue_account_url_list");
        if (!el) return;
        const cached = teLocalGet(teAccountQueueKey(), "");
        if (cached && !String(el.value || "").trim()) el.value = cached;
        if (String(el.value || "").trim()) teAutoPyRenderAccountQueueEntries(teAutoPyAccountQueueEntries(), false);
        try {
          const h = Math.max(28, Math.min(520, parseInt(teLocalGet(teAccountQueueHeightKey(), ""), 10) || 0));
          if (h) el.style.height = h + "px";
          const saveHeight = () => { try { const hh = Math.max(28, Math.min(520, Math.round(el.getBoundingClientRect().height || el.offsetHeight || 0))); if (hh) teLocalSet(teAccountQueueHeightKey(), String(hh)); } catch (_) {} };
          el.addEventListener("mouseup", saveHeight);
          el.addEventListener("blur", saveHeight);
          if (window.ResizeObserver) { const ro = new ResizeObserver(saveHeight); ro.observe(el); }
        } catch (_) {}
        const idxInput = document.getElementById("igblue_account_queue_index_input");
        if (idxInput) {
          idxInput.value = String(teAutoPyAccountQueueIndex());
          const saveIdx = () => teAutoPySetAccountQueueIndex(Math.max(0, Math.floor(Number(idxInput.value || 0) || 0)));
          idxInput.addEventListener("input", saveIdx);
          idxInput.addEventListener("blur", saveIdx);
          idxInput.addEventListener("change", saveIdx);
        }
        const disabledInput = document.getElementById("igblue_account_queue_disabled");
        if (disabledInput) {
          disabledInput.checked = teAutoPyAccountQueueDisabled();
          disabledInput.addEventListener("change", () => {
            teAutoPySetAccountQueueDisabled(!!disabledInput.checked);
            n(disabledInput.checked ? "수집계정 목록 미사용: 0/0으로 처리합니다." : "수집계정 목록 사용: SQLite 목록을 다시 표시합니다.");
          });
        }
        teAutoPySetAccountQueueDirty(false);
        el.addEventListener("input", () => { teAutoPySetAccountQueueDirty(true); teAutoPyUpdateAccountQueueStatus(); });
        const addCur = document.getElementById("igblue_account_url_add_current");
        const addShown = document.getElementById("igblue_account_url_current");
        const saveBtn = document.getElementById("igblue_account_url_save");
        saveBtn && saveBtn.addEventListener("click", async () => {
          const result = await teAutoPySaveAccountQueue();
          if (!result.ok) return void n(`⚠ PC SQLite 저장 실패: ${result.error}`);
          n(`수집계정 ${result.total}개를 PC SQLite에 저장했습니다.`);
        });
        function currentProfileUsername() {
          try {
            const target = String(teCurrentTargetUrl() || "");
            const parsed = new URL(target);
            const parts = String(parsed.pathname || "/").split("/").filter(Boolean);
            const first = String(parts[0] || "");
            if (!first || IGBLUE_ACCOUNT_RESERVED_PATHS.has(first.toLowerCase())) return "";
            if (parts.length > 1 && String(parts[1] || "").toLowerCase() !== "reels") return "";
            return teAutoPyNormalizeAccountUsername(first);
          } catch (_) { return ""; }
        }
        function refreshCurrentUrlAddButton() {
          const username = currentProfileUsername();
          if (addCur) { addCur.disabled = !username; addCur.title = username ? `현재 프로필 계정 추가: @${username}` : "Explore·Reels·게시물 주소는 계정으로 추가하지 않습니다."; }
        }
        addCur && addCur.addEventListener("click", () => {
          const username = currentProfileUsername();
          if (!username) return void n("현재 화면은 계정 프로필이 아닙니다. 표시계정추가를 사용하세요.");
          const entries = teAutoPyAccountQueueEntries();
          if (entries.some((entry) => entry.username === username)) return void n("이미 목록에 있는 계정입니다.");
          entries.push({ username, deleted: false, enabled: true });
          teAutoPyRenderAccountQueueEntries(entries, false);
          teAutoPySetAccountQueueDirty(true);
          teAutoPySetAccountQueueIndex(0);
          n(`@${username} 추가됨. 💾 저장을 눌러 확정하세요.`);
        });
        addShown && addShown.addEventListener("click", () => {
          const mediaList = Array.isArray(oe) && oe.length ? oe : Array.isArray(se) ? se : [];
          const entries = teAutoPyAccountQueueEntries();
          const seen = new Set(entries.map((entry) => entry.username));
          let added = 0;
          mediaList.forEach((media) => {
            const username = teAutoPyNormalizeAccountUsername(teOwnerAccountForMedia ? teOwnerAccountForMedia(media) : media && (media.username || media.owner_username));
            if (!username || seen.has(username)) return;
            seen.add(username);
            entries.push({ username, deleted: false, enabled: true });
            added += 1;
          });
          if (!added) return void n("현재 표시된 게시물에서 새 IG account를 찾지 못했습니다.");
          teAutoPyRenderAccountQueueEntries(entries, false);
          teAutoPySetAccountQueueDirty(true);
          teAutoPySetAccountQueueIndex(0);
          n(`현재 표시된 IG account ${added}개를 추가했습니다. 💾 저장을 눌러 확정하세요.`);
        });
        refreshCurrentUrlAddButton();
        teAutoPyUpdateAccountQueueStatus();
        try {
          const response = await fetch("http://127.0.0.1:8780/api/collection-accounts", { cache: "no-store" });
          const payload = await response.json().catch(() => ({}));
          if (!response.ok || payload.ok !== true) throw new Error(String(payload.error || `HTTP ${response.status}`));
          if (teAccountQueueDirty) return;
          const sqliteEntries = (Array.isArray(payload.items) ? payload.items : []).map((item) => ({
            username: teAutoPyNormalizeAccountUsername(item && item.username),
            deleted: !!(item && (item.is_deleted || item.deleted)),
            enabled: item && item.enabled !== false
          })).filter((item) => item.username);
          if (sqliteEntries.length) {
            teAutoPyRenderAccountQueueEntries(sqliteEntries, true);
            teAutoPySetAccountQueueDirty(false);
          } else if (teAutoPyAccountQueueEntries().length) {
            teAutoPySetAccountQueueDirty(true);
            n("기존 브라우저 목록을 불러왔습니다. 💾 저장을 눌러 PC SQLite로 이전하세요.");
          }
        } catch (error) {
          if (teAutoPyAccountQueueEntries().length) n(`8780 연결 실패 · 브라우저 캐시 사용 중: ${String(error && error.message || error)}`);
        }
        teAutoPyStartAnalyze8780DeletedPolling();
      }
      async function teAutoPyNavigateAccountUrl(url, mySeq, reason, index, total) {
        const targetUrl = teAutoPyNormalizeAccountUrl(url);
        if (!targetUrl) return { ok: false, error: "invalid_account_url" };
        if (mySeq && mySeq !== teAutoPyRunSeq) return { ok: false, error: "run_seq_changed" };
        const label = `${Math.max(1, Number(index || 0) || 1)}/${Number(total || 0) || "?"}`;
        const account = teAccountNameFromAccountUrl(targetUrl);
        teProgressResetForContext && teProgressResetForContext("account_navigation");
        teAutoPyAnnounce(`계정 ${label} 이동`, `${account} · ${targetUrl}`);
        await teAutoPyCycle("S00_ACCOUNT_NAVIGATE", { cycle_id: teBuildCycleId("ACCOUNT_NAV"), run_seq: mySeq || 0, decision: "navigate", action: "ndy_goto", reason: String(reason || "account_queue"), account_queue_index: Number(index || 0), account_queue_total: Number(total || 0), account_url: targetUrl, account_username: account.replace(/^@/, "") });
        const ok = await teSendToTargetTab({ type: "ndy_goto", url: targetUrl, newpage: 0 }, "Instagram tab not found");
        if (!ok) return { ok: false, error: "goto_failed" };
        if (teChannelLocked()) {
          tePinnedTargetUrl = targetUrl;
          teLocalSet("target/pinned_url", targetUrl);
          teSetTargetLabel({ tabId: tePinnedTargetTabId || teCurrentTargetTabId || 0, url: targetUrl });
        }
        teDownloadSessionAccount = teSafePath(account.replace(/^@/, "") || teAccountFromUrl(targetUrl) || "instagram");
        teAutoPySetLastRank(0, "account_queue_navigation_reset");
        await teSleep(4500);
        try { teRequestTargetState(true); } catch (_) {}
        try { teRefreshRanksAndMetricCount(); } catch (_) {}
        await teAutoPyCycle("S00_ACCOUNT_NAVIGATED", { cycle_id: teBuildCycleId("ACCOUNT_NAV_OK"), run_seq: mySeq || 0, decision: "navigated", action: "wait_page_ready", reason: String(reason || "account_queue"), account_queue_index: Number(index || 0), account_queue_total: Number(total || 0), account_url: targetUrl, account_username: account.replace(/^@/, "") });
        return { ok: true, url: targetUrl, account };
      }
      async function teAutoPyPrepareAccountQueueForStart(mySeq) {
        const urls = teAutoPyAccountQueueList();
        const uiIndex = teAutoPyAccountQueueIndex();
        if (!urls.length) return { ok: true, skipped: true, has_queue: false, index: uiIndex, total: 0 };
        if (uiIndex <= 0) {
          const curUrl = teCurrentTargetUrl ? String(teCurrentTargetUrl() || "") : "";
          teAutoPySetAccountQueueIndex(0);
          teDownloadSessionAccount = teSafePath(teAccountFromUrl(curUrl) || "instagram");
          teAutoPyAnnounce(`계정 0/${urls.length} 준비`, "현재 웹페이지부터 시작");
          return { ok: true, url: curUrl, index: 0, total: urls.length, has_queue: true, current_page: true };
        }
        let urlIdx = teAutoPyQueueUrlIndexFromUiIndex(uiIndex, urls);
        teAutoPySetAccountQueueIndex(Math.min(uiIndex, urls.length));
        const wanted = urls[urlIdx];
        const account = teAccountNameFromAccountUrl(wanted);
        teAutoPyAnnounce(`계정 ${Math.min(uiIndex, urls.length)}/${urls.length} 준비`, account || wanted);
        if (teAutoPySameAccountUrl(wanted, teCurrentTargetUrl())) {
          teDownloadSessionAccount = teSafePath(account.replace(/^@/, "") || teAccountFromUrl(wanted) || "instagram");
          return { ok: true, url: wanted, index: Math.min(uiIndex, urls.length), url_index: urlIdx, total: urls.length, has_queue: true };
        }
        return Object.assign({ has_queue: true }, await teAutoPyNavigateAccountUrl(wanted, mySeq, "auto_start_prepare_account", Math.min(uiIndex, urls.length), urls.length));
      }
      function teAutoPyScheduleRestartAfterAccount(mySeq, reason) {
        setTimeout(() => {
          try {
            if (mySeq && mySeq !== teAutoPyRunSeq) return;
            if (teAutoPyRunning || teAutoPyStopping) return;
            if (!teAutoPyEnabled()) return;
            teAutoPyStartOnceFromUi();
          } catch (err) { n("다음 계정 자동시작 실패 " + String(err && err.message || err)); }
        }, 1900);
      }
      function teAutoPyFinishAccountQueue(reason) {
        try {
          const cb = document.getElementById("igblue_auto_py_down");
          if (cb) cb.checked = false;
          teLocalSet("download/auto_py", "0");
          teAutoPyUpdateButtons(teAutoPyLastPyOk);
        } catch (_) {}
        try { teAutoPySetAccountQueueIndex(0); } catch (_) {}
        teAutoPyAnnounce("계정목록 전체 완료", String(reason || "끝까지 처리하여 자동 중단") + " · 시작 위치 0 복귀");
      }
      async function teAutoPySkipCurrentAccountAndAdvanceIfAny(mySeq, reason, detail, extra) {
        const urls = teAutoPyAccountQueueList();
        if (!urls.length) return { advanced: false, empty: true };
        const uiIndex = teAutoPyAccountQueueIndex();
        const payload = Object.assign({
          cycle_id: teBuildCycleId("ACCOUNT_SKIP"),
          run_seq: Number(mySeq || 0),
          decision: "skip_current_account",
          action: "next_account",
          reason: String(reason || "no_matching_media"),
          skip_reason: String(reason || "no_matching_media"),
          skip_reason_ko: String(detail || "조건에 맞는 대상 없음: 현재 계정 건너뛰고 다음 URL 이동"),
          account_queue_index: Number(uiIndex || 0),
          account_queue_total: urls.length,
          account_url: Number(uiIndex || 0) > 0 ? (urls[Math.max(0, uiIndex - 1)] || "") : (teCurrentTargetUrl ? String(teCurrentTargetUrl() || "") : "")
        }, extra || {});
        await teAutoPyCycle("S00_ACCOUNT_TARGET_EMPTY_SKIP", payload);
        n("조건에 맞는 대상 없음: 현재 계정 건너뛰고 다음 URL로 이동");
        return await teAutoPyAdvanceAccountQueueIfAny(mySeq, reason || "no_matching_media");
      }

      async function teAutoPyMediaFailAdvanceOrStop(mySeq, stage, cycleBase, code, detail, extra) {
        const payload = Object.assign({}, cycleBase || {}, extra || {}, {
          run_seq: Number(mySeq || 0),
          decision: "required_download_failed_stop",
          action: "pause_without_next",
          reason: String(code || "media_click_failed"),
          fail_policy: "stop_not_advance",
          next_account_blocked: true,
          required_download_failed: true
        });
        return await teAutoPyStopCycle(stage || "S_AUTOPLAY_STOPPED", payload, code || "media_click_failed", detail || "media_click_failed", { fail_policy: "stop_not_advance", next_account_blocked: true, required_download_failed: true });
      }

      async function teAutoPyAdvanceAccountQueueIfAny(mySeq, reason) {
        const urls = teAutoPyAccountQueueList();
        if (!urls.length) return { advanced: false, empty: true };
        let uiIndex = teAutoPyAccountQueueIndex();
        const nextUiIndex = uiIndex + 1;
        if (nextUiIndex > urls.length) {
          teAutoPySetAccountQueueIndex(uiIndex);
          await teAutoPyCycle("S00_ACCOUNT_QUEUE_DONE", { cycle_id: teBuildCycleId("ACCOUNT_DONE"), run_seq: mySeq || 0, decision: "queue_done", action: "stop", reason: String(reason || "account_queue_completed"), account_queue_index: uiIndex, account_queue_total: urls.length, account_url: uiIndex > 0 ? urls[uiIndex - 1] || "" : "current_page" });
          teAutoPyFinishAccountQueue("마지막 계정까지 완료");
          return { advanced: false, completed_all: true, index: uiIndex, total: urls.length };
        }
        const urlIdx = teAutoPyQueueUrlIndexFromUiIndex(nextUiIndex, urls);
        teAutoPySetAccountQueueIndex(nextUiIndex);
        const nav = await teAutoPyNavigateAccountUrl(urls[urlIdx], mySeq, reason || "account_queue_next", nextUiIndex, urls.length);
        if (!(nav && nav.ok)) {
          await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { cycle_id: teBuildCycleId("ACCOUNT_NAV_FAIL"), run_seq: mySeq || 0, account_queue_index: nextUiIndex, account_queue_total: urls.length, account_url: urls[urlIdx], navigate_result: nav || null }, "account_navigation_failed", String(nav && nav.error || "goto_failed"));
          return { advanced: false, error: String(nav && nav.error || "goto_failed"), index: nextUiIndex, total: urls.length };
        }
        teAutoPyAnnounce("다음 계정 자동시작 대기", `${nextUiIndex}/${urls.length} ${teAccountNameFromAccountUrl(urls[urlIdx])}`);
        teAutoPyScheduleRestartAfterAccount(mySeq, reason || "account_queue_next");
        return { advanced: true, index: nextUiIndex, total: urls.length };
      }

      function teSortedAllTopN(e) {
        if (!Array.isArray(se) || 0 === se.length) return [];
        const t = Math.min(e, se.length),
          r = G[Z] || "";
        let n = [...se];
        return (
          r &&
            (n = n.sort((e, t) => {
              const n = e[r],
                i = t[r];
              return void 0 === n && void 0 === i
                ? 0
                : void 0 === n
                  ? 1
                  : void 0 === i
                    ? -1
                    : i - n;
            })),
          n.slice(0, t)
        );
      }
      // IGblue local patch v31: robust numeric parsing for rank/count/bar values.
      function teMetricNumber(e) {
        if (null == e) return 0;
        if (typeof e === "number") return Number.isFinite(e) ? e : 0;
        const t = String(e).replace(/[,\s]/g, "").trim();
        const r = Number(t);
        return Number.isFinite(r) ? r : 0;
      }
      // IGblue local patch v12: threshold/count/rank helper for the current Sort by metric.
      function teTopMetricField() {
        const e = G[Z] || "";
        return ["like_count", "comment_count", "media_repost_count", "play_count"].includes(e) ? e : "";
      }
      function teMetricLabel(e) {
        return "like_count" === e ? "Likes" : "comment_count" === e ? "Comments" : "media_repost_count" === e ? "Reposts" : "play_count" === e ? "Views" : "Metric";
      }
      function teServerSortField(e) {
        return "like_count" === e ? "likes" : "comment_count" === e ? "comments" : "media_repost_count" === e ? "reposts" : "play_count" === e ? "play_count" : "created_at" === e ? "upload_at" : e || "";
      }
      function teSourceTypeFromUrl(url) {
        const s = String(url || teCurrentTargetUrl() || location.href || "");
        if (/instagram\.com\/explore/i.test(s)) return "explore";
        if (/instagram\.com\/(?:reel|reels)\//i.test(s)) return "reel";
        if (/instagram\.com\/p\//i.test(s)) return "post";
        if (/instagram\.com\/[^/?#]+\/reels/i.test(s)) return "profile_reels";
        if (/instagram\.com\/[^/?#]+/i.test(s)) return "profile";
        return "instagram";
      }
      function teThresholdLabelForMetric(rawField, threshold) {
        const n = Number(threshold || 0) || 0;
        return n > 0 ? `${teMetricLabel(rawField)} >= ${teShortNumber(n)}` : "";
      }
      function teMetricUnits() {
        // IGblue local patch v14: the visible first input is the threshold in 10k units.
        const e = document.getElementById("top_n_count"),
          t = Number.parseFloat(e?.value);
        return Number.isFinite(t) && t > 0 ? t : 0;
      }
      function teMetricThresholdValue() {
        return Math.floor(1e4 * teMetricUnits());
      }
      function teShortNumber(e) {
        return e >= 1e6 ? `${Number(e / 1e6).toLocaleString()}M` : e >= 1e3 ? `${Number(e / 1e3).toLocaleString()}k` : String(e);
      }
      function teAutoPyDateFilterActive() {
        try { return Math.max(0, Number(K || 0) || 0) > 0; } catch (_) { return false; }
      }
      function teMetricSourceList() {
        // 날짜 필터가 활성화된 경우 oe=[] 자체가 정상적인 "필터 결과 0개"다.
        // 이때 전체 원본 se로 되돌아가면 오래된 게시물이 No001로 다시 살아난다.
        if (teAutoPyDateFilterActive()) return Array.isArray(oe) ? oe : [];
        return Array.isArray(oe) && oe.length ? oe : Array.isArray(se) ? se : [];
      }
      function teBuildRankContext(e) {
        const t = teTopMetricField(),
          r = teMetricThresholdValue(),
          n = new Map();
        if (!t || r <= 0 || !Array.isArray(e)) return { active: !1, map: n };
        let i = 0;
        return (e.forEach((e) => {
          const s = teMetricNumber(e?.[t]);
          s >= r && e?.code && n.set(e.code, ++i);
        }), { active: !0, map: n });
      }
      function teRankAccountText(media) {
        const account = teDisplayAccountForMedia ? teDisplayAccountForMedia(media) : "";
        const clean = teCleanOwnerAccountName(account);
        return clean ? String(clean) : "";
      }
      function teApplySideAccountLabel(element, media) {
        if (!element) return;
        const span = element.querySelector("span");
        if (!span) return;
        const account = teRankAccountText(media);
        element.style.display = "flex";
        element.classList.remove("igblue-rank-badge");
        span.textContent = account ? `IG ${account}` : "IG";
      }
      function teApplySideRankLabel(element, no, active) {
        if (!element) return;
        const span = element.querySelector("span");
        if (!span) return;
        const noText = teRankNoText(no);
        const settings = teRankBadgeSettings();
        const visible = !!(active && noText && settings.enabled && settings.side);
        element.style.display = visible ? "flex" : "none";
        span.textContent = visible ? noText : "";
        element.classList.toggle("igblue-rank-badge", visible);
        if (visible) teApplyRankBadgeVars(element, settings);
        else teClearRankBadgeVars(element);
      }
      function teRankNoText(no) {
        const n = Number(no) || 0;
        return n > 0 ? `No ${String(n).padStart(3, "0")}` : "";
      }
      function teMediaKindText(media) {
        if (media?.video_url) return "VID";
        if (media?.carousel_media) return "CAR";
        if (media?.img_origin || media?.img_small || media?.img_b64) return "IMG";
        return "UNK";
      }
      let teMainRankLabelsTimer = null;
      let teMainRankLabelsSignature = "";
      let teMainRankLabelsLastUrl = "";
      function teBuildMainRankLabels(list, ctx) {
        if (!Array.isArray(list)) return [];
        const active = !!(ctx && ctx.active);
        const map = ctx && ctx.map instanceof Map ? ctx.map : new Map();
        const labels = list.map((media, idx) => {
          const no = active ? Number(map.get(media?.code) || 0) : idx + 1;
          if (!media?.code || no <= 0) return null;
          return {
            code: media.code,
            username: teOwnerAccountForMedia(media),
            owner_username: teOwnerAccountForMedia(media),
            upload_stamp: teUploadStampFromMedia(media),
            is_single_media_page: teIsSingleMediaPage(),
            no,
            no_text: teRankNoText(no),
            kind: teMediaKindText(media),
            has_image: !!(media?.img_origin || media?.img_small || media?.img_b64),
            has_video: !!media?.video_url,
            has_carousel: !!media?.carousel_media,
            likes: teMetricNumber(media?.like_count),
            comments: teMetricNumber(media?.comment_count),
            reposts: teMetricNumber(media?.media_repost_count),
            views: teMetricNumber(media?.play_count),
            download_prefix: teMakeDownloadPrefix(media, 0, 1, media?.video_url || media?.img_origin || media?.img_small || ""),
            download_prefix_image: teMakeDownloadPrefix(media, 0, 1, media?.img_origin || media?.img_small || media?.video_url || ""),
            download_prefix_video: teMakeDownloadPrefix(media, 0, 1, media?.video_url || media?.img_origin || media?.img_small || ""),
            instagram_original_filename: teOriginalInstagramFileInfo(media?.video_url || media?.img_origin || media?.img_small || "").original_filename,
            download_account: teActiveFolderAccountName(),
            download_root: tePersistDownloadRootNow(),
            anchor_id: `No${String(no).padStart(3, "0")}`,
            next_anchor_id: `No${String(no + 1).padStart(3, "0")}`
          };
        }).filter(Boolean);
        const total = labels.length;
        labels.forEach((x) => {
          x.rank_total = total;
          x.rank_ready = true;
          x.prev_anchor_id = `No${String(Math.max(1, Number(x.no || 1) - 1)).padStart(3, "0")}`;
          x.next_anchor_id = `No${String(Math.min(total || Number(x.no || 1), Number(x.no || 1) + 1)).padStart(3, "0")}`;
        });
        return labels;
      }
      function teMainRankLabelSignature(labels) {
        const url = teCurrentTargetUrl ? String(teCurrentTargetUrl() || "") : "";
        return url + "\n" + (Array.isArray(labels) ? labels.map(x => `${x.code}:${x.no}:${x.rank_total || 0}:${x.username}:${x.kind}:${x.download_prefix || ""}:${x.download_root || ""}:${x.download_account || ""}`).join("|") : "");
      }
      function teSendMainRankLabelsNow(labels, reason) {
        const payload = { type: "ndy_ig_main_rank_labels_changed", labels: Array.isArray(labels) ? labels : [], reason: String(reason || "rank_labels") };
        return teSendToTargetTab(payload, "").catch(() => {});
      }
      function teScheduleMainRankLabels(list, ctx, forceReason) {
        const labels = teBuildMainRankLabels(list, ctx);
        const url = teCurrentTargetUrl ? String(teCurrentTargetUrl() || "") : "";
        const signature = teMainRankLabelSignature(labels);
        const force = !!forceReason || url !== teMainRankLabelsLastUrl;
        teMainRankLabelsLastUrl = url;
        if (!force && signature === teMainRankLabelsSignature) return;
        teMainRankLabelsSignature = signature;
        if (teMainRankLabelsTimer) clearTimeout(teMainRankLabelsTimer);
        teMainRankLabelsTimer = setTimeout(() => {
          teMainRankLabelsTimer = null;
          teSendMainRankLabelsNow(labels, forceReason || "rank_labels_changed");
          setTimeout(() => teSendMainRankLabelsNow(labels, "rank_labels_rehydrate_1"), 650);
          setTimeout(() => teSendMainRankLabelsNow(labels, "rank_labels_rehydrate_2"), 1600);
        }, 80);
      }
      async function teAutoPyEnsureMainInjectionReady(labels, requestedNo, runSeq) {
        const arr = Array.isArray(labels) ? labels : [];
        const reqNo = Math.max(1, Number(requestedNo || 1) || 1);
        if (!arr.length) return { ok: false, error: "no_rank_labels" };
        for (let attempt = 1; attempt <= 5; attempt++) {
          await teSendMainRankLabelsNow(arr, `auto_py_main_rehydrate_${attempt}`);
          await teSleep(attempt === 1 ? 280 : 520);
          try { await teSendToTargetTabResponse({ type: "igblue_auto_py_go_no", no: reqNo, wait_ms: 320 }, "", 1600); } catch (_) {}
          const st = await teSendToTargetTabResponse({ type: "igblue_get_main_injection_state", requested_no: reqNo, wait_ms: 160 }, "", 1600).catch((err) => ({ ok: false, error: String(err && err.message || err) }));
          if (st && st.ok && st.panel_found && st.current_button_found && (st.has_requested || Number(st.current_rank_no || 0) === reqNo || Number(st.loaded_max_no || 0) >= reqNo)) {
            return Object.assign({ attempt }, st);
          }
          if (runSeq && runSeq !== teAutoPyRunSeq) return { ok: false, error: "run_seq_changed", attempt, state: st || null };
        }
        const finalState = await teSendToTargetTabResponse({ type: "igblue_get_main_injection_state", requested_no: reqNo, wait_ms: 160 }, "", 1600).catch((err) => ({ ok: false, error: String(err && err.message || err) }));
        return Object.assign({ ok: false, error: "main_injection_not_ready" }, finalState || {});
      }
      function teUpdateMetricThresholdCount() {
        const t = document.getElementById("top_metric_label"),
          r = document.getElementById("top_n_count"),
          n = teTopMetricField(),
          i = teMetricThresholdValue(),
          s = teMetricSourceList();
        r && (teLocalSet("filter/metric_threshold_10k", r.value || ""), teLocalSet("top_n_count", r.value || ""));
        t && (t.textContent = "(10k)");
        if (!n || i <= 0) {
          teSetTopMetricCountValue(0, "조회수 기준에 맞는 실제 게시물 개수");
          return;
        }
        const o = s.filter((e) => {
          const t = teMetricNumber(e?.[n]);
          return t >= i;
        }).length;
        teSetTopMetricCountValue(o, `${teMetricLabel(n)} >= ${i.toLocaleString()} count`);
      }
      function teRefreshRanksAndMetricCount() {
        teUpdateMetricThresholdCount();
        Array.isArray(oe) && ee(oe);
      }
      function teRestoreMetricThreshold() {
        const e = document.getElementById("top_n_count");
        if (!e) return;
        const r = teLocalGetAny(["filter/metric_threshold_10k", "top_metric_units", "top_n_count"], e.value || "10.0");
        "" !== String(r) && (e.value = String(r));
        teSetTopMetricCountValue(teLocalGetAny(["filter/metric_count", "top_metric_count"], "0"), "조회수 기준에 맞는 실제 게시물 개수");
        const n = () => {
          teLocalSet("filter/metric_threshold_10k", e.value || "");
          teLocalSet("top_metric_units", e.value || "");
          teLocalSet("top_n_count", e.value || "");
          teApplyMetricFolderSuggestion("metric_threshold_change");
          teRefreshRanksAndMetricCount();
        };
        e.addEventListener("input", n);
        e.addEventListener("blur", n);
        e.addEventListener("change", n);
        const autoTarget = document.getElementById("igblue_auto_py_target_count");
        if (autoTarget) {
          const savedTarget = teLocalGetAny(["download/auto_py_target_count", "auto_py_target_count"], autoTarget.value || "100");
          if (String(savedTarget || "") !== "") autoTarget.value = String(savedTarget);
          autoTarget.addEventListener("input", teAutoPyTargetCountSave);
          autoTarget.addEventListener("blur", teAutoPyTargetCountSave);
          autoTarget.addEventListener("change", teAutoPyTargetCountSave);
        }
        teUpdateMetricThresholdCount();
      }
      // IGblue local patch: popup window support. Chrome's native side-panel title bar cannot be edited,
      // so side.html exposes a one-time popout button inside the page.
      let teCurrentTargetTabId = 0;
      let teHasInstagramTarget = !1;
      let tePinnedTargetTabId = 0;
      let tePinnedTargetUrl = "";
      // v223: automation owner-heart gating retired. Target tab ID remains routing metadata only.
      let teDownloadSessionAccount = "";
      let teDownloadSessionAccountFolderMerge = null;
      let teDownloadStopRequested = !1;
      let teDownloadResumeState = null;
      const IGBLUE_TAB_SESSION_KEY = "target/tab_sessions_v179a12";
      const IGBLUE_TAB_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
      function teReadTabSessions() {
        try { const obj = JSON.parse(teLocalGet(IGBLUE_TAB_SESSION_KEY, "{}") || "{}"); return obj && typeof obj === "object" ? obj : {}; } catch (_) { return {}; }
      }
      function teWriteTabSessions(obj) { try { teLocalSet(IGBLUE_TAB_SESSION_KEY, JSON.stringify(obj || {})); } catch (_) {} }
      function tePruneTabSessions() {
        const now = Date.now();
        const obj = teReadTabSessions();
        let changed = false;
        Object.keys(obj).forEach((k) => {
          const last = Number(obj[k] && (obj[k].last_seen_at_ms || obj[k].updated_at_ms || obj[k].started_at_ms) || 0);
          if (!last || now - last > IGBLUE_TAB_SESSION_TTL_MS) { delete obj[k]; changed = true; }
        });
        if (changed) teWriteTabSessions(obj);
        return obj;
      }
      function teCurrentGuardTabId() { return Number((teChannelLocked && teChannelLocked() && tePinnedTargetTabId > 0) ? tePinnedTargetTabId : teCurrentTargetTabId) || 0; }
      function teCurrentWsTargetPayload(reason, extra) {
        try {
          const tabId = teCurrentGuardTabId();
          const url = String((teChannelLocked && teChannelLocked() && tePinnedTargetUrl) ? tePinnedTargetUrl : (teCurrentTargetUrl ? teCurrentTargetUrl() : "") || "");
          return Object.assign({
            tab_id: tabId,
            request_tab_id: tabId,
            window_id: 0,
            url,
            href: url,
            account_url: url,
            account_username: teAccountFromUrl ? String(teAccountFromUrl(url) || "") : "",
            request_id: "side_" + Date.now() + "_" + Math.floor(Math.random() * 100000),
            request_reason: String(reason || "side_request"),
            build_version: IGBLUE_BUILD_VERSION,
            build_tag: IGBLUE_BUILD_TAG
          }, extra || {});
        } catch (_) {
          return Object.assign({ tab_id: 0, request_reason: String(reason || "side_request") }, extra || {});
        }
      }
      function teHandleNotActiveTargetResponse(res, reason) {
        return false;
      }
      function teUpdateTabIdBadge() {
        try {
          const id = teCurrentGuardTabId();
          const url = String((teChannelLocked && teChannelLocked() && tePinnedTargetUrl) ? tePinnedTargetUrl : (teCurrentTargetUrl ? teCurrentTargetUrl() : "") || "");
          const sessions = tePruneTabSessions();
          const session = id ? sessions[String(id)] || null : null;
          document.querySelectorAll(".igblue-tab-id-badge").forEach((el) => {
            el.textContent = `tab_id: ${id || "-"}`;
            el.title = `tab_id: ${id || "-"}
state: ${(session && session.on) ? "on" : "off"}
url: ${url || "-"}`;
          });
        } catch (_) {}
      }
      function teTouchTabSession(on, reason) {
        try {
          const id = teCurrentGuardTabId();
          if (!id) { teUpdateTabIdBadge(); return; }
          const now = Date.now();
          const url = String((teChannelLocked && teChannelLocked() && tePinnedTargetUrl) ? tePinnedTargetUrl : (teCurrentTargetUrl ? teCurrentTargetUrl() : "") || "");
          const obj = tePruneTabSessions();
          const prev = obj[String(id)] || {};
          obj[String(id)] = Object.assign({}, prev, {
            tab_id: id,
            url,
            username: teAccountFromUrl ? String(teAccountFromUrl(url) || "") : "",
            on: !!on,
            reason: String(reason || (on ? "on" : "off")),
            started_at_ms: on ? (prev.started_at_ms || now) : (prev.started_at_ms || now),
            started_at: on ? (prev.started_at || new Date(now).toISOString()) : (prev.started_at || new Date(now).toISOString()),
            last_seen_at_ms: now,
            last_seen_at: new Date(now).toISOString()
          });
          if (!on) {
            obj[String(id)].ended_at_ms = now;
            obj[String(id)].ended_at = new Date(now).toISOString();
          }
          teWriteTabSessions(obj);
          teUpdateTabIdBadge();
        } catch (_) {}
      }
      function tePayloadAccountFromUrl(url) { try { return teSafePath(teAccountFromUrl(url || "") || ""); } catch (_) { return ""; } }
      function teShouldAcceptSideShopPayload(msg) {
        const tabId = Number(msg && msg.tabId || 0) || 0;
        const url = String(msg && (msg.url || msg.source_url || msg.href) || "");
        const targetTab = teCurrentGuardTabId();
        if (targetTab > 0 && tabId > 0 && tabId !== targetTab) return { ok: false, reason: "tab_id_mismatch", tab_id: tabId, target_tab_id: targetTab, url };
        if (teChannelLocked && teChannelLocked() && targetTab > 0 && !tabId) return { ok: false, reason: "missing_tab_id_when_locked", target_tab_id: targetTab, url };
        const targetUrl = String((teChannelLocked && teChannelLocked() && tePinnedTargetUrl) ? tePinnedTargetUrl : (teCurrentTargetUrl ? teCurrentTargetUrl() : "") || "");
        if (url && targetUrl && teIsInstagramUrl(url) && teIsInstagramUrl(targetUrl)) {
          const a = tePayloadAccountFromUrl(url);
          const b = tePayloadAccountFromUrl(targetUrl);
          if (a && b && a !== b && teChannelLocked && teChannelLocked()) return { ok: false, reason: "account_mismatch", tab_id: tabId, target_tab_id: targetTab, url, target_url: targetUrl, account: a, target_account: b };
        }
        return { ok: true, tab_id: tabId, target_tab_id: targetTab, url };
      }
      function teIsPopupMode() {
        try { return new URLSearchParams(location.search).get("popup") === "1"; } catch { return !1; }
      }
      function teIsInstagramUrl(e) {
        try {
          const t = new URL(e || "");
          return (t.protocol === "https:" || t.protocol === "http:") && t.hostname.replace(/^www\./i, "").toLowerCase() === "instagram.com";
        } catch {
          return !1;
        }
      }
      function teShortIgUrl(e) {
        try {
          const t = new URL(e || "");
          if (!/instagram\.com$/i.test(t.hostname.replace(/^www\./i, ""))) return "No active Instagram tab";
          const r = t.pathname || "/";
          const n = r.split("/").filter(Boolean)[0] || "home";
          return `Linked: ${n}  ·  ${t.origin}${r}`;
        } catch {
          return "No active Instagram tab";
        }
      }
      // IGblue local patch v17: channel lock freezes target tab/account and bulk download folder.
      function teChannelLocked() {
        const e = document.getElementById("channel_lock");
        return !!e?.checked;
      }
      function tePinnedAccountName() {
        return teSafePath(teAccountFromUrl(tePinnedTargetUrl || teCurrentTargetUrl()) || "instagram");
      }
      function tePinTarget(e) {
        const t = e && e.url ? String(e.url) : teCurrentTargetUrl();
        if (teIsInstagramUrl(t)) {
          tePinnedTargetUrl = t;
          tePinnedTargetTabId = Number(e?.tabId || teCurrentTargetTabId || 0);
          teLocalSet("target/pinned_url", tePinnedTargetUrl);
          teLocalSet("target/pinned_tab_id", tePinnedTargetTabId || "");
          teUpdateDownloadFolderPath(tePinnedAccountName());
        }
      }
      const teChannelLockTabMissingKo = "채널고정 탭이 없어 채널고정을 취소합니다.";
      let teChannelLockLastRemovedLogAt = 0;
      function teChannelLockWsLog(reason, detail) {
        try {
          const payload = Object.assign({
            schema: IGBLUE_CYCLE_SCHEMA,
            build_version: IGBLUE_BUILD_VERSION,
            build_tag: IGBLUE_BUILD_TAG,
            cycle_id: teBuildCycleId("CHANNEL_LOCK_TAB_REMOVED"),
            cycle_stage: "S00_CHANNEL_LOCK_TAB_REMOVED",
            decision: "channel_lock_disabled",
            action: "channel_lock_auto_uncheck",
            reason: String(reason || "pinned_instagram_tab_removed"),
            message: teChannelLockTabMissingKo,
            message_ko: teChannelLockTabMissingKo,
            pinned_tab_id: Number(detail && detail.tabId || 0) || 0,
            pinned_url: String(detail && detail.url || ""),
            href: teCurrentTargetUrl(),
            host: teDownTxtHost()
          }, detail || {});
          return teAiwkWsRequest("ig_auto_cycle_state", payload, 1200).catch(() => null);
        } catch (_) {
          return Promise.resolve(null);
        }
      }
      function teDisableChannelLockForMissingTab(reason, detail) {
        const now = Date.now();
        const oldId = Number(detail && detail.tabId || tePinnedTargetTabId || 0) || 0;
        const oldUrl = String(detail && detail.url || tePinnedTargetUrl || "");
        const cb = document.getElementById("channel_lock");
        if (cb) cb.checked = !1;
        teLocalSet("target/channel_lock", "0");
        tePinnedTargetTabId = 0;
        tePinnedTargetUrl = "";
        teLocalSet("target/pinned_url", "");
        teLocalSet("target/pinned_tab_id", "");
        teSetTargetLabel(null);
        teUpdateDownloadFolderPath();
        if (!teDownloadSessionAccount) teLoadDownTxtLocal(teActiveFolderAccountName());
        P();
        n(teChannelLockTabMissingKo);
        if (now - teChannelLockLastRemovedLogAt > 1500) {
          teChannelLockLastRemovedLogAt = now;
          teChannelLockWsLog(reason || "pinned_instagram_tab_removed", Object.assign({}, detail || {}, { tabId: oldId, url: oldUrl }));
        }
      }
      function teValidatePinnedTargetAlive(reason, cb) {
        const pinnedId = Number(tePinnedTargetTabId || 0) || 0;
        if (!(teChannelLocked() && pinnedId > 0)) {
          cb && cb(true);
          return;
        }
        try {
          chrome.tabs.get(pinnedId, (tab) => {
            if (chrome.runtime.lastError || !tab || !tab.id) {
              teDisableChannelLockForMissingTab(reason || "pinned_instagram_tab_removed", { tabId: pinnedId, url: tePinnedTargetUrl, error: chrome.runtime.lastError && chrome.runtime.lastError.message || "tab_missing" });
              cb && cb(false);
              return;
            }
            const url = String(tab.url || tePinnedTargetUrl || "");
            if (!teIsInstagramUrl(url)) {
              teDisableChannelLockForMissingTab("pinned_instagram_tab_not_instagram", { tabId: pinnedId, url });
              cb && cb(false);
              return;
            }
            tePinnedTargetUrl = url;
            teLocalSet("target/pinned_url", tePinnedTargetUrl);
            cb && cb(true);
          });
        } catch (err) {
          teDisableChannelLockForMissingTab(reason || "pinned_instagram_tab_removed", { tabId: pinnedId, url: tePinnedTargetUrl, error: String(err && err.message || err) });
          cb && cb(false);
        }
      }
      function teInitChannelLock() {
        const e = document.getElementById("channel_lock");
        if (!e) return;
        e.checked = "1" === teLocalGet("target/channel_lock", "1");
        tePinnedTargetUrl = teLocalGet("target/pinned_url", "");
        tePinnedTargetTabId = Number(teLocalGet("target/pinned_tab_id", "0") || 0);
        if (e.checked && tePinnedTargetTabId > 0) teValidatePinnedTargetAlive("channel_lock_init", () => {});
        e.addEventListener("change", () => {
          teLocalSet("target/channel_lock", e.checked ? "1" : "0");
          if (e.checked) tePinTarget({ tabId: teCurrentTargetTabId, url: teCurrentTargetUrl() });
          else {
            tePinnedTargetUrl = "";
            tePinnedTargetTabId = 0;
            teLocalSet("target/pinned_url", "");
            teLocalSet("target/pinned_tab_id", "");
            teRequestTargetState(teIsPopupMode());
          }
          teUpdateDownloadFolderPath();
          if (!teDownloadSessionAccount) teLoadDownTxtLocal(teActiveFolderAccountName());
          P();
        });
        try {
          chrome.tabs.onRemoved.addListener((tabId) => {
            if (teChannelLocked() && Number(tabId || 0) === Number(tePinnedTargetTabId || 0)) {
              teDisableChannelLockForMissingTab("pinned_instagram_tab_removed", { tabId: Number(tabId || 0), url: tePinnedTargetUrl });
            }
          });
        } catch (_) {}
      }
      function teActiveFolderAccountName() {
        return teSafePath(teDownloadSessionAccount || (teChannelLocked() && tePinnedTargetUrl ? tePinnedAccountName() : teAccountFromUrl(teCurrentTargetUrl())) || "instagram");
      }
      function teSetTargetLabel(e) {
        try {
          if (teChannelLocked() && tePinnedTargetUrl) e = { tabId: tePinnedTargetTabId || teCurrentTargetTabId, url: tePinnedTargetUrl };
          else if (teChannelLocked() && e && e.url) tePinTarget(e);
          const t = document.getElementById("igblue_target_url");
          const r = e && e.url ? String(e.url) : "";
          const isIg = teIsInstagramUrl(r);
          teHasInstagramTarget = !!isIg;
          if (!t) { P(); return; }
          if (isIg) {
            t.textContent = teShortIgUrl(r);
            t.title = r;
            t.classList.remove("igblue-target-missing");
          } else {
            t.textContent = "No active Instagram tab";
            t.title = "Switch to an Instagram tab to link this popup";
            t.classList.add("igblue-target-missing");
          }
          teUpdateDownloadFolderPath();
          if (isIg) teTouchTabSession(!!(teAutoPyRunning && !teAutoPyStopping), "target_label");
          teUpdateTabIdBadge();
          P();
        } catch { teHasInstagramTarget = !1; teUpdateTabIdBadge(); }
      }
      function teApplyTargetInfo(e) {
        if (!e || !Array.isArray(e.info)) return !1;
        const guard = teShouldAcceptSideShopPayload(e);
        if (!(guard && guard.ok)) {
          try { localStorage.setItem("IG_dbg/side.js/ndy_side_shop_ignored", JSON.stringify(Object.assign({ts:new Date().toISOString()}, guard || {}))); } catch (_) {}
          return !1;
        }
        u || ((u = !0), P());
        c && clearTimeout(c);
        c = setTimeout(F, 2e3);
        se = e.info;
        if (!teChannelLocked() && Number(e.tabId || 0)) teCurrentTargetTabId = Number(e.tabId || 0);
        if (e.url) teSetTargetLabel({ tabId: Number(e.tabId || 0) || teCurrentTargetTabId, url: String(e.url || "") });
        teTouchTabSession(!!(teAutoPyRunning && !teAutoPyStopping), "side_payload_accept");
        ae(e.info, K, Z, !1, !1);
        return !0;
      }
      function teRequestTargetState(e = !1) {
        try {
          chrome.runtime.sendMessage({ type: "IGBLUE_GET_TARGET_STATE" }, (t) => {
            if (chrome.runtime.lastError || !t) return void teSetTargetLabel(null);
            if (!(teChannelLocked() && tePinnedTargetUrl)) teCurrentTargetTabId = Number(t.tabId || 0);
            const state = teChannelLocked() && tePinnedTargetUrl ? Object.assign({}, t, {tabId: tePinnedTargetTabId || t.tabId, url: tePinnedTargetUrl}) : t;
            teSetTargetLabel(state);
            e && teApplyTargetInfo(state);
          });
        } catch { teSetTargetLabel(null); }
      }
      function teInitPopupUi() {
        try {
          const isPopup = teIsPopupMode();
          isPopup && document.body.classList.add("igblue-popup-mode");
          const bindPopout = (btn) => {
            if (!btn) return;
            if (isPopup) {
              btn.disabled = !0;
              btn.style.display = "none";
              btn.setAttribute("aria-disabled", "true");
            } else {
              btn.addEventListener("click", () => {
                chrome.runtime.sendMessage({ type: "IGBLUE_OPEN_POPUP" }, () => {
                  chrome.runtime.lastError;
                  // v230: popup 전환 뒤에도 기존 Side/탭을 닫지 않는다.
                });
              });
            }
          };
          bindPopout(document.getElementById("open_popup"));
          bindPopout(document.getElementById("open_popup_inline"));
          teInitChannelLock();
          teRequestTargetState(isPopup);
        } catch {}
      }
      function teGetTargetTabId(cb) {
        if (teChannelLocked() && tePinnedTargetTabId > 0) {
          return void teValidatePinnedTargetAlive("before_send_to_pinned_tab", (ok) => cb(ok ? tePinnedTargetTabId : 0));
        }
        try {
          chrome.runtime.sendMessage({ type: "IGBLUE_GET_TARGET_STATE" }, (res) => {
            if (!chrome.runtime.lastError && res) {
              teCurrentTargetTabId = Number(res?.tabId || 0);
              teSetTargetLabel(res);
              const id = Number(res?.tabId || 0);
              if (Number.isFinite(id) && id > 0) return void cb(id);
            }
            cb(0);
          });
        } catch { cb(0); }
      }
      function teTabMessageJsonSize(value) {
        try { return JSON.stringify(value || {}).length; } catch (_) { return 999999999; }
      }
      function teSlimValueForTab(value, depth = 0) {
        try {
          if (value == null) return value;
          if (typeof value === "string") return value.length > 16000 ? value.slice(0, 16000) + `...<${value.length}>` : value;
          if (typeof value !== "object") return value;
          if (depth >= 4) return Array.isArray(value) ? `[array len=${value.length}]` : "[object truncated]";
          if (Array.isArray(value)) return value.slice(-24).map((v) => teSlimValueForTab(v, depth + 1));
          const out = {};
          const skipHuge = new Set(["calibration_log", "calibration_log_text", "runtime_memory_events", "cycle_log", "cycle_log_text", "cycle_log_lines", "latest_click", "click_result", "startup_check"]);
          Object.keys(value).slice(0, 90).forEach((k) => {
            const v = value[k];
            if (skipHuge.has(k)) {
              if (typeof v === "string") out[k] = v.slice(-12000);
              else if (Array.isArray(v)) out[k] = v.slice(-20).map((x) => teSlimValueForTab(x, depth + 1));
              else if (v && typeof v === "object") out[k] = { truncated: true, keys: Object.keys(v).slice(0, 30) };
              else out[k] = v;
              return;
            }
            out[k] = teSlimValueForTab(v, depth + 1);
          });
          return out;
        } catch (_) { return "[slim_failed]"; }
      }
      function teSafeTabMessage(msg) {
        try {
          if (teTabMessageJsonSize(msg) <= 512 * 1024) return msg;
          const slim = teSlimValueForTab(msg, 0);
          if (teTabMessageJsonSize(slim) <= 1024 * 1024) return slim;
          return {
            type: msg && msg.type || "igblue_py_state",
            value: {
              state: msg && msg.value && msg.value.state || msg && msg.state || "message_too_large_truncated",
              message: "side.js tabs.sendMessage payload truncated before Chrome 64MiB limit",
              error: msg && msg.value && (msg.value.error || msg.value.error_code) || msg && (msg.error || msg.error_code) || "payload_too_large",
              build_tag: IGBLUE_BUILD_TAG
            }
          };
        } catch (_) {
          return { type: msg && msg.type || "igblue_py_state", value: { state: "message_too_large_truncated", error: "payload_slim_exception", build_tag: IGBLUE_BUILD_TAG } };
        }
      }
      function teSendToTargetTab(msg, failMsg) {
        return new Promise((resolve) => {
          teGetTargetTabId((tabId) => {
            if (!tabId) return (failMsg && n(failMsg), void resolve(!1));
            const tabMsg = teSafeTabMessage(msg);
            const send = () => chrome.tabs.sendMessage(tabId, tabMsg, () => {
              if (!chrome.runtime.lastError) return resolve(!0);
              // IGblue local patch v11: target tab may exist but content scripts may not be injected
              // after extension reload. Ask background to inject and retry once.
              try {
                chrome.runtime.sendMessage({ type: "IGBLUE_ENSURE_CONTENT", tabId }, (res) => {
                  if (chrome.runtime.lastError || !res || res.status !== "ok") {
                    failMsg && n(failMsg + " / script not ready");
                    return resolve(!1);
                  }
                  chrome.tabs.sendMessage(tabId, tabMsg, () => {
                    chrome.runtime.lastError ? (failMsg && n(failMsg + " / retry failed"), resolve(!1)) : resolve(!0);
                  });
                });
              } catch {
                failMsg && n(failMsg);
                resolve(!1);
              }
            });
            send();
          });
        });
      }
      function teSendToTargetTabResponse(msg, failMsg, timeoutMs = 3000) {
        return new Promise((resolve) => {
          teGetTargetTabId((tabId) => {
            if (!tabId) return (failMsg && n(failMsg), void resolve({ ok: false, error: "no_instagram_tab" }));
            const tabMsg = teSafeTabMessage(msg);
            let done = false;
            const finish = (res) => { if (done) return; done = true; resolve(res || { ok: false, error: "empty_response" }); };
            const send = () => {
              const timer = setTimeout(() => finish({ ok: false, error: "content_timeout" }), Math.max(500, Number(timeoutMs) || 3000));
              chrome.tabs.sendMessage(tabId, tabMsg, (res) => {
                clearTimeout(timer);
                if (!chrome.runtime.lastError) return finish(res || { ok: true });
                try {
                  chrome.runtime.sendMessage({ type: "IGBLUE_ENSURE_CONTENT", tabId }, (ens) => {
                    if (chrome.runtime.lastError || !ens || ens.status !== "ok") {
                      failMsg && n(failMsg + " / script not ready");
                      return finish({ ok: false, error: "script_not_ready" });
                    }
                    const timer2 = setTimeout(() => finish({ ok: false, error: "content_retry_timeout" }), Math.max(500, Number(timeoutMs) || 3000));
                    chrome.tabs.sendMessage(tabId, tabMsg, (res2) => {
                      clearTimeout(timer2);
                      chrome.runtime.lastError ? (failMsg && n(failMsg + " / retry failed"), finish({ ok: false, error: chrome.runtime.lastError.message || "retry_failed" })) : finish(res2 || { ok: true });
                    });
                  });
                } catch (err) {
                  failMsg && n(failMsg);
                  finish({ ok: false, error: String(err && err.message || err) });
                }
              });
            };
            send();
          });
        });
      }
      // IGblue local patch v10: prevent accidental duplicate download commands from rapid double click,
      // touch/click duplication, or multiple UI instances calling the same URL immediately.
      const teClickLocks = new Map();
      const teDownloadLocks = new Map();
      function teLockOnce(key, ms = 1200) {
        const now = Date.now();
        const until = Number(teClickLocks.get(key) || 0);
        if (until > now) return !1;
        teClickLocks.set(key, now + ms);
        setTimeout(() => {
          if (Number(teClickLocks.get(key) || 0) <= Date.now()) teClickLocks.delete(key);
        }, ms + 80);
        return !0;
      }
      function teRunButtonOnce(buttonId, key, ms, fn) {
        const btn = document.getElementById(buttonId);
        if (!teLockOnce(key, ms)) return void n("Ignored duplicate click");
        if (btn) btn.disabled = !0;
        try {
          return fn();
        } finally {
          setTimeout(() => {
            if (btn) btn.disabled = !1;
            P();
          }, ms);
        }
      }
      function teDownloadOnce(url, prefix, sender) {
        dbg("download_once", "entry", 58530, {prefix, url:String(url||"").slice(0,220)});
        const key = `${String(url || "")}@@${String(prefix || "")}`;
        const now = Date.now();
        const until = Number(teDownloadLocks.get(key) || 0);
        if (until > now) return dbg("download_once", "duplicate_lock", 58534, {prefix, url:String(url||"").slice(0,220), until, now}), n(`Ignored duplicate download: ${prefix || "media"}`), Promise.resolve(!1);
        teDownloadLocks.set(key, now + 1800);
        setTimeout(() => {
          if (Number(teDownloadLocks.get(key) || 0) <= Date.now()) teDownloadLocks.delete(key);
        }, 1900);
        return sender();
      }
      // IGblue local patch v16/v20: Chrome down API target folder helpers.
      function tePathPartsFromUrl(e) {
        try {
          const t = new URL(String(e || ""));
          if (!teIsInstagramUrl(t.href)) return [];
          return (t.pathname || "/").split("/").filter(Boolean);
        } catch { return []; }
      }
      function teUrlMode(e) {
        const t = tePathPartsFromUrl(e), r = String(t[0] || "home").toLowerCase();
        return r === "explore" ? "explore" : r === "p" ? "p" : r === "reel" ? "reel" : r === "reels" ? "reels" : r === "stories" ? "stories" : "profile";
      }
      function teAccountFromUrl(e) {
        const t = tePathPartsFromUrl(e), r = String(t[0] || "home").toLowerCase();
        if (!t.length) return "home";
        // /p/SHORTCODE and /reel/SHORTCODE do not have a channel in the URL, so keep a stable path folder.
        if ((r === "p" || r === "reel") && t[1]) return `${r}/${t[1]}`;
        if (r === "stories" && t[1]) return `stories/${t[1]}`;
        if (r === "explore") return "explore";
        return t[0] || "home";
      }
      function teSafePath(e) {
        const t = String(e || "instagram").replace(/\\+/g, "/").split("/").map((e) => teSafeName(e)).filter(Boolean);
        return t.length ? t.join("/") : "instagram";
      }
      function teIsPlaceholderAccountName(value) {
        const v = String(value || "").replace(/^@/, "").trim().toLowerCase();
        return !v || ["instagram", "explore", "explorer", "reel", "reels", "p", "home", "ig", "unknown", "undefined", "null", "none"].includes(v);
      }
      function teCleanOwnerAccountName(value) {
        const raw = String(value || "").replace(/^@/, "").trim();
        const safe = teSafeName(raw);
        return teIsPlaceholderAccountName(safe) ? "" : safe;
      }
      function teMediaOwnerName(e) {
        const m = e || {};
        const choices = [
          m?.owner_username,
          m?.username,
          m?.user?.username,
          m?.owner?.username,
          m?.caption?.user?.username,
          m?.media?.owner_username,
          m?.media?.username,
          m?.media?.user?.username,
          m?.media?.owner?.username,
          m?.media?.caption?.user?.username
        ];
        for (const c of choices) {
          const owner = teCleanOwnerAccountName(c);
          if (owner) return owner;
        }
        return "";
      }
      function teCurrentTargetUrl() {
        if (teChannelLocked() && tePinnedTargetUrl) return tePinnedTargetUrl;
        const e = document.getElementById("igblue_target_url");
        return e?.title && teIsInstagramUrl(e.title) ? e.title : "";
      }
      function teAutoPyHashStartNoFromUrl(url) {
        try {
          const u = new URL(String(url || teCurrentTargetUrl() || ""));
          const raw = String(u.hash || "").replace(/^#/, "").trim();
          const m = raw.match(/^No\s*0*(\d{1,6})$/i);
          const n = m ? Number(m[1]) : 0;
          return Number.isFinite(n) && n > 0 ? n : 0;
        } catch (_) {
          return 0;
        }
      }
      function teAutoPyActiveHashNo(state) {
        try {
          const direct = Number(state && (state.active_hash_no || state.hash_rank_no) || 0) || 0;
          if (direct > 0) return direct;
          return teAutoPyHashStartNoFromUrl(state && state.href || teCurrentTargetUrl() || "");
        } catch (_) { return 0; }
      }
      function teAutoPyLastRankKey() { return "download/auto_py_last_rank_no"; }
      function teAutoPySetLastRank(no, reason) {
        const nNo = Math.max(0, Number(no || 0) || 0);
        try { teLocalSet(teAutoPyLastRankKey(), String(nNo)); } catch (_) {}
        try { teLocalSet("auto_py_last_rank_no", String(nNo)); } catch (_) {}
        const el = document.getElementById("igblue_auto_py_last_rank");
        if (el && String(el.value || "") !== String(nNo)) el.value = String(nNo);
        try { chrome.storage && chrome.storage.local && chrome.storage.local.set({ "IGblue/download/auto_py_last_rank_no": nNo, "IGblue/auto_py_last_rank_no": nNo, "IGblue/auto_py_last_rank_reason": String(reason || "") }); } catch (_) {}
        return nNo;
      }
      function teAutoPyGetLastRank() {
        const el = document.getElementById("igblue_auto_py_last_rank");
        const raw = el && String(el.value || "").trim() ? el.value : teLocalGetAny([teAutoPyLastRankKey(), "auto_py_last_rank_no"], "0");
        const nNo = Math.max(0, Number(raw || 0) || 0);
        return Number.isFinite(nNo) ? nNo : 0;
      }
      function teAutoPyReadLastRankTextValue() {
        const el = document.getElementById("igblue_auto_py_last_rank");
        const raw = el ? String(el.value || "").trim() : "";
        if (!raw) return 0;
        const nNo = Math.max(0, Number(raw) || 0);
        return Number.isFinite(nNo) ? nNo : 0;
      }
      function teAutoPyLoadedUiCount() {
        const nNo = Math.max(0, Number(teGetTopMetricCountValue && teGetTopMetricCountValue() || 0) || 0);
        return Number.isFinite(nNo) ? nNo : 0;
      }
      function teAutoPyAnnounce(message, detail) {
        const msg = String(message || "").trim();
        if (!msg) return;
        try { n(msg); } catch (_) {}
        try {
          const el = document.getElementById("down_progress");
          if (el) el.textContent = msg + (detail ? "\n" + String(detail) : "");
        } catch (_) {}
      }
      function teAutoPyCountsSnapshot(requestedNo, state) {
        try { teRefreshRanksAndMetricCount(); } catch (_) {}
        const counts = O();
        const metricCount = Math.max(0, Number(teAutoPyLoadedUiCount() || 0) || 0);
        const allCount = Number(counts && counts.all || 0) || 0;
        const pageFilteredCount = Math.max(0, Number(counts && counts.filtered || 0) || 0);
        const metricSourceLen = teAutoPyFilteredSourceList().length;
        const topSortedCount = teAutoPyTopSortedReadyCount();
        const domLoadedMax = Number(state && state.loaded_max_no || 0) || 0;
        return {
          requested_no: Number(requestedNo || 0) || 0,
          all_count: allCount,
          filtered_count: pageFilteredCount,
          metric_count: metricCount,
          metric_source_count: metricSourceLen,
          top_sorted_count: topSortedCount,
          ready_count: topSortedCount,
          loaded_max_no: topSortedCount,
          dom_loaded_max_no: domLoadedMax,
          loaded_count: Number(state && state.loaded_count || 0) || 0,
          current_rank_no: Number(state && state.current_rank_no || 0) || 0,
          active_rank_no: Number(state && (state.active_rank_no || state.current_rank_no) || 0) || 0,
          active_hash_no: teAutoPyActiveHashNo(state),
          loaded_ready_no: topSortedCount,
          has_requested: topSortedCount >= (Number(requestedNo || 0) || 0) && Number(requestedNo || 0) > 0,
          dom_has_requested: !!(state && state.has_requested),
          at_bottom: !!(state && state.at_bottom),
          scroll_y: Number(state && state.scroll_y || 0) || 0,
          scroll_height: Number(state && state.scroll_height || 0) || 0,
          inner_height: Number(state && state.inner_height || 0) || 0,
          href: String(state && state.href || teCurrentTargetUrl() || "")
        };
      }
      function teAutoPyMetricFilterActive() {
        try { return !!(teTopMetricField && teTopMetricField()) && Number(teMetricThresholdValue && teMetricThresholdValue() || 0) > 0; } catch (_) { return false; }
      }
      function teAutoPyAnyFilterActive() {
        return !!(teAutoPyDateFilterActive() || teAutoPyMetricFilterActive());
      }
      function teAutoPyFilteredSourceList() {
        try { teRefreshRanksAndMetricCount(); } catch (_) {}
        const base = Array.isArray(teMetricSourceList && teMetricSourceList()) ? teMetricSourceList().filter(Boolean) : [];
        if (!teAutoPyMetricFilterActive()) return base;
        const field = teTopMetricField();
        const threshold = Number(teMetricThresholdValue && teMetricThresholdValue() || 0) || 0;
        return base.filter((media) => teMetricNumber(media && media[field]) >= threshold);
      }
      function teAutoPyTopSortedReadyCount() {
        try { teRefreshRanksAndMetricCount(); } catch (_) {}
        // v170 inherited v169 절대 원칙:
        // Auto PY 시작 가능 여부는 Top sorted medias 구역의 top_metric_count 값만 사용한다.
        // page_info_filtered, page_info_all, DOM max, URL hash, source length는 다른 구역/참고값이므로
        // 최종 다운로드 지점 확보 판단에 섞지 않는다.
        const metricUi = Math.max(0, Number(teAutoPyLoadedUiCount && teAutoPyLoadedUiCount() || 0) || 0);
        return Number.isFinite(metricUi) ? metricUi : 0;
      }
      function teAutoPyFilteredSortedTopN(limitNo) {
        try { teRefreshRanksAndMetricCount(); } catch (_) {}
        const raw = teAutoPyFilteredSourceList();
        const t = Math.max(0, Number(limitNo || raw.length || 0) || 0);
        const field = G[Z] || "";
        let arr = raw.slice();
        if (field) {
          arr = arr.sort((a, b) => {
            const av = a && a[field], bv = b && b[field];
            return av === undefined && bv === undefined ? 0 : av === undefined ? 1 : bv === undefined ? -1 : bv - av;
          });
        }
        return t > 0 ? arr.slice(0, Math.min(t, arr.length)) : arr;
      }
      function teAutoPyBuildDynamicRankPlan(requestedNo) {
        const reqNo = Math.max(1, Number(requestedNo || 0) || 1);
        try { teRefreshRanksAndMetricCount(); } catch (_) {}
        const topSortedCount = teAutoPyTopSortedReadyCount();
        const list = teAutoPyFilteredSortedTopN(Math.max(reqNo, topSortedCount));
        const ctx = teBuildRankContext(list);
        const labels = teBuildMainRankLabels(list, ctx);
        try { teScheduleMainRankLabels(list, ctx); } catch (_) {}
        const counts = O();
        const dateFilterActive = teAutoPyDateFilterActive();
        const metricFilterActive = teAutoPyMetricFilterActive();
        const filterActive = !!(dateFilterActive || metricFilterActive);
        return {
          requested_no: reqNo,
          list,
          ctx,
          labels,
          max_rank_no: Math.max(labels.length, topSortedCount),
          all_count: Number(counts && counts.all || 0) || 0,
          filtered_count: Number(counts && counts.filtered || 0) || 0,
          metric_count: Number(teAutoPyLoadedUiCount() || 0) || 0,
          top_sorted_count: topSortedCount,
          ready_count: topSortedCount,
          date_filter_active: dateFilterActive,
          metric_filter_active: metricFilterActive,
          filter_active: filterActive,
          filter_result_count: list.length,
          filter_result_empty: !!(filterActive && list.length === 0)
        };
      }
      function teAutoPyDateRangeLoadSnapshot() {
        const threshold = Math.max(0, Number(K || 0) || 0);
        const active = threshold > 0;
        const map = new Map();
        const list = Array.isArray(se) ? se.filter(Boolean) : [];
        list.forEach((media, index) => {
          const code = String(media && (media.code || media.shortcode || media.id || media.pk) || "").trim();
          const stamp = Number(media && (media.created_at || media.taken_at || media.caption && media.caption.created_at) || 0) || 0;
          const key = code || `${stamp}:${String(media && (media.url || media.display_url || media.thumbnail_src) || "").slice(0, 160)}:${index}`;
          map.set(key, stamp);
        });
        return { active, threshold, map, loaded_count: list.length };
      }
      function teAutoPyDateRangeDelta(before) {
        const prev = before && before.map instanceof Map ? before.map : new Map();
        const now = teAutoPyDateRangeLoadSnapshot();
        let newKnown = 0, newValid = 0, newOutOfRange = 0, newUnknown = 0;
        now.map.forEach((stamp, key) => {
          if (prev.has(key)) return;
          if (!(stamp > 0)) newUnknown += 1;
          else if (stamp > now.threshold) { newKnown += 1; newValid += 1; }
          else { newKnown += 1; newOutOfRange += 1; }
        });
        return Object.assign({}, now, {
          new_count: Math.max(0, now.map.size - prev.size),
          new_known_count: newKnown,
          new_valid_count: newValid,
          new_out_of_range_count: newOutOfRange,
          new_unknown_count: newUnknown,
          exhausted_batch: !!(now.active && newKnown > 0 && newValid === 0 && newOutOfRange === newKnown)
        });
      }
      function teAutoPyEstimateSwipeNeed(requestedNo, snap) {
        const reqNo = Math.max(1, Number(requestedNo || 1) || 1);
        const allCount = Math.max(1, Number(snap && snap.all_count || 0) || 0);
        const topSortedCount = Math.max(0, Number(snap && (snap.top_sorted_count || snap.ready_count || snap.metric_count) || 0) || 0);
        if (topSortedCount >= reqNo) return 0;
        if (topSortedCount <= 0) return Math.min(10, Math.max(3, Math.ceil(reqNo / 2)));
        const rate = Math.max(0.01, Math.min(1, topSortedCount / allCount));
        const needAll = Math.ceil(reqNo / rate);
        const gapAll = Math.max(0, needAll - allCount);
        return Math.max(1, Math.ceil(gapAll / 20));
      }
      async function teAutoPyWaitLoadingStable(requestedNo, mySeq, base, timeoutMs) {
        const reqNo = Math.max(1, Number(requestedNo || 1) || 1);
        const endAt = Date.now() + Math.max(2600, Number(timeoutMs || 7000) || 7000);
        let lastSig = "";
        let stableTicks = 0;
        let lastSnap = null;
        while (Date.now() < endAt) {
          if (mySeq && mySeq !== teAutoPyRunSeq) return { ok: false, error: "run_seq_changed", snapshot: lastSnap };
          await teSleep(650);
          const plan = teAutoPyBuildDynamicRankPlan(reqNo);
          const state = await teAutoPyReadLoadedRankState(180, reqNo);
          const snap = teAutoPyCountsSnapshot(reqNo, state);
          lastSnap = Object.assign({}, snap, { max_rank_no: plan.max_rank_no, top_sorted_count: snap.top_sorted_count, dom_loaded_max_no: snap.dom_loaded_max_no });
          const ready = Number(snap.top_sorted_count || snap.ready_count || 0) >= reqNo;
          if (ready) return { ok: true, ready: true, snapshot: lastSnap, plan };
          const sig = [snap.all_count, snap.filtered_count, snap.metric_count, snap.top_sorted_count, snap.dom_loaded_max_no, snap.scroll_y, snap.scroll_height].join("/");
          if (sig === lastSig) stableTicks += 1;
          else stableTicks = 0;
          lastSig = sig;
          if (stableTicks >= 3) return { ok: true, stable: true, snapshot: lastSnap, plan };
        }
        return { ok: true, timeout: true, snapshot: lastSnap, plan: teAutoPyBuildDynamicRankPlan(reqNo) };
      }
      async function teAutoPyEnsureDownloadPointLoaded(requestedNo, mySeq, reason) {
        const reqNo = Math.max(0, Number(requestedNo || 0) || 0);
        const plan0 = teAutoPyBuildDynamicRankPlan(Math.max(1, reqNo || teTopNCount() || 1));
        if (!(reqNo > 0)) return Object.assign({ ok: true, skipped: true, reason: "no_download_point" }, plan0);
        const base = { cycle_id: teBuildCycleId("LOAD_POINT"), run_seq: Number(mySeq || 0), rank_no: reqNo, rank_label: teAutoPyNoLabel(reqNo), requested_rank_no: reqNo, start_source: String(reason || "auto_start_load_first"), build_version: IGBLUE_BUILD_VERSION, build_tag: IGBLUE_BUILD_TAG };
        await teAutoPyCycle("S00_LOAD_FIRST_START", Object.assign({}, base, { decision: "load_for_direct_download", action: "ensure_download_point_loaded", reason: "direct_download_requires_media_data" }));
        teAutoPyAnnounce(`${teAutoPyNoLabel(reqNo)} 이상 직접 다운로드 데이터를 확인합니다.`);
        let totalSwipe = 0;
        let firstSwipeDone = false;
        let noGrowth = 0;
        let previousSig = "";
        let estimatedMax = 12;
        let consecutiveDateExhausted = 0;
        for (let attempt = 1; attempt <= 80; attempt++) {
          if (mySeq && mySeq !== teAutoPyRunSeq) return { ok: false, error: "run_seq_changed" };
          const plan = teAutoPyBuildDynamicRankPlan(reqNo);
          const state = await teAutoPyReadLoadedRankState(240, reqNo);
          const snap = teAutoPyCountsSnapshot(reqNo, state);
          const effectiveLoaded = Number(snap.top_sorted_count || snap.ready_count || 0) || 0;
          const ready = effectiveLoaded >= reqNo;
          const estimate = teAutoPyEstimateSwipeNeed(reqNo, snap);
          if (attempt === 1) estimatedMax = Math.min(20, Math.max(8, estimate + 4));
          await teAutoPyCycle("S00_LOAD_FIRST_CHECK", Object.assign({}, base, snap, { decision: ready ? "download_point_ready" : "download_point_not_ready", action: "check_top_sorted_after_origin", reason: ready ? "top_sorted_target_loaded" : "top_sorted_target_shortage", attempt, active_hash_no: snap.active_hash_no, loaded_ready_no: effectiveLoaded, effective_loaded_max_no: effectiveLoaded, top_sorted_count: effectiveLoaded, dom_loaded_max_no: snap.dom_loaded_max_no, estimated_swipe_need: estimate, total_swipe_count: totalSwipe, max_swipe_count: estimatedMax }));
          if (ready) {
            teAutoPyAnnounce(`최종 다운로드 지점 ${teAutoPyNoLabel(reqNo)} 이상으로 로딩 완료하였습니다.`, `All medias ${snap.all_count} / Top sorted ${effectiveLoaded}`);
            await teAutoPyCycle("S00_LOAD_FIRST_READY", Object.assign({}, base, snap, { decision: "ready", action: "start_download_next", reason: "top_sorted_loaded_after_origin", active_hash_no: snap.active_hash_no, loaded_ready_no: effectiveLoaded, effective_loaded_max_no: effectiveLoaded, top_sorted_count: effectiveLoaded, dom_loaded_max_no: snap.dom_loaded_max_no }));
            return Object.assign({ ok: true, snapshot: snap }, plan, { max_rank_no: Math.max(plan.max_rank_no, effectiveLoaded) });
          }
          const sig = [snap.top_sorted_count, snap.all_count, snap.scroll_y, snap.scroll_height, snap.at_bottom ? 1 : 0].join("/");
          noGrowth = sig === previousSig ? noGrowth + 1 : 0;
          previousSig = sig;
          // v170 inherited v169: top_metric_count가 목표보다 작으면 고정 slow-swipe 횟수 때문에 중단하지 않는다.
          // 오직 top_metric_count가 더 이상 증가하지 않고, 페이지 하단으로 판단될 때만 현재 주소 끝 도달로 본다.
          const endReached = !!(snap.at_bottom && noGrowth >= 4 && attempt > 4);
          if (endReached) {
            const endReason = "current_url_end_reached";
            const msg = "현재 주소의 끝까지 도달하였습니다. 다음 계정주소 연동이 필요합니다.";
            teAutoPyAnnounce(msg, `현재 All medias ${snap.all_count} / Top sorted ${effectiveLoaded} / 목표 ${teAutoPyNoLabel(reqNo)}`);
            await teAutoPyCycle("S00_LOAD_FIRST_END_REACHED", Object.assign({}, base, snap, { decision: "not_ready", action: "stop_or_next_account", reason: endReason, attempt, active_hash_no: snap.active_hash_no, loaded_ready_no: effectiveLoaded, top_sorted_count: effectiveLoaded, dom_loaded_max_no: snap.dom_loaded_max_no, total_swipe_count: totalSwipe, max_swipe_count: estimatedMax, no_growth_count: noGrowth }));
            return Object.assign({ ok: false, error: endReason, snapshot: snap }, plan);
          }
          const dateBeforeSwipe = teAutoPyDateRangeLoadSnapshot();
          const swipeCount = firstSwipeDone ? 1 : Math.max(1, Math.min(10, estimate || 10));
          firstSwipeDone = true;
          totalSwipe += swipeCount;
          const action = swipeCount >= 10 ? "swipe_10" : "swipe_down";
          teAutoPyAnnounce(swipeCount >= 10 ? "Swipe 10회를 천천히 실행합니다." : "Swipe down 1회를 실행합니다.", `로딩 대기 중입니다. All medias ${snap.all_count} / Top sorted ${effectiveLoaded}`);
          await teAutoPyCycle("S00_LOAD_FIRST_SWIPE", Object.assign({}, base, snap, { decision: "load_more", action, reason: "top_sorted_target_shortage", attempt, active_hash_no: snap.active_hash_no, loaded_ready_no: effectiveLoaded, top_sorted_count: effectiveLoaded, dom_loaded_max_no: snap.dom_loaded_max_no, swipe_count: swipeCount, total_swipe_count: totalSwipe, estimated_swipe_need: estimate }));
          await teAutoPySwipeLoad(swipeCount, "download_point_load_first");
          teAutoPyAnnounce("로딩 대기중입니다...");
          const waitRes = await teAutoPyWaitLoadingStable(reqNo, mySeq, base, swipeCount >= 10 ? 9500 : 6200);
          await teAutoPyCycle("S00_LOAD_FIRST_WAIT_DONE", Object.assign({}, base, { decision: waitRes && waitRes.ready ? "loaded" : "stable_or_wait_done", action: "wait_loading_stable", reason: waitRes && waitRes.ready ? "loading_completed_target_ready" : waitRes && waitRes.stable ? "loading_stable" : waitRes && waitRes.timeout ? "loading_wait_timeout" : String(waitRes && waitRes.error || "wait_done"), attempt, active_hash_no: waitRes && waitRes.snapshot && waitRes.snapshot.active_hash_no, loaded_ready_no: waitRes && waitRes.snapshot && waitRes.snapshot.loaded_ready_no, top_sorted_count: waitRes && waitRes.snapshot && waitRes.snapshot.top_sorted_count, dom_loaded_max_no: waitRes && waitRes.snapshot && waitRes.snapshot.dom_loaded_max_no, wait_result: waitRes || null }));
          const dateDelta = teAutoPyDateRangeDelta(dateBeforeSwipe);
          consecutiveDateExhausted = dateDelta.exhausted_batch ? consecutiveDateExhausted + 1 : (dateDelta.new_valid_count > 0 ? 0 : consecutiveDateExhausted);
          await teAutoPyCycle("S00_LOAD_FIRST_DATE_RANGE_CHECK", Object.assign({}, base, {
            decision: dateDelta.exhausted_batch ? "date_range_out_of_range" : "date_range_continue",
            action: "check_new_media_date_range",
            reason: dateDelta.exhausted_batch ? "new_media_all_older_than_filter" : "valid_or_unknown_date_media_present",
            attempt,
            date_filter_active: dateDelta.active,
            date_filter_threshold: dateDelta.threshold,
            new_media_count: dateDelta.new_count,
            new_known_date_count: dateDelta.new_known_count,
            new_valid_date_count: dateDelta.new_valid_count,
            new_out_of_range_count: dateDelta.new_out_of_range_count,
            new_unknown_date_count: dateDelta.new_unknown_count,
            consecutive_out_of_range_swipes: consecutiveDateExhausted
          }));
          if (dateDelta.active && consecutiveDateExhausted >= 2) {
            const partialPlan = teAutoPyBuildDynamicRankPlan(reqNo);
            const emptyFiltered = !!partialPlan.filter_result_empty;
            teAutoPyAnnounce(
              "날짜 필터 범위의 끝에 도달했습니다.",
              emptyFiltered ? "필터 결과 0개: 현재 URL을 종료하고 다음 URL을 확인합니다." : `필터 결과 ${partialPlan.filter_result_count || partialPlan.filtered_count || 0}개만 처리합니다.`
            );
            await teAutoPyCycle("S00_LOAD_FIRST_DATE_RANGE_END", Object.assign({}, base, partialPlan, {
              decision: emptyFiltered ? "filter_result_empty" : "ready_partial",
              action: emptyFiltered ? "skip_current_account_or_finish" : "process_loaded_filtered_media",
              reason: emptyFiltered ? "date_filter_range_exhausted_empty" : "date_filter_range_exhausted",
              requested_rank_no: reqNo,
              loaded_ready_no: partialPlan.top_sorted_count || 0,
              consecutive_out_of_range_swipes: consecutiveDateExhausted,
              date_range_result: dateDelta
            }));
            return Object.assign({
              ok: true,
              partial: true,
              reason: emptyFiltered ? "date_filter_range_exhausted_empty" : "date_filter_range_exhausted",
              date_range_result: dateDelta
            }, partialPlan);
          }
        }
        const finalPlan = teAutoPyBuildDynamicRankPlan(reqNo);
        teAutoPyAnnounce("목표 No까지 아직 부족하지만 반복 안전 횟수에 도달했습니다.", `Top sorted ${finalPlan.top_sorted_count || 0} / 목표 ${teAutoPyNoLabel(reqNo)}`);
        await teAutoPyCycle("S00_LOAD_FIRST_ATTEMPTS_EXCEEDED", Object.assign({}, base, finalPlan, { decision: "not_ready", action: "stop_or_next_account", reason: "download_point_load_attempts_exceeded", requested_rank_no: reqNo, loaded_ready_no: finalPlan.top_sorted_count || 0, top_sorted_count: finalPlan.top_sorted_count || 0, total_swipe_count: totalSwipe }));
        return Object.assign({ ok: false, error: "download_point_load_attempts_exceeded" }, finalPlan);
      }
      function teAutoPyResolveStartNo() {
        const textNo = teAutoPyReadLastRankTextValue();
        if (textNo > 0) return { no: textNo, source: "last_rank_textbox" };
        const hashNo = teAutoPyHashStartNoFromUrl(teCurrentTargetUrl());
        if (hashNo > 0) return { no: hashNo, source: "url_hash" };
        const savedNo = teAutoPyGetLastRank();
        if (savedNo > 0) return { no: savedNo, source: "saved_last_rank" };
        return { no: 0, source: "default_current" };
      }
      function teInitAutoPyLastRankUi() {
        const el = document.getElementById("igblue_auto_py_last_rank");
        if (!el) return;
        const markEdited = () => { el.__igblueUserEdited = true; };
        const apply = () => { markEdited(); teAutoPySetLastRank(el.value, "manual_edit"); };
        el.value = String(teAutoPyGetLastRank());
        try { chrome.storage && chrome.storage.local && chrome.storage.local.get(["IGblue/download/auto_py_last_rank_no", "IGblue/auto_py_last_rank_no"], (got) => {
          const saved = got && (got["IGblue/download/auto_py_last_rank_no"] ?? got["IGblue/auto_py_last_rank_no"]);
          const nNo = Math.max(0, Number(saved || 0) || 0);
          const curNo = teAutoPyReadLastRankTextValue();
          if (nNo && !el.__igblueUserEdited && document.getElementById("igblue_auto_py_last_rank") === el && !curNo) el.value = String(nNo);
        }); } catch (_) {}
        el.addEventListener("input", () => { markEdited(); clearTimeout(el.__igblueRankTimer); el.__igblueRankTimer = setTimeout(apply, 250); });
        el.addEventListener("change", apply);
        el.addEventListener("blur", apply);
        el.addEventListener("keydown", (ev) => { if (ev.key === "Enter") { ev.preventDefault(); apply(); } });
      }
      async function teAutoPyReadLoadedRankState(waitMs = 120, requestedNo = 0) {
        try {
          return await teSendToTargetTabResponse({ type: "igblue_auto_py_loaded_rank_state", wait_ms: waitMs, requested_no: Number(requestedNo || 0) || 0 }, "", 1600);
        } catch (err) {
          return { ok: false, error: String(err && err.message || err) };
        }
      }
      async function teAutoPySwipeLoad(count, reason) {
        const c = Math.max(1, Math.min(10, Number(count || 1) || 1));
        try {
          await teSendToTargetTab({ type: "ndy_swipe", count: c }, "Instagram tab not found");
        } catch (_) {}
        n(c >= 10 ? "No 로딩 대기: Swipe 10" : "No 로딩 대기: Swipe down");
        await teSleep(900 + Math.min(2200, c * 120));
        return { ok: true, count: c, reason: String(reason || "preload_scroll") };
      }
      function teAutoPyRankNoOfMediaInList(media, list, ctx) {
        const m = media && typeof media === "object" ? media : null;
        if (!m) return 0;
        const code = String(m.code || m.shortcode || "").trim();
        const c = ctx && typeof ctx === "object" ? ctx : null;
        try {
          if (code && c && c.active && c.map instanceof Map && c.map.has(code)) return Number(c.map.get(code)) || 0;
        } catch (_) {}
        const explicit = Number(m.rank_no || m.rank || m.no || m.file_no || 0) || 0;
        if (explicit > 0) return explicit;
        const label = String(m.rank_label || m.no_text || m.file_no_text || "").trim();
        const mm = label.match(/No\s*0*(\d+)/i);
        if (mm) return Number(mm[1]) || 0;
        const arr = Array.isArray(list) ? list : [];
        if (arr.length) {
          const idx = arr.findIndex((x) => x === m || (!!code && String(x && (x.code || x.shortcode) || "").trim() === code));
          if (idx >= 0) return idx + 1;
        }
        return 0;
      }
      function teAutoPyFindMediaByRankNo(rankNo, list, ctx) {
        const no = Number(rankNo || 0) || 0;
        if (!(no > 0)) return null;
        const arr = Array.isArray(list) && list.length ? list.filter(Boolean) : teMetricSourceList().filter(Boolean);
        const c = ctx && typeof ctx === "object" ? ctx : teBuildRankContext(arr);
        try {
          if (c && c.active && c.map instanceof Map) {
            for (const m of arr) {
              const code = String(m && (m.code || m.shortcode) || "").trim();
              if (code && Number(c.map.get(code) || 0) === no) return m;
            }
          }
        } catch (_) {}
        for (let i = 0; i < arr.length; i++) {
          const gotNo = teAutoPyRankNoOfMediaInList(arr[i], arr, c);
          if (gotNo === no) return arr[i];
        }
        return arr[Math.max(0, Math.min(arr.length - 1, no - 1))] || null;
      }
      function teAutoPyCloneSnapshotMedia(media, no, snapshotId) {
        const m = media && typeof media === "object" ? media : {};
        const code = String(m.code || m.shortcode || "").trim();
        const rank = Number(no || 0) || 0;
        const label = rank > 0 ? teAutoPyNoLabel(rank) : "";
        return Object.assign({}, m, {
          rank_no: rank,
          rank: rank,
          rank_label: label,
          no_text: label,
          __igblue_auto_snapshot_no: rank,
          __igblue_auto_snapshot_label: label,
          __igblue_auto_snapshot_shortcode: code,
          __igblue_auto_snapshot_id: String(snapshotId || ""),
          __igblue_auto_snapshot_key: rank > 0 && code ? (label + "|" + code) : ""
        });
      }
      function teAutoPyBuildNoSnapshotIndex(list, ctx, reason) {
        const arr = Array.isArray(list) ? list.filter(Boolean) : [];
        const c = ctx && typeof ctx === "object" ? ctx : teBuildRankContext(arr);
        const byNo = new Map(), byNoCode = new Map(), byCode = new Map(), entries = [];
        const snapshotId = [IGBLUE_BUILD_TAG, Date.now(), String(reason || "auto"), String(G[Z] || ""), String(teMetricThresholdValue())].join("|");
        for (let i = 0; i < arr.length; i++) {
          const raw = arr[i];
          const code = String(raw && (raw.code || raw.shortcode) || "").trim();
          let no = 0;
          try {
            no = c && c.active && c.map instanceof Map && code ? Number(c.map.get(code) || 0) || 0 : 0;
          } catch (_) { no = 0; }
          if (c && c.active && !(no > 0)) continue;
          if (!(no > 0)) no = i + 1;
          if (!(no > 0) || !code) continue;
          const media = teAutoPyCloneSnapshotMedia(raw, no, snapshotId);
          const entry = {
            no,
            rank_no: no,
            label: teAutoPyNoLabel(no),
            shortcode: code,
            code,
            key: teAutoPyNoLabel(no) + "|" + code,
            media,
            list_index: i,
            sort_field: String(G[Z] || ""),
            threshold: teMetricThresholdValue(),
            snapshot_id: snapshotId
          };
          entries.push(entry);
          if (!byNo.has(no)) byNo.set(no, entry);
          byNoCode.set(String(no) + "|" + code, entry);
          if (!byCode.has(code)) byCode.set(code, entry);
        }
        return {
          schema: "ig_auto_py_no_snapshot_index_v177",
          snapshot_id: snapshotId,
          created_at: Date.now(),
          reason: String(reason || "auto_start"),
          sort_field: String(G[Z] || ""),
          threshold: teMetricThresholdValue(),
          count: entries.length,
          max_rank_no: entries.reduce((max, e) => Math.max(max, Number(e.no || 0) || 0), 0),
          entries, byNo, byNoCode, byCode
        };
      }
      function teAutoPySnapshotEntryByNo(snapshot, rankNo) {
        const s = snapshot && typeof snapshot === "object" ? snapshot : null;
        const no = Number(rankNo || 0) || 0;
        if (!s || !(no > 0)) return null;
        try {
          if (s.byNo instanceof Map && s.byNo.has(no)) return s.byNo.get(no) || null;
        } catch (_) {}
        const arr = Array.isArray(s.entries) ? s.entries : [];
        return arr.find((e) => Number(e && e.no || 0) === no) || null;
      }
      function teAutoPySnapshotMediaByNo(snapshot, rankNo) {
        const entry = teAutoPySnapshotEntryByNo(snapshot, rankNo);
        return entry && entry.media || null;
      }
      function teAutoPySnapshotMediaSummary(snapshot, rankNo) {
        const entry = teAutoPySnapshotEntryByNo(snapshot, rankNo);
        if (!entry) return null;
        return {
          snapshot_id: String(entry.snapshot_id || snapshot && snapshot.snapshot_id || ""),
          rank_no: Number(entry.no || 0) || 0,
          rank_label: entry.label || teAutoPyNoLabel(entry.no),
          shortcode: String(entry.shortcode || ""),
          key: String(entry.key || ""),
          list_index: Number(entry.list_index || 0) || 0,
          sort_field: String(entry.sort_field || snapshot && snapshot.sort_field || ""),
          threshold: Number(entry.threshold || snapshot && snapshot.threshold || 0) || 0
        };
      }
      function teAutoPyDownloadableKindsFromCache(media) {
        const m = media && typeof media === "object" ? media : {};
        const out = [];
        if (String(m.video_url || "").trim()) out.push("mp4");
        if (String(m.img_origin || m.img_large || m.img_small || m.img_b64 || "").trim()) out.push("jpg");
        return out;
      }
      function teAutoPyCacheTargetForMedia(media, rankNo) {
        const m = media && typeof media === "object" ? media : {};
        const rank = Number(rankNo || 0) || 0;
        return {
          rank,
          code: String(m.code || m.shortcode || "").trim(),
          shortcode: String(m.code || m.shortcode || "").trim(),
          asset_kind: m.video_url ? "video" : "image",
          source: "side_cache_rank_fallback"
        };
      }
      async function teAutoPyDownloadMediaPairFromCache(media, rankNo, cycleBase, reason) {
        const m = media && typeof media === "object" ? media : null;
        const no = Number(rankNo || 0) || 0;
        const base = Object.assign({}, cycleBase || {}, { rank_no: no, rank_label: teAutoPyNoLabel(no), requested_rank_no: no });
        const target = teAutoPyCacheTargetForMedia(m, no);
        const shortcode = String(target.code || "").trim();
        const kinds = teAutoPyDownloadableKindsFromCache(m);
        const mediaSummary = m ? { shortcode, rank_no: no, has_video_url: !!m.video_url, has_image_url: !!(m.img_origin || m.img_large || m.img_small || m.img_b64) } : null;
        await teAutoPyCycle("S00_CACHE_LOOKUP_REQUEST", Object.assign({}, base, {
          decision: "cache_lookup",
          action: base.auto_snapshot_lookup ? "find_requested_No_in_auto_snapshot_index" : "find_requested_No_in_side_array",
          reason: String(reason || "active_no_mismatch_or_preload_failed"),
          shortcode,
          cache_candidate_found: !!m,
          auto_snapshot_lookup: !!base.auto_snapshot_lookup,
          auto_snapshot_match: !!base.auto_snapshot_match,
          auto_snapshot: base.auto_snapshot || null
        }));
        if (!m || !kinds.length || !shortcode) {
          await teAutoPyCycle("S00_CACHE_LOOKUP_MISS", Object.assign({}, base, {
            decision: "cache_miss",
            action: "fallback_to_strict_stop",
            reason: !m ? "requested_no_not_found_in_side_array" : !shortcode ? "cache_media_shortcode_missing" : "cache_media_url_missing",
            shortcode,
            cache_downloadable_kinds: kinds.join(",")
          }));
          return { ok: false, error: !m ? "cache_media_not_found" : !shortcode ? "cache_shortcode_missing" : "cache_url_missing", media_summary: mediaSummary, kinds };
        }
        const collectMeta = teBuildAutoPyCollectMeta(m, target);
        await teAutoPyCycle("S00_CACHE_LOOKUP_OK", Object.assign({}, base, {
          decision: "cache_target_found",
          action: base.auto_snapshot_lookup ? "use_auto_snapshot_download_url" : "use_side_array_download_url",
          reason: base.auto_snapshot_match ? "requested_no_found_in_auto_snapshot_cache" : "requested_no_found_in_ext_cache",
          shortcode,
          cache_downloadable_kinds: kinds.join(","),
          has_video_url: !!m.video_url,
          has_image_url: !!(m.img_origin || m.img_large || m.img_small || m.img_b64),
          filename_stem: collectMeta.filename_stem || ""
        }));
        const results = [];
        for (const kind of ["mp4", "jpg"]) {
          if (!kinds.includes(kind)) continue;
          const meta = teAutoPyCollectMetaForMedia(collectMeta, m, kind);
          const url = teAutoPySourceUrlForMedia(m, collectMeta, kind);
          const prefix = teAutoPyPrefixForMedia(collectMeta, kind);
          if (!url) continue;
          if (!prefix || collectMeta.owner_missing || meta.owner_missing || !teCleanOwnerAccountName(meta.owner_username)) {
            await teAutoPyCycle("S00_CACHE_DOWNLOAD_OWNER_MISSING", Object.assign({}, base, {
              decision: "download_blocked",
              action: "owner_required_before_download",
              reason: "media_owner_username_not_found",
              media_type: kind,
              asset_kind: meta.asset_kind,
              shortcode
            }));
            return { ok: false, error: "owner_missing", media_summary: mediaSummary, kinds, results };
          }
          await teAutoPyCycle(kind === "jpg" ? "S00_CACHE_DOWNLOAD_JPG_START" : "S00_CACHE_DOWNLOAD_MP4_START", Object.assign({}, base, {
            decision: "download_from_cache",
            action: "ws8771_direct_download_url",
            reason: "cached_media_url_available",
            media_type: kind,
            asset_kind: meta.asset_kind,
            shortcode,
            file_name: prefix,
            source_url: String(url).slice(0, 260),
          }));
          const direct = await teAutoPyDirectDownloadAsset(kind, m, collectMeta, base, {
            cache_fallback: true,
            fallback_reason: String(reason || "active_no_mismatch_or_preload_failed"),
            active_dom_required: false,
            requested_rank_no: no
          });
          const ok = !!(direct && direct.ok);
          if (ok) teAutoPyStatsRecordAsset(kind, Number(direct.elapsed_ms || 0) || 0);
          results.push(Object.assign({ media_type: kind, asset_kind: meta.asset_kind, ok, file_name: prefix }, direct || {}));
          await teAutoPyCycle(kind === "jpg" ? "S00_CACHE_DOWNLOAD_JPG_OK" : "S00_CACHE_DOWNLOAD_MP4_OK", Object.assign({}, base, {
            decision: ok ? "download_ok" : "download_failed",
            action: "ws8771_direct_download_url_result",
            reason: ok ? "cache_url_download_or_already_done" : "cache_url_download_failed",
            media_type: kind,
            asset_kind: meta.asset_kind,
            shortcode,
            file_name: prefix,
            result_ok: !!ok
          }));
          if (!ok) return { ok: false, error: "cache_" + kind + "_download_failed", media_summary: mediaSummary, kinds, results };
          await teSleep(900);
        }
        const anyOk = results.some((x) => x.ok);
        const pairDelaySec = anyOk ? Math.max(2, Number(teDownloadDelaySec && teDownloadDelaySec() || 0) || 0) : 0;
        if (pairDelaySec > 0) {
          await teAutoPyCycle("S00_CACHE_DOWNLOAD_PAIR_DELAY", Object.assign({}, base, {
            decision: "wait_after_media_pair",
            action: "delay_before_next_rank",
            reason: "cache_mp4_jpg_pair_done",
            wait_sec: pairDelaySec,
            shortcode,
            results
          }));
          await teSleep(pairDelaySec * 1000);
        }
        await teAutoPyCycle("S00_CACHE_DOWNLOAD_PAIR_DONE", Object.assign({}, base, {
          decision: anyOk ? "cache_download_done" : "cache_download_empty",
          action: "complete_cached_url_downloads",
          reason: anyOk ? "requested_no_cached_url_download_done" : "no_cached_url_download_executed",
          shortcode,
          results
        }));
        if (anyOk) teAutoPySetLastRank(no, "cache_download_done");
        return { ok: anyOk, cache_download_done: anyOk, media_summary: mediaSummary, kinds, results, shortcode };
      }
      async function teAutoPyTryCacheDownloadForRank(rankNo, list, ctx, cycleBase, reason) {
        const no = Number(rankNo || 0) || 0;
        const arr = Array.isArray(list) && list.length ? list : teMetricSourceList();
        const c = ctx && typeof ctx === "object" ? ctx : teBuildRankContext(arr);
        const snap = c && (c.auto_snapshot_index || c.snapshot_index || c.snapshotIndex) || null;
        const snapEntry = teAutoPySnapshotEntryByNo(snap, no);
        const media = snapEntry && snapEntry.media ? snapEntry.media : teAutoPyFindMediaByRankNo(no, arr, c);
        const base = Object.assign({}, cycleBase || {}, {
          auto_snapshot_lookup: !!snap,
          auto_snapshot_match: !!snapEntry,
          auto_snapshot: snapEntry ? teAutoPySnapshotMediaSummary(snap, no) : null
        });
        const expectedCode = String(base.expected_shortcode || snapEntry && snapEntry.shortcode || "").trim();
        const mediaCode = String(media && (media.__igblue_auto_snapshot_shortcode || media.code || media.shortcode) || "").trim();
        if (expectedCode && mediaCode !== expectedCode) {
          await teAutoPyCycle("S00_CACHE_SHORTCODE_MISMATCH", Object.assign({}, base, {
            decision: "cache_blocked",
            action: "require_exact_No_shortcode_before_direct_download",
            reason: "cache_shortcode_mismatch",
            expected_shortcode: expectedCode,
            cache_shortcode: mediaCode
          }));
          return { ok: false, error: "cache_shortcode_mismatch", requested_rank_no: no, expected_shortcode: expectedCode, cache_shortcode: mediaCode };
        }
        return teAutoPyDownloadMediaPairFromCache(media, no, base, reason);
      }
      async function teAutoPyPreloadRequestedNo(requestedNo, maxRank, base, mySeq, sourceList, rankCtx, opts = {}) {
        const reqNo = Number(requestedNo || 0) || 0;
        if (!(reqNo > 0)) return { ok: true, skipped: true, reason: "no_requested_no" };
        const allowCacheFallback = !(opts && opts.allow_cache_fallback === false);
        const startSource = String(base && base.start_source || base && base.hash_start_source || "requested_no");
        const info = Object.assign({}, base || {}, { rank_no: reqNo, rank_label: teAutoPyNoLabel(reqNo), requested_rank_no: reqNo, max_rank_no: Math.max(Number(maxRank || 0) || 0, reqNo), loaded_ui_count: teAutoPyLoadedUiCount(), start_source: startSource });
        await teAutoPyCycle("S00_HASH_START_REQUEST", Object.assign({}, info, { decision: "start_rank_request", action: "read_start_No", reason: startSource + "_preload" }));
        for (let attempt = 1; attempt <= 8; attempt++) {
          if (mySeq && mySeq !== teAutoPyRunSeq) return { ok: false, error: "run_seq_changed" };
          try { teAutoPyBuildDynamicRankPlan(reqNo); } catch (_) {}
          const uiLoadedCountNow = teAutoPyLoadedUiCount();
          const state = await teAutoPyReadLoadedRankState(180, reqNo);
          const snap = teAutoPyCountsSnapshot(reqNo, state);
          const loadedMax = Number(state && state.loaded_max_no || 0) || 0;
          const effectiveLoadedMax = Number(snap.top_sorted_count || snap.ready_count || 0) || 0;
          const hasRequested = effectiveLoadedMax >= reqNo;
          const uiSaysLoaded = uiLoadedCountNow >= reqNo;
          await teAutoPyCycle("S00_LOAD_CHECK", Object.assign({}, info, snap, { decision: hasRequested ? "requested_loaded" : "requested_not_loaded", action: "check_top_sorted_rank", reason: hasRequested ? "top_sorted_count_enough" : "top_sorted_requested_missing", attempt, active_hash_no: teAutoPyActiveHashNo(state), loaded_ready_no: effectiveLoadedMax, loaded_max_no: effectiveLoadedMax, dom_loaded_max_no: loadedMax, effective_loaded_max_no: effectiveLoadedMax, loaded_ui_count: uiLoadedCountNow, top_sorted_count: effectiveLoadedMax, requested_rank_no: reqNo, loaded_count: Number(state && state.loaded_count || 0) || 0, current_rank_no: Number(state && state.current_rank_no || 0) || 0, state_response: state || null }));
          if (hasRequested) {
            const goRes = await teAutoPyGoNoWithConfirm(reqNo, Object.assign({}, info, { next_reason: "hash_start_preload_ready" }), 1800, "S00_PRELOAD_GO_NO");
            const read = await teAutoPyReadCurrentNoNav(700);
            const afterRank = Number(read && read.rank_no || goRes && goRes.after_rank || 0) || 0;
            const ok = afterRank === reqNo || !!(goRes && goRes.target_found && Number(goRes.after_rank || 0) === reqNo);
            await teAutoPyCycle(ok ? "S00_PRELOAD_OK" : "S00_PRELOAD_GO_RETRY", Object.assign({}, info, { decision: ok ? "preload_ok" : "preload_go_unconfirmed", action: "go_no_after_loaded", reason: ok ? "requested_no_current" : "requested_no_go_unconfirmed", attempt, loaded_max_no: loadedMax, effective_loaded_max_no: effectiveLoadedMax, loaded_ui_count: uiLoadedCountNow, active_rank_no: afterRank, go_response: goRes || null, current_read: read || null }));
            if (ok) return { ok: true, active_rank_no: afterRank, loaded_max_no: effectiveLoadedMax, go_response: goRes || null };
            if (hasRequested || uiSaysLoaded) {
              await teSleep(500);
              continue;
            }
          }
          const gap = reqNo - Math.max(effectiveLoadedMax, 0);
          const swipeCount = gap > 3 ? 10 : 1;
          await teAutoPyCycle("S00_PRELOAD_SCROLL", Object.assign({}, info, { decision: "scroll_for_requested_no", action: swipeCount >= 10 ? "swipe_10" : "swipe_down", reason: "requested_no_not_loaded", attempt, active_hash_no: teAutoPyActiveHashNo(state), loaded_ready_no: effectiveLoadedMax, loaded_max_no: loadedMax, effective_loaded_max_no: effectiveLoadedMax, loaded_ui_count: uiLoadedCountNow, requested_rank_no: reqNo, swipe_count: swipeCount }));
          await teAutoPySwipeLoad(swipeCount, "start_no_preload");
          await teAutoPyCycle("S00_PRELOAD_WAIT", Object.assign({}, info, { decision: "wait_after_scroll", action: "wait_rank_loading", reason: "scroll_requested", attempt, requested_rank_no: reqNo }));
          await teSleep(700);
        }
        const cacheRes = allowCacheFallback ? await teAutoPyTryCacheDownloadForRank(reqNo, sourceList, rankCtx, info, "requested_no_not_loaded_after_scroll") : { ok: false, skipped: true, reason: "cache_fallback_disabled" };
        if (allowCacheFallback && cacheRes && cacheRes.ok) {
          await teAutoPyCycle("S00_PRELOAD_CACHE_FALLBACK_OK", Object.assign({}, info, {
            decision: "preload_cache_download_ok",
            action: "continue_after_cache_download",
            reason: "requested_no_downloaded_from_ext_cache_after_preload_failed",
            requested_rank_no: reqNo,
            cache_result: cacheRes
          }));
          return Object.assign({ ok: true, reason: "cache_download_after_preload_failed", requested_no: reqNo }, cacheRes, { cache_download_done: true });
        }
        await teAutoPyCycle("S00_PRELOAD_FAILED", Object.assign({}, info, { decision: "preload_failed", action: "stop_before_auto_click", reason: "requested_no_not_loaded_after_scroll", requested_rank_no: reqNo, cache_result: cacheRes || null }));
        return { ok: false, error: "requested_no_not_loaded_after_scroll", requested_no: reqNo, cache_result: cacheRes || null };
      }
      function teMetricThresholdFolderName(unitsRaw) {
        const units = Number(unitsRaw);
        if (!Number.isFinite(units) || units <= 0) return "";
        const views = units * 10000;
        const trim = (v) => {
          const n = Math.round(Number(v) * 10) / 10;
          return String(Number.isInteger(n) ? Math.trunc(n) : n).replace(/\.0$/, "");
        };
        if (views >= 10000) return trim(views / 10000) + "만";
        if (views >= 1000) return trim(views / 1000) + "천";
        return String(Math.max(1, Math.round(views)));
      }
      let teDownloadRootProgrammaticChange = false;
      function teCurrentMetricFolderSuggestion() {
        const input = document.getElementById("top_n_count");
        return teSafeName(teMetricThresholdFolderName(input ? input.value : teLocalGetAny(["filter/metric_threshold_10k", "top_metric_units", "top_n_count"], "")) || "");
      }
      function teApplyMetricFolderSuggestion(reason) {
        const rootEl = document.getElementById("download_root_folder");
        if (!rootEl) return false;
        const suggested = teCurrentMetricFolderSuggestion();
        if (!suggested) return false;
        const current = teSafeName(rootEl.value || teLocalGetAny(["download/root_folder", "download_root_folder"], "") || "");
        const lastAuto = teSafeName(teLocalGet("download/root_folder_auto_last", "") || "");
        const manual = teLocalGet("download/root_folder_manual", "0") === "1";
        if (manual && current && current !== lastAuto) return false;
        if (current === suggested) {
          teLocalSet("download/root_folder_auto_last", suggested);
          teLocalSet("download/root_folder_manual", "0");
          return false;
        }
        teDownloadRootProgrammaticChange = true;
        try { rootEl.value = suggested; } finally { teDownloadRootProgrammaticChange = false; }
        teLocalSet("download/root_folder_auto_last", suggested);
        teLocalSet("download/root_folder_manual", "0");
        tePersistDownloadRootNow();
        teUpdateDownloadFolderPath();
        try { teLoadDownTxtLocal(teDownTxtAccount()); } catch (_) {}
        return true;
      }
      function teDownloadRoot() {
        const e = document.getElementById("download_root_folder");
        return teSafeName(e?.value || teLocalGet("download/root_folder", "10만") || "10만");
      }
      function tePersistDownloadRootNow() {
        const e = document.getElementById("download_root_folder");
        const value = teSafeName(e?.value || teLocalGet("download/root_folder", "10만") || "10만");
        if (e && e.value !== value) e.value = value;
        teLocalSet("download/root_folder", value);
        teLocalSet("download_root_folder", value);
        try { chrome.storage && chrome.storage.local && chrome.storage.local.set({ "IGblue/download/root_folder": value, "IGblue/download_root_folder": value }); } catch {}
        return value;
      }
      function teDownloadAccountFolderMerge() {
        const el = document.getElementById("download_account_folder_merge");
        if (el) return !!el.checked;
        return teLocalGet("download/account_folder_merge", "0") === "1";
      }
      function tePersistDownloadAccountFolderMergeNow() {
        const el = document.getElementById("download_account_folder_merge");
        const value = el && el.checked ? "1" : "0";
        teLocalSet("download/account_folder_merge", value);
        try { chrome.storage && chrome.storage.local && chrome.storage.local.set({ "IGblue/download/account_folder_merge": value }); } catch {}
        return value === "1";
      }
      function teAccountFromPrefix(e) {
        const t = String(e || "").match(/^(.*)_\d{8}_\d{6}(?:_\d+)?$/);
        return t && t[1] ? teSafeName(t[1]) : "";
      }
      function teCurrentAccountName(e) {
        return teMediaOwnerName(e || {}) || teSafeName(teAccountFromUrl(teCurrentTargetUrl()) || "instagram");
      }
      function teUpdateDownloadFolderPath(e) {
        const t = document.getElementById("download_folder_path");
        if (!t) return;
        const n = teSafePath(e || teActiveFolderAccountName() || "instagram");
        const merged = (typeof teDownloadSessionAccountFolderMerge === "boolean") ? teDownloadSessionAccountFolderMerge : teDownloadAccountFolderMerge();
        const i = merged ? `..\\_StDown\\instagram\\` : `..\\_StDown\\instagram\\${n}\\`;
        t.textContent = i;
        t.title = merged ? "계정.폴더통합 ON: 파일은 _StDown\\instagram에 저장됩니다." : "Instagram 파일은 _StDown\\instagram\\계정 폴더에 저장됩니다.";
      }
      function teInitDownloadFolderUi() {
        const e = document.getElementById("download_root_folder"), t = document.getElementById("open_download_folder"), r = document.getElementById("download_folder_path"), mergeEl = document.getElementById("download_account_folder_merge");
        const apply = () => { tePersistDownloadRootNow(); tePersistDownloadAccountFolderMergeNow(); teUpdateDownloadFolderPath(); teLoadDownTxtLocal(teDownTxtAccount()); };
        if (mergeEl && !mergeEl.__igblueBound) {
          mergeEl.__igblueBound = true;
          mergeEl.checked = teLocalGet("download/account_folder_merge", "0") === "1";
          try { chrome.storage && chrome.storage.local && chrome.storage.local.get(["IGblue/download/account_folder_merge"], (got) => {
            const saved = got && got["IGblue/download/account_folder_merge"];
            if (saved === "1" || saved === "0") { mergeEl.checked = saved === "1"; apply(); }
          }); } catch {}
          mergeEl.addEventListener("change", apply);
        }
        if (e) {
          e.value = teLocalGetAny(["download/root_folder", "download_root_folder"], e.value || "10만");
          try { chrome.storage && chrome.storage.local && chrome.storage.local.get(["IGblue/download/root_folder", "IGblue/download_root_folder"], (got) => {
            const saved = got && (got["IGblue/download/root_folder"] || got["IGblue/download_root_folder"]);
            if (saved && document.getElementById("download_root_folder") === e && e.value !== saved) { e.value = saved; apply(); }
          }); } catch {}
          let timer = 0;
          const markManual = () => {
            if (teDownloadRootProgrammaticChange) return;
            const current = teSafeName(e.value || "");
            const suggested = teCurrentMetricFolderSuggestion();
            teLocalSet("download/root_folder_manual", current && suggested && current === suggested ? "0" : "1");
          };
          const live = () => { markManual(); clearTimeout(timer); timer = setTimeout(apply, 250); };
          e.addEventListener("input", live);
          e.addEventListener("change", () => { markManual(); apply(); });
          e.addEventListener("blur", () => { markManual(); apply(); });
          e.addEventListener("keydown", (ev) => { if (ev.key === "Enter") { ev.preventDefault(); markManual(); apply(); } });
          apply();
        }
        const i = () => {
          try { chrome.runtime.sendMessage({ type: "IGBLUE_OPEN_DOWNLOAD_FOLDER" }, () => { chrome.runtime.lastError; }); } catch {}
        };
        t && t.addEventListener("click", i);
        r && r.addEventListener("click", i);
        teUpdateDownloadFolderPath();
      }

      // IGblue local patch v96: WS 통신 본체는 side_ws.js로 분리한다.
      const teAiwkSideWs = window.IGBLUE_SIDE_WS || {};
      const teAiwkSetupZipUrl = teAiwkSideWs.setupZipUrl || "http://aiwk.yjm.kr/down/_St.zip";
      function teAiwkIsWsFailure(res) {
        if (teAiwkSideWs && typeof teAiwkSideWs.isWsFailure === "function") return teAiwkSideWs.isWsFailure(res);
        if (!res || res.ok) return false;
        const code = String(res.error_code || res.error || "").toUpperCase();
        return /AIWK|WEBSOCKET|WS_|TIMEOUT|CONNECTION|REFUSED|EMPTY_RESPONSE|BAD_JSON|UNKNOWN_ACT|BAD_TOKEN/.test(code);
      }
      function teAiwkSetupNotice(show, detail) {
        const el = document.getElementById("igblue_aiwk_setup_notice");
        if (!el) return;
        el.classList.toggle("is-visible", !!show);
        if (show && detail) el.title = String(detail);
      }
      function teAiwkTrackWsResult(res) {
        if (res && res.ok) teAiwkSetupNotice(false);
        else if (teAiwkIsWsFailure(res)) teAiwkSetupNotice(true, res.error_code || res.error || "WS 대기중 / 로컬 host 미접속");
        return res;
      }
      function teDownTxtStatus(msg) {
        const el = document.getElementById("igblue_down_txt_status");
        if (el) el.textContent = "목록: " + String(msg || "");
      }
      function teDownTxtAccount() {
        return teSafePath(teActiveFolderAccountName() || "instagram");
      }
      function teDownTxtHost() {
        try {
          const u = new URL(teCurrentTargetUrl() || "https://www.instagram.com/");
          return u.hostname || "www.instagram.com";
        } catch { return "www.instagram.com"; }
      }
      function teDownTxtStorageKey(account) {
        return "download/folder_json_list/" + teDownloadRoot() + "/" + (teDownloadAccountFolderMerge() ? "__merged__" : teSafePath(account || teDownTxtAccount()));
      }
      function teDownItemKey(item) {
        const it = item && typeof item === "object" ? item : {};
        const sc = String(it.shortcode || "").trim();
        const kind = String(it.asset_kind || it.kind || "video").toLowerCase();
        const idx = Number(it.media_index || 0) || 0;
        return [sc, kind, idx].join("|").toLowerCase();
      }
      function teRankLabelFromItem(item) {
        const it = item && typeof item === "object" ? item : {};
        const label = String(it.rank_label || "").trim();
        if (/^No\s*\d+/i.test(label)) return label.replace(/\s+/g, "");
        const no = Number(it.rank_no || 0) || 0;
        return no > 0 ? teAutoPyNoLabel(no) : "No---";
      }
      function teNormalizeDownItems(items) {
        const out = [];
        const seen = new Set();
        if (Array.isArray(items)) {
          for (const raw of items) {
            if (!raw || typeof raw !== "object") continue;
            const kind = String(raw.asset_kind || raw.kind || "").toLowerCase();
            if (kind && !/video|mp4|image|jpg|jpeg/.test(kind)) continue;
            const it = Object.assign({}, raw, { asset_kind: /image|jpg|jpeg/.test(kind) ? "image" : "video" });
            const k = teDownItemKey(it) || JSON.stringify([it.rank_label, it.rank_no, it.asset_kind, it.file_name || ""]);
            if (seen.has(k)) continue;
            seen.add(k);
            out.push(it);
          }
        } else {
          const text = String(items || "");
          text.split(/\r?\n/).map(x => x.trim()).filter(Boolean).forEach((name, i) => {
            const file = name.split(/[\\/]/).pop();
            if (!/\.mp4$/i.test(file)) return;
            const m = file.match(/_No(\d{3})_/i);
            const it = { rank_no: m ? Number(m[1]) : 0, rank_label: m ? ("No" + m[1]) : "", asset_kind: "video", file_name: file, shortcode: "" };
            const k = teDownItemKey(it) || file.toLowerCase();
            if (seen.has(k)) return;
            seen.add(k);
            out.push(it);
          });
        }
        out.sort((a, b) => (Number(a.rank_no || 999999) - Number(b.rank_no || 999999)) || String(a.shortcode || "").localeCompare(String(b.shortcode || "")));
        return out;
      }
      let teDownItems = [];
      let teDownIndex = Object.create(null);
      function teRebuildDownIndex() {
        teDownIndex = Object.create(null);
        for (const it of teDownItems) {
          const k = teDownItemKey(it);
          if (k && !/^\|/.test(k)) teDownIndex[k] = true;
        }
      }
      function teDownItemExists(meta, kind) {
        const m = meta && typeof meta === "object" ? meta : {};
        const k = [String(m.shortcode || "").trim(), String(kind || m.asset_kind || "video").toLowerCase(), Number(m.media_index || 0) || 0].join("|").toLowerCase();
        return !!(k && teDownIndex[k]);
      }
      function teRenderDownList(items, account) {
        teDownItems = teNormalizeDownItems(items);
        teRebuildDownIndex();
        const el = document.getElementById("igblue_down_file_list");
        if (el) {
          el.innerHTML = "";
          if (!teDownItems.length) {
            const empty = document.createElement("div");
            empty.className = "igblue-downlog-empty";
            empty.textContent = "목록 없음";
            el.appendChild(empty);
          } else {
            for (const item of teDownItems) {
              const row = document.createElement("div");
              row.className = "igblue-downlog-item";
              row.dataset.shortcode = String(item.shortcode || "");
              row.dataset.kind = String(item.asset_kind || "");
              row.dataset.rank = teRankLabelFromItem(item);
              const rank = document.createElement("span"); rank.className = "rank"; rank.textContent = teRankLabelFromItem(item);
              const kind = document.createElement("span"); kind.className = "kind"; kind.textContent = /image|jpg/i.test(String(item.asset_kind || "")) ? "JPG" : "MP4";
              const meta = document.createElement("span"); meta.className = "meta";
              const parts = [];
              if (item.views) parts.push("조회 " + item.views);
              if (item.likes) parts.push("하트 " + item.likes);
              if (item.comments) parts.push("댓글 " + item.comments);
              if (item.shortcode) parts.push(String(item.shortcode));
              meta.textContent = parts.join(" · ") || "완료";
              row.append(rank, kind, meta);
              el.appendChild(row);
            }
            el.scrollTop = el.scrollHeight;
          }
        }
        try { teLocalSet(teDownTxtStorageKey(account), JSON.stringify(teDownItems)); } catch (_) {}
        return teDownItems;
      }
      function teNormalizeDownTxt(text) {
        return teNormalizeDownItems(text).map(x => x.file_name || x.shortcode || teRankLabelFromItem(x)).filter(Boolean).join("\n");
      }
      function teGetDownTxtText() {
        return teDownItems.map(x => x.file_name || x.shortcode || teRankLabelFromItem(x)).filter(Boolean).join("\n");
      }
      function teSetDownTxtText(text, account) {
        const items = teNormalizeDownItems(text);
        teRenderDownList(items, account);
        return items.map(x => x.file_name || x.shortcode || teRankLabelFromItem(x)).filter(Boolean).join("\n");
      }
      function teLoadDownTxtLocal(account) {
        let raw = teLocalGet(teDownTxtStorageKey(account), "");
        let items = [];
        try { items = raw ? JSON.parse(raw) : []; } catch (_) { items = teNormalizeDownItems(raw); }
        teRenderDownList(items, account);
        teDownTxtStatus(teDownloadAccountFolderMerge() ? ("local " + teDownloadRoot() + ".json") : ("local " + teDownloadRoot() + "/" + teSafePath(account || teDownTxtAccount()) + ".json"));
      }
      async function teAiwkPollThreadQueueResult(accepted, originalAct, timeoutMs = 4500, opts = {}) {
        const jobId = String(accepted && accepted.job_id || "").trim();
        if (!jobId || !teAiwkSideWs || typeof teAiwkSideWs.request !== "function") return accepted;
        const started = Date.now();
        const deadline = started + Math.max(1500, Number(timeoutMs || 4500) || 4500);
        const pollMs = Math.max(120, Math.min(1500, Number(accepted.poll_interval_ms || 250) || 250));
        let last = null;
        while (Date.now() < deadline) {
          const payload = Object.assign({}, teCurrentWsTargetPayload("ig_thread_queue_result", { host: teDownTxtHost() }), { job_id: jobId, original_act: originalAct });
          try {
            const poll = await teAiwkSideWs.request("ig_thread_queue_result", payload, {
              timeoutMs: Math.min(2200, Math.max(800, deadline - Date.now())),
              forceProbe: true,
              from: { endpoint: "thread_queue_poll", href: teCurrentTargetUrl(), host: teDownTxtHost(), tab_id: payload.tab_id || 0 }
            });
            last = poll;
            if (poll && poll.done) {
              const result = poll.result && typeof poll.result === "object" ? poll.result : {};
              result.thread_queue_mode = true;
              result.thread_queue_job_id = jobId;
              result.thread_queue_status = poll.status || "done";
              return result;
            }
            if (poll && poll.ok === false && poll.error === "job_not_found") {
              return Object.assign({ ok: false, error: "thread_queue_job_not_found", job_id: jobId, original_act: originalAct }, poll || {});
            }
          } catch (err) {
            last = { ok: false, error: String(err && err.message || err) };
          }
          await teSleep(pollMs);
        }
        return { ok: false, error: "thread_queue_result_timeout", job_id: jobId, original_act: originalAct, accepted, last_poll: last };
      }

      function teAiwkWsRequest(act, data, timeoutMs = 4500, opts = {}) {
        if (!teAiwkSideWs || typeof teAiwkSideWs.request !== "function") {
          return Promise.resolve({ ok: 0, error_code: "SIDE_WS_MODULE_NOT_LOADED", state: "ws_waiting", expected_offline: true });
        }
        const payload = Object.assign({}, teCurrentWsTargetPayload(act, { host: teDownTxtHost() }), data || {});
        return teAiwkSideWs.request(String(act || ""), payload, {
          timeoutMs,
          forceProbe: opts && opts.forceProbe === true,
          from: { endpoint: "domain_down_txt", href: teCurrentTargetUrl(), host: teDownTxtHost(), tab_id: payload.tab_id || 0 }
        }).then(async (res) => {
          if (res && res.thread_queue_mode && res.queued && res.job_id) {
            res = await teAiwkPollThreadQueueResult(res, act, timeoutMs, opts || {});
          }
          return teAiwkTrackWsResult(res);
        });
      }


      function teArmWsAutoEnter(reason, detail = {}) {
        try {
          return teAiwkWsRequest("ig_rtc_enter_auto_arm", Object.assign({
            reason: reason || "manual_main_click",
            source: "main_manual_download_button",
            href: teCurrentTargetUrl(),
            host: teDownTxtHost()
          }, detail || {}), 2500, { forceProbe: true }).then(teAiwkTrackWsResult);
        } catch (err) {
          return Promise.resolve({ ok: false, error: String(err && err.message || err) });
        }
      }
      let tePyAutoStatusTimer = null;
      let tePyStatusProbeBusy = false;
      const tePyStatusProbeMs = 10000;
      let tePyLastOkAt = 0;
      let tePyStatusFailSince = 0;
      function tePyStartStatusProbe(reason) {
        if (tePyAutoStatusTimer) clearInterval(tePyAutoStatusTimer);
        setTimeout(() => { tePyProbeStatusOnce(reason || "init"); }, 350);
        tePyAutoStatusTimer = setInterval(() => { tePyProbeStatusOnce("interval"); }, tePyStatusProbeMs);
      }
      async function tePyProbeStatusOnce(reason) {
        if (tePyStatusProbeBusy) return teAutoPyLastPyOk;
        tePyStatusProbeBusy = true;
        try {
          return await tePyPingOnce(true);
        } finally {
          tePyStatusProbeBusy = false;
        }
      }
      const teAutoPyFeatureCheckSeen = new Set();
      function teAutoPyFeatureCheckReset(runSeq) {
        teAutoPyFeatureCheckSeen.clear();
        teAutoPyFeatureCheckSeen.add(`RUN|${Number(runSeq || 0)}`);
      }
      function teAutoPyFeatureCheck(checkId, status, messageKo, detail = {}) {
        try {
          const check = String(checkId || "").trim();
          const state = String(status || "").trim();
          if (!check || !state) return Promise.resolve(null);
          const d = detail && typeof detail === "object" ? detail : {};
          const cycleId = String(d.cycle_id || d.feature_cycle_id || d.client_batch_key || `run_${Number(teAutoPyRunSeq || 0)}`);
          const asset = String(d.asset_kind || d.media_type || "");
          const key = [Number(teAutoPyRunSeq || 0), cycleId, check, state, asset].join("|");
          if (teAutoPyFeatureCheckSeen.has(key)) return Promise.resolve(null);
          teAutoPyFeatureCheckSeen.add(key);
          const line = `[자동다운로드][${check}][${state}] ${String(messageKo || "").trim()}`;
          try { n(line); } catch (_) {}
          const payload = Object.assign({
            schema: IGBLUE_CYCLE_SCHEMA,
            build_version: IGBLUE_BUILD_VERSION,
            build_tag: IGBLUE_BUILD_TAG,
            cycle_stage: "FEATURE_CHECK",
            feature_check_mode: true,
            feature_name_ko: "자동다운로드",
            check_id: check,
            check_status: state,
            check_message_ko: String(messageKo || "").trim(),
            cycle_id: cycleId,
            run_seq: Number(teAutoPyRunSeq || 0),
            href: teCurrentTargetUrl(),
            host: teDownTxtHost()
          }, d);
          return teAiwkWsRequest("ig_auto_cycle_state", payload, 1800, { forceProbe: true }).catch(() => null);
        } catch (_) {
          return Promise.resolve(null);
        }
      }
      function teAutoPyCheckedRaw() {
        const cb = document.getElementById("igblue_auto_py_down");
        return !!(cb && cb.checked);
      }
      function teAutoPyEnabled() {
        // v107: 체크 상태와 WS 연결 상태를 분리한다.
        return teAutoPyCheckedRaw();
      }
      let teAutoPyRunSeq = 0;
      let teAutoPyRunning = false;
      let teAutoPyStopping = false;
      let teAutoPyLastPyOk = false;
      function teAutoPyUpdateButtons(ok = teAutoPyLastPyOk) {
        teAutoPyLastPyOk = !!ok;
        const cb = document.getElementById("igblue_auto_py_down"), startBtn = document.getElementById("igblue_auto_py_start"), stopBtn = document.getElementById("igblue_auto_py_stop");
        if (cb) cb.disabled = !!(teAutoPyRunning || teAutoPyStopping);
        if (startBtn) {
          startBtn.textContent = "자동시작";
          startBtn.disabled = !teAutoPyCheckedRaw() || teAutoPyRunning || teAutoPyStopping;
        }
        if (stopBtn) {
          stopBtn.textContent = teAutoPyStopping ? "중지중..." : "중지";
          stopBtn.disabled = !(teAutoPyRunning || teAutoPyStopping);
          stopBtn.title = "자동 실행을 즉시 중지합니다.\n단축키: Ctrl + Shift + F12";
        }
      }
      function tePySetStatus(ok, detail) {
        const st = document.getElementById("igblue_py_status");
        const good = !!ok;
        const now = Date.now();
        if (good) {
          tePyLastOkAt = now;
          tePyStatusFailSince = 0;
        } else if (!tePyStatusFailSince) {
          tePyStatusFailSince = now;
        }
        const recentOk = !!(tePyLastOkAt && now - tePyLastOkAt < 20000);
        const transientWait = !good && !!teAutoPyRunning && (recentOk || now - tePyStatusFailSince < 60000);
        if (st) {
          st.textContent = good ? (teAutoPyRunning ? "Py RUN" : "Py OK") : (transientWait ? "WS 재확인" : "WS 대기");
          st.classList.toggle("is-ok", !!(good || transientWait));
          st.title = detail ? (transientWait ? ("WS 재확인 중: " + String(detail)) : String(detail)) : (good ? "WS 연결됨" : (transientWait ? "WS 재확인 중" : "WS 연결 대기"));
        }
        teAutoPyUpdateButtons(!!(good || transientWait));
      }
      async function tePyPingOnce(forceProbe = false) {
        try {
          const res = await teAiwkWsRequest("ping", { host: teDownTxtHost(), href: teCurrentTargetUrl() }, 1500, { forceProbe: !!forceProbe });
          const ok = !!(res && res.ok);
          tePySetStatus(ok, ok ? "ping ok" : ((res && (res.error_code || res.error)) || "ping failed"));
          return ok;
        } catch (err) {
          tePySetStatus(false, String(err && err.message || err));
          return false;
        }
      }
      async function tePyPingStable(reason = "auto", attempts = 4, intervalMs = 900, forceProbe = false) {
        const total = Math.max(1, Number(attempts || 1) || 1);
        for (let i = 1; i <= total; i++) {
          if (!teAutoPyCheckedRaw()) return { ok: false, user_off: true, reason: "auto_py_checkbox_off", attempts: i - 1 };
          const ok = await tePyPingOnce(!!forceProbe);
          if (ok) return { ok: true, attempts: i, reason: "ping_ok" };
          if (i < total) {
            const st = document.getElementById("igblue_py_status");
            if (st && (!teAutoPyRunning || i === 1)) {
              st.textContent = "WS 재확인";
              st.title = `WS ping 재확인 중: ${String(reason || "auto")} (${i}/${total})`;
            }
            await teSleep(intervalMs);
          }
        }
        return { ok: false, user_off: false, reason: "ws_ping_failed_after_retry", attempts: total };
      }
      function teInitPyAutoUi() {
        const cb = document.getElementById("igblue_auto_py_down");
        if (!cb) return;
        cb.checked = "1" === teLocalGet("download/auto_py", "0");
        teInitAutoPyLastRankUi();
        cb.addEventListener("change", () => {
          teLocalSet("download/auto_py", cb.checked ? "1" : "0");
          n(cb.checked ? "자동시작 준비" : "자동시작 꺼짐");
          if (cb.checked) setTimeout(() => { tePyProbeStatusOnce("checkbox_on"); }, 50);
        });
        tePySetStatus(false, "WS 확인중...");
        // v147: auto_service_manager가 WS8771을 띄운 뒤에도 Side가 Py OFF에 고정되지 않도록
        // Side 표시용 ping만 느린 주기로 수행한다. ping은 정상 WebSocket handshake라서 빈 TCP health check와 다르다.
        tePyStartStatusProbe("side_init");
      }
      function teSleep(ms) { return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0))); }
      function teAutoPySafeTarget(target) {
        const t = target && typeof target === "object" ? target : {};
        return !!(Number(t.rank || 0) && String(t.code || t.shortcode || "").trim());
      }
      function teAutoPyRequestWsStop(reason, detail) {
        try {
          return teAiwkWsRequest("ig_auto_cycle_state", {
            schema: IGBLUE_CYCLE_SCHEMA,
            build_version: IGBLUE_BUILD_VERSION,
            build_tag: IGBLUE_BUILD_TAG,
            cycle_stage: "S00_EXT_STOP_COMMAND",
            decision: "auto_stopped",
            action: "stop",
            reason: String(reason || "ext_stop"),
            stop_reason: String(reason || "ext_stop"),
            stop_reason_ko: String(detail || reason || "EXT stop"),
            autoplay_stop_reason: String(reason || "ext_stop"),
            autoplay_stop_reason_ko: String(detail || reason || "EXT stop"),
            href: teCurrentTargetUrl(),
            host: teDownTxtHost()
          }, 1200).catch(() => null);
        } catch (_) {
          return Promise.resolve(null);
        }
      }
      function teAutoPyStop(reason) {
        if (!teAutoPyRunning && !teAutoPyStopping) return void n("자동시작: 실행중 아님");
        teAutoPyStopping = true;
        teAutoPyRunSeq += 1;
        teAutoPyUpdateButtons(teAutoPyLastPyOk);
        teAutoPyRequestWsStop("user_stop", reason || "user");
        n("자동중지" + (reason ? ": " + reason : ""));
      }
      function teAutoPyStopReasonKo(code, detail) {
        const c = String(code || "unknown_stop");
        const map = {
          py_off: "WS 응답 제한시간 초과로 자동 중단",
          user_stop: "사용자가 AutoPlay 중지를 요청함",
          run_seq_changed: "새 AutoPlay 실행/중지 신호로 현재 사이클 중단",
          direct_media_url_missing: "현재 No의 직접 다운로드 URL을 못 읽어 자동 중단",
          direct_download_failed: "Chrome 직접 다운로드 또는 저장 완료 처리가 실패해 자동 중단",
          direct_finalize_failed: "파일 저장 후 JSON·보고서 완료 처리가 실패해 자동 중단",
          target_rank_mismatch: "요청 No와 Main active No가 달라 자동 중단",
          target_shortcode_mismatch: "요청 No의 snapshot shortcode와 Main active shortcode가 달라 자동 중단",
          target_incomplete: "현재 No의 필수 정보가 부족해 자동 중단",
          stopped_before_target_read: "target 읽기 직전 AutoPlay가 중단됨",
          stopped_before_next_rank: "다음 No 처리 직전 AutoPlay가 중단됨",
          download_point_not_loaded: "최종 다운로드 지점까지 로딩하지 못해 자동 중단",
          current_url_end_reached: "현재 주소의 끝까지 도달해 자동 중단",
        };
        let msg = map[c] || ("AutoPlay 중단: " + c);
        const d = String(detail || "").trim();
        if (d && !msg.includes(d)) msg += " / " + d;
        return msg;
      }
      function teAutoPyTargetReadCheck(target, media, rankNo) {
        const t = target && typeof target === "object" ? target : {};
        const m = media && typeof media === "object" ? media : {};
        const activeRank = Number(t.rank || 0) || 0;
        const requestedRank = Number(rankNo || m.__igblue_auto_snapshot_no || 0) || 0;
        const activeShortcode = String(t.code || t.shortcode || "").trim();
        const expectedShortcode = String(m.__igblue_auto_snapshot_shortcode || m.code || m.shortcode || "").trim();
        const shortcode = String(activeShortcode || expectedShortcode || "").trim();
        const videoUrl = teAutoPySourceUrlForMedia(m, {}, "mp4");
        const imageUrl = teAutoPySourceUrlForMedia(m, {}, "jpg");
        const assetKind = videoUrl ? "video" : imageUrl ? "image" : "";
        const hasDirectSource = !!(videoUrl || imageUrl);
        const strictSnapshot = !!(m.__igblue_auto_snapshot_no || m.__igblue_auto_snapshot_key);
        const hasRank = !!(activeRank && requestedRank);
        const rawRankMatches = !!(activeRank && requestedRank && activeRank === requestedRank);
        const shortcodeConfirmsRequested = !!(activeShortcode && expectedShortcode && activeShortcode === expectedShortcode);
        const rankStaleConfirmed = !!(!strictSnapshot && !rawRankMatches && shortcodeConfirmsRequested);
        const rankMatches = strictSnapshot ? rawRankMatches : (rawRankMatches || rankStaleConfirmed);
        const shortcodeMatches = strictSnapshot ? shortcodeConfirmsRequested : (!activeShortcode || !expectedShortcode || shortcodeConfirmsRequested);
        const A = !!(hasRank && rankMatches);
        const B = !!shortcode;
        const C = !!assetKind;
        const D = !!hasDirectSource;
        const missing = [];
        if (!hasRank) missing.push("missing_rank");
        if (hasRank && !rankMatches) missing.push("rank_mismatch");
        if (!B) missing.push("missing_shortcode");
        if (strictSnapshot && !activeShortcode) missing.push("missing_active_shortcode");
        if (strictSnapshot && activeShortcode && expectedShortcode && !shortcodeMatches) missing.push("shortcode_mismatch");
        if (!C) missing.push("missing_asset_kind");
        if (!D) missing.push("missing_direct_media_url");
        const ok = A && B && C && D && shortcodeMatches;
        const targetTs = Number(t.ts || t.updated_ts || 0) || 0;
        const ageMs = targetTs ? Math.max(0, Date.now() - targetTs) : 0;
        const mismatchReason = missing.includes("shortcode_mismatch") || missing.includes("missing_active_shortcode") ? "target_shortcode_mismatch" : missing.includes("rank_mismatch") ? "target_rank_mismatch" : missing.includes("missing_direct_media_url") ? "direct_media_url_missing" : "target_incomplete";
        return {
          ok,
          reason: ok ? (rankStaleConfirmed ? "target_complete_rank_stale_shortcode_confirmed" : strictSnapshot ? "target_complete_snapshot_rank_shortcode_confirmed" : "target_complete") : missing.join(","),
          stop_reason: ok ? "" : mismatchReason,
          stop_reason_ko: ok ? "" : teAutoPyStopReasonKo(mismatchReason, missing.join(",")),
          active_rank_no: activeRank,
          requested_rank_no: requestedRank,
          active_shortcode: activeShortcode,
          expected_shortcode: expectedShortcode,
          shortcode,
          asset_kind: assetKind,
          target_A_rank: A,
          target_B_shortcode: B,
          target_C_asset_kind: C,
          target_D_direct_media_url: D,
          target_D_button_rect: false,
          target_missing_fields: missing.join(","),
          rank_raw_match: rawRankMatches,
          rank_stale_confirmed: rankStaleConfirmed,
          shortcode_confirms_requested: shortcodeConfirmsRequested,
          shortcode_match: shortcodeMatches,
          strict_snapshot_check: strictSnapshot,
          auto_snapshot_key: String(m.__igblue_auto_snapshot_key || ""),
          active_target_age_ms: ageMs,
          target_has_raw: !!target,
          target_has_code_raw: !!String(t.code || ""),
          target_resolved_from_media: !String(t.code || "") && !!String(m.code || ""),
          direct_video_url_present: !!videoUrl,
          direct_image_url_present: !!imageUrl
        };
      }
      function teAutoPyMediaNorm(mediaType) {
        const m = String(mediaType || "").toLowerCase();
        if (m === "jpg" || m === "jpeg" || m === "image" || m === "photo") return "jpg";
        return "mp4";
      }
      function teAutoPyAssetKindForMedia(mediaType) {
        return teAutoPyMediaNorm(mediaType) === "jpg" ? "image" : "video";
      }
      function teAutoPyButtonMediaOf(btn, total) {
        const b = btn && typeof btn === "object" ? btn : {};
        const direct = String(b.media_type || b.kind || "").toLowerCase().trim();
        if (/^(mp4|video)$/.test(direct)) return "mp4";
        if (/^(jpg|jpeg|image|photo)$/.test(direct)) return "jpg";
        const text = String(b.text || "").toLowerCase();
        if (/mp4|video|동영상/.test(text)) return "mp4";
        if (/jpg|jpeg|image|photo|사진|이미지/.test(text)) return "jpg";
        const idx = Number(b.index);
        if (Number.isFinite(idx) && Number(total || 0) > 1 && idx === Number(total || 0) - 1) return "mp4";
        return "jpg";
      }
      function teAutoPyRectFromButtonItem(btn) {
        const b = btn && typeof btn === "object" ? btn : {};
        const r = b.rect && typeof b.rect === "object" ? b.rect : b.button_rect && typeof b.button_rect === "object" ? b.button_rect : b;
        const cx = Number(r.cx || 0) || (Number(r.x || r.left || 0) + Number(r.w || r.width || 0) / 2);
        const cy = Number(r.cy || 0) || (Number(r.y || r.top || 0) + Number(r.h || r.height || 0) / 2);
        const w = Number(r.w || r.width || 0) || 0;
        const h = Number(r.h || r.height || 0) || 0;
        if (!Number.isFinite(cx) || !Number.isFinite(cy) || !cx || !cy) return null;
        return Object.assign({}, r, { cx: Math.round(cx), cy: Math.round(cy), w: Math.round(w), h: Math.round(h) });
      }
      function teAutoPyButtonRectForMedia(target, mediaType) {
        const want = teAutoPyMediaNorm(mediaType);
        const t = target && typeof target === "object" ? target : {};
        const automationList = Array.isArray(t.automation_button_rects) ? t.automation_button_rects : [];
        const automationRequired = t.automation_target_required === true || t.visual_overlay_enabled === true;
        const list = automationList.length ? automationList : (automationRequired ? [] : (Array.isArray(t.download_button_rects) ? t.download_button_rects : []));
        for (const item of list) {
          if (!item || typeof item !== "object") continue;
          if (item.visible === false) continue;
          const got = teAutoPyButtonMediaOf(item, list.length);
          if (got !== want) continue;
          const rect = teAutoPyRectFromButtonItem(item);
          if (rect) return { rect, source: automationList.length ? "automation_button_rects" : "download_button_rects", item };
        }
        if (want === "mp4") {
          const rect = teAutoPyRectFromButtonItem(t.button_rect || {});
          if (rect) return { rect, source: "active_button_rect", item: null };
        }
        return null;
      }
      function teAutoPyTargetForMedia(target, mediaType) {
        const want = teAutoPyMediaNorm(mediaType);
        const found = teAutoPyButtonRectForMedia(target, want);
        if (!found || !found.rect) return null;
        const t = Object.assign({}, target || {});
        t.button_rect = found.rect;
        t.target_media_type = want;
        t.asset_kind = teAutoPyAssetKindForMedia(want);
        t.button_rect_source = found.source;
        t.button_rect_item = found.item || null;
        return t;
      }
      function teAutoPyViewportOfTarget(target) {
        const t = target && typeof target === "object" ? target : {};
        const vp = t.viewport && typeof t.viewport === "object" ? t.viewport : {};
        const w = Number(vp.innerWidth || vp.width || vp.clientWidth || 0) || 0;
        const h = Number(vp.innerHeight || vp.height || vp.clientHeight || 0) || 0;
        return { w, h, raw: vp };
      }
      function teAutoPyMediaButtonState(target, mediaType) {
        const want = teAutoPyMediaNorm(mediaType);
        const found = teAutoPyButtonRectForMedia(target, want);
        if (!found || !found.rect) return { ok: false, media_type: want, reason: "missing_" + want + "_button_rect", rect: null, found: null };
        const r = found.rect || {};
        const vp = teAutoPyViewportOfTarget(target);
        const cx = Number(r.cx || 0), cy = Number(r.cy || 0);
        const x = Number(r.x || r.left || 0), y = Number(r.y || r.top || 0);
        const w = Number(r.w || r.width || 0), h = Number(r.h || r.height || 0);
        const hasPoint = Number.isFinite(cx) && Number.isFinite(cy) && cx > 0 && cy > 0;
        if (!hasPoint) return { ok: false, media_type: want, reason: "invalid_" + want + "_button_center", rect: r, found };
        if (vp.w > 0 && vp.h > 0) {
          const centerOutside = cx < 1 || cy < 1 || cx > vp.w - 1 || cy > vp.h - 1;
          const rectClipped = Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0 && (x < 0 || y < 0 || x + w > vp.w || y + h > vp.h);
          if (centerOutside) return { ok: false, media_type: want, reason: "unsafe_" + want + "_button_center_outside_viewport", rect: r, viewport: vp, found };
          if (rectClipped) return { ok: false, media_type: want, reason: "clipped_" + want + "_button_rect", rect: r, viewport: vp, found };
        }
        return { ok: true, media_type: want, reason: "button_rect_ok", rect: r, viewport: vp, found };
      }
      async function teAutoPyReadActiveTargetOnce(waitMs = 260) {
        await teSleep(Math.max(0, Number(waitMs || 0) || 0));
        const got = await teSendToTargetTabResponse({ type: "igblue_get_active_download_target" }, "", 1800).catch((err) => ({ ok: false, error: String(err && err.message || err) }));
        return { got, target: got && got.target };
      }
      async function teAutoPyRefocusCurrentForMediaButton(mediaType, target, rankNo, currentMedia, cycleBase, reason) {
        const want = teAutoPyMediaNorm(mediaType);
        const before = teAutoPyMediaButtonState(target, want);
        await teAutoPyCycle(want === "jpg" ? "S21_JPG_BUTTON_REFOCUS" : "S10_MP4_BUTTON_REFOCUS", Object.assign({}, cycleBase || {}, {
          decision: "refocus_current_no",
          action: "dom_click_NoNav_Current_then_reread_target",
          media_type: want,
          reason: String(reason || before.reason || "button_rect_not_ready"),
          before_button_state: before,
          requested_rank_no: Number(rankNo || 0) || 0,
          expected_shortcode: String(currentMedia && (currentMedia.code || currentMedia.shortcode) || "")
        }));
        const clickRes = await teAutoPyClickCurrentNoNav(Object.assign({}, cycleBase || {}, { current_refocus: true, current_reason: "media_button_rect_retry_" + want, media_type: want, refocus_reason: String(reason || before.reason || "button_rect_not_ready") }), 220, want === "jpg" ? "S21_JPG_CURRENT_RECLICK" : "S10_MP4_CURRENT_RECLICK");
        const rr = await teAutoPyReadActiveTargetOnce(220);
        const afterTarget = rr.target || null;
        const after = teAutoPyMediaButtonState(afterTarget, want);
        await teAutoPyCycle(want === "jpg" ? "S21_JPG_BUTTON_RECHECK" : "S10_MP4_BUTTON_RECHECK", Object.assign({}, cycleBase || {}, {
          decision: after.ok ? "button_rect_ready" : "button_rect_still_bad",
          action: "reread_active_target_after_current_click",
          media_type: want,
          reason: after.reason,
          after_button_state: after,
          current_click_response: clickRes || null,
          reread_response: rr.got || null,
          active_rank_no: Number(afterTarget && afterTarget.rank || 0) || 0,
          active_shortcode: String(afterTarget && (afterTarget.code || afterTarget.shortcode) || "")
        }));
        return { ok: !!after.ok, target: afterTarget, state: after, before, current_click_response: clickRes || null, reread_response: rr.got || null, error: after.reason };
      }
      function teAutoPySourceUrlForMedia(media, collectMeta, mediaType) {
        const m = media && typeof media === "object" ? media : {};
        const c = collectMeta && typeof collectMeta === "object" ? collectMeta : {};
        if (teAutoPyMediaNorm(mediaType) === "jpg") {
          return String(m.img_origin || m.img_large || m.img_small || c.image_url || c.img_origin || c.source_image_url || "");
        }
        return String(m.video_url || c.video_url || c.source_url || "");
      }
      function teAutoPyPrefixForMedia(collectMeta, mediaType) {
        const c = collectMeta && typeof collectMeta === "object" ? collectMeta : {};
        const exp = c.expected_files && typeof c.expected_files === "object" ? c.expected_files : {};
        return teAutoPyMediaNorm(mediaType) === "jpg" ? String(exp.jpg || `${c.filename_stem || "media"}.jpg`) : String(exp.mp4 || `${c.filename_stem || "media"}.mp4`);
      }
      function teAutoPyCollectMetaForMedia(baseMeta, media, mediaType) {
        const want = teAutoPyMediaNorm(mediaType);
        const url = teAutoPySourceUrlForMedia(media, baseMeta, want);
        const prefix = teAutoPyPrefixForMedia(baseMeta, want);
        const ext = want === "jpg" ? "jpg" : "mp4";
        const originalInfo = teOriginalInstagramFileInfo(url || "");
        return Object.assign({}, baseMeta || {}, {
          asset_kind: teAutoPyAssetKindForMedia(want),
          media_kind: teAutoPyAssetKindForMedia(want),
          target_media_type: want,
          source_url: url,
          media_url: url,
          file_name: prefix,
          download_prefix: prefix,
          instagram_original_ext: originalInfo.original_ext || ext,
          instagram_original_filename: originalInfo.original_filename,
          instagram_original_filename_with_ext: originalInfo.original_filename_with_ext
        });
      }
      async function teAutoPyPrecheckMedia(mediaType, currentMedia, target, collectMeta, cycleBase) {
        const want = teAutoPyMediaNorm(mediaType);
        const meta = teAutoPyCollectMetaForMedia(collectMeta, currentMedia, want);
        const url = teAutoPySourceUrlForMedia(currentMedia, collectMeta, want);
        const prefix = teAutoPyPrefixForMedia(collectMeta, want);
        const assetKind = teAutoPyAssetKindForMedia(want);
        if (!prefix || meta.owner_missing || !teCleanOwnerAccountName(meta.owner_username)) {
          await teAutoPyCycle(want === "jpg" ? "S20_JPG_OWNER_MISSING" : "S07_MP4_OWNER_MISSING", Object.assign({}, cycleBase, {
            decision: "download_blocked",
            action: "owner_required_before_precheck",
            reason: "media_owner_username_not_found",
            media_type: want,
            asset_kind: assetKind,
            shortcode: String(meta.shortcode || target?.code || currentMedia?.code || "")
          }));
          return { media_type: want, asset_kind: assetKind, url, prefix, meta, precheck: { ok: false, error: "owner_missing" }, duplicate: { duplicate_confirmed: false, reason: "owner_missing" }, duplicate_confirmed: false, owner_missing: true, error: "owner_missing" };
        }
        const sideMemoryDuplicate = assetKind === "video" && teDownItemExists(meta, assetKind);
        // v221: Side 메모리 중복만으로 완료 처리하지 않는다.
        // 기존 파일의 JSON/ECharts 보고서 재생성 결과를 받아야 하므로 WS ig_media_exists를 항상 호출한다.
        const pre = await teMediaPrecheckAiwk({
          url,
          prefix,
          account: teDownTxtAccount(),
          cycle: Object.assign({}, cycleBase, { cycle_stage: want === "jpg" ? "S20_JPG_DUPLICATE_PRECHECK" : "S07_DUPLICATE_PRECHECK" }),
          meta: Object.assign({}, meta, { asset_kind: assetKind, file_name: prefix, download_prefix: prefix, source_url: url })
        });
        const dup = teAutoPyDuplicateDecision(pre, assetKind, String(collectMeta.shortcode || target.code || (currentMedia && currentMedia.code) || ""));
        if (pre && pre.local && pre.local.item) teRecordDownloadedItem(pre.local.item);
        else if (pre && pre.item) teRecordDownloadedItem(pre.item);
        return { media_type: want, asset_kind: assetKind, url, prefix, meta, precheck: pre, duplicate: dup, duplicate_confirmed: !!(dup && dup.duplicate_confirmed), side_memory_duplicate: !!sideMemoryDuplicate };
      }
      function teAutoPyDirectReportResultFromResponse(res) {
        const r = res && typeof res === "object" ? res : {};
        const direct = r.result && typeof r.result === "object" ? r.result : {};
        const directFinalize = direct.finalize && typeof direct.finalize === "object" ? direct.finalize : {};
        const notify = r.notify_aiwk && typeof r.notify_aiwk === "object"
          ? r.notify_aiwk
          : (Object.keys(directFinalize).length ? directFinalize : {});
        const exists = r.exists && typeof r.exists === "object" ? r.exists : {};
        const refresh = exists.metadata_refresh && typeof exists.metadata_refresh === "object" ? exists.metadata_refresh : {};
        const duplicateReport = refresh.report && typeof refresh.report === "object" ? refresh.report : {};
        const alreadyDone = r.status === "already_done";
        const jsonPath = String(notify.json_path || r.json_path || directFinalize.json_path || refresh.json_path || "");
        const reportOk = alreadyDone ? !!duplicateReport.ok : !!(notify.report_ok || r.report_ok || directFinalize.report_ok);
        const reportPath = String((alreadyDone ? duplicateReport.report_path : (notify.report_path || r.report_path || directFinalize.report_path)) || "");
        const reportError = String((alreadyDone ? duplicateReport.error : (notify.report_error || r.report_error || directFinalize.report_error)) || "");
        const notifyOk = alreadyDone
          ? !!(r.ok && jsonPath)
          : !!(notify.ok || r.storage_ok || direct.finalize_ok || directFinalize.storage_ok);
        const fileSaved = alreadyDone ? !!r.file_path : !!(r.file_saved || r.file_path || direct.file_path);
        return {
          ok: !!(fileSaved && notifyOk && jsonPath && reportOk && reportPath),
          already_done: alreadyDone,
          file_saved: fileSaved,
          json_saved: !!jsonPath,
          json_path: jsonPath,
          report_saved: !!(reportOk && reportPath),
          report_ok: reportOk,
          report_path: reportPath,
          report_error: reportError,
          notify_ok: notifyOk,
          notify: notify,
          response: r
        };
      }
      async function teAutoPyDirectDownloadAsset(mediaType, currentMedia, collectMeta, cycleBase, extraMeta = {}) {
        const want = teAutoPyMediaNorm(mediaType);
        const meta = Object.assign({}, teAutoPyCollectMetaForMedia(collectMeta, currentMedia, want), extraMeta || {}, {
          auto_direct_download: true,
          direct_finalize_required: true
        });
        const url = teAutoPySourceUrlForMedia(currentMedia, collectMeta, want);
        const prefix = teAutoPyPrefixForMedia(collectMeta, want);
        const accountFolderMerge = (typeof meta.account_folder_merge === "boolean") ? meta.account_folder_merge : teDownloadAccountFolderMerge();
        const account = accountFolderMerge ? "" : teSafePath(meta.owner_username || teDownTxtAccount() || teActiveFolderAccountName() || "instagram");
        const stagePrefix = want === "jpg" ? "S2" : "S1";
        const base = Object.assign({}, cycleBase || {}, {
          media_type: want,
          asset_kind: meta.asset_kind,
          shortcode: String(meta.shortcode || ""),
          account,
          account_folder_merge: accountFolderMerge,
          file_name: prefix
        });
        if (!url) {
          await teAutoPyCycle(stagePrefix + "0_DIRECT_URL_MISSING", Object.assign({}, base, { decision: "download_blocked", action: "require_direct_media_url", reason: "direct_media_url_missing" }));
          return { ok: false, error: "direct_media_url_missing", error_code: "direct_media_url_missing", file_saved: false, json_saved: false, report_saved: false };
        }
        if (!prefix || meta.owner_missing || !teCleanOwnerAccountName(meta.owner_username)) {
          await teAutoPyCycle(stagePrefix + "0_DIRECT_OWNER_MISSING", Object.assign({}, base, { decision: "download_blocked", action: "require_owner_before_direct_download", reason: "media_owner_username_not_found" }));
          return { ok: false, error: "owner_missing", error_code: "owner_missing", file_saved: false, json_saved: false, report_saved: false };
        }
        teUpdateDownloadFolderPath(account);
        await teAutoPyCycle(stagePrefix + "0_DIRECT_DOWNLOAD_BEGIN", Object.assign({}, base, {
          decision: "direct_download_begin",
          action: "ws8771_direct_download",
          reason: "auto_download_via_ws8771_direct_url",
          source_url_present: true,
          source_url: String(url).slice(0, 260)
        }));
        const startedAt = Date.now();
        const response = await teDownloadOnce(url, prefix, () => new Promise((resolve) => {
          try {
            chrome.runtime.sendMessage({
              type: "IGBLUE_DOWNLOAD_URL",
              url,
              prefix,
              root: tePersistDownloadRootNow(),
              account,
              account_folder_merge: accountFolderMerge,
              notify_aiwk: true,
              auto_direct_download: true,
              href: teCurrentTargetUrl(),
              meta: tePrepareDownloadFlowMeta(Object.assign({ href: teCurrentTargetUrl(), page_url: teCurrentTargetUrl(), account_folder_merge: accountFolderMerge }, meta), { source_mode: "TOP_SORTED", client_batch_key: String(base.cycle_id || teDownloadClientId("top-batch")), batch_requested_count: 1 })
            }, (res) => {
              if (chrome.runtime.lastError) return resolve({ status: "error", message: chrome.runtime.lastError.message, error_code: "runtime_send_failed" });
              resolve(res || { status: "error", message: "empty_download_response", error_code: "empty_download_response" });
            });
          } catch (err) {
            resolve({ status: "error", message: String(err && err.message || err), error_code: "direct_download_exception" });
          }
        }));
        const result = teAutoPyDirectReportResultFromResponse(response);
        result.media_type = want;
        result.asset_kind = meta.asset_kind;
        result.file_name = prefix;
        result.file_path = String(response && (response.filename || response.file_path) || "");
        result.elapsed_ms = Date.now() - startedAt;
        result.error = result.ok ? "" : String(result.report_error || response && (response.error_code || response.message) || (result.notify_ok ? "direct_finalize_incomplete" : "direct_finalize_failed"));
        result.error_code = result.ok ? "" : (result.file_saved ? "direct_finalize_failed" : "direct_download_failed");
        if (result.file_path) teRecordDownloadedFile(result.file_path, result.notify && result.notify.item);
        await teAutoPyCycle(stagePrefix + "4_DIRECT_FINALIZE_RESULT", Object.assign({}, base, {
          decision: result.ok ? "direct_asset_complete" : "direct_asset_failed",
          action: "file_json_report_finalize_result",
          reason: result.ok ? "file_json_report_all_saved" : result.error_code,
          file_path: result.file_path,
          file_saved: result.file_saved,
          json_saved: result.json_saved,
          json_path: result.json_path,
          report_saved: result.report_saved,
          report_path: result.report_path,
          report_error: result.report_error,
          notify_ok: result.notify_ok,
          elapsed_ms: result.elapsed_ms
        }));
        return result;
      }
      async function teAutoPyDownloadMediaPairDirect(currentMedia, target, rankNo, cycleBase) {
        const collectMeta = teBuildAutoPyCollectMeta(currentMedia, target || { rank: rankNo, code: currentMedia && (currentMedia.code || currentMedia.shortcode) || "" });
        const results = [];
        for (const kind of ["mp4", "jpg"]) {
          const pre = await teAutoPyPrecheckMedia(kind, currentMedia, target || {}, collectMeta, cycleBase || {});
          if (pre && pre.owner_missing) return { ok: false, error: "owner_missing", error_code: "owner_missing", results };
          if (pre && pre.duplicate_confirmed) {
            const dupRaw = pre.precheck && typeof pre.precheck === "object" ? pre.precheck : {};
            const dupReportOk = !!(dupRaw.report_ok && dupRaw.report_path);
            const duplicateResult = { media_type: kind, ok: dupReportOk, skipped: true, duplicate: true, precheck: pre, json_saved: !!dupRaw.json_path, json_path: String(dupRaw.json_path || ""), report_saved: dupReportOk, report_path: String(dupRaw.report_path || ""), report_error: String(dupRaw.report_error || "") };
            results.push(duplicateResult);
            await teAutoPyCycle(kind === "jpg" ? "S24_DIRECT_DUPLICATE_SKIP" : "S14_DIRECT_DUPLICATE_SKIP", Object.assign({}, cycleBase || {}, { decision: dupReportOk ? "duplicate_report_ready" : "duplicate_report_failed", action: "skip_direct_download_after_metadata_report_refresh", reason: dupReportOk ? (pre.duplicate && pre.duplicate.reason || "duplicate_confirmed") : "duplicate_report_finalize_failed", media_type: kind, duplicate: pre.duplicate || null, json_path: duplicateResult.json_path, report_path: duplicateResult.report_path, report_saved: duplicateResult.report_saved, report_error: duplicateResult.report_error }));
            if (!dupReportOk) return { ok: false, error: "duplicate_report_finalize_failed", error_code: "direct_finalize_failed", results, collect_meta: collectMeta };
            continue;
          }
          if (!pre || !pre.url) {
            results.push({ media_type: kind, ok: true, skipped: true, source_missing: true });
            await teAutoPyCycle(kind === "jpg" ? "S24_DIRECT_SOURCE_SKIP" : "S14_DIRECT_SOURCE_SKIP", Object.assign({}, cycleBase || {}, { decision: "source_missing_skip", action: "skip_unavailable_asset", reason: "direct_media_url_missing", media_type: kind }));
            continue;
          }
          const asset = await teAutoPyDirectDownloadAsset(kind, currentMedia, collectMeta, cycleBase || {}, { requested_rank_no: Number(rankNo || 0) || 0 });
          results.push(asset);
          if (!(asset && asset.ok)) return { ok: false, error: asset && asset.error || "direct_download_failed", error_code: asset && asset.error_code || "direct_download_failed", results, collect_meta: collectMeta };
          teAutoPyStatsRecordAsset(kind, asset.elapsed_ms || 0);
        }
        const completed = results.filter((x) => x && x.ok && !x.skipped);
        const acceptable = results.length > 0 && results.every((x) => x && x.ok);
        return { ok: acceptable, direct_download_done: completed.length > 0, results, collect_meta: collectMeta, shortcode: String(collectMeta.shortcode || "") };
      }
      async function teAutoPyStopCycle(stage, base, code, detail, extra) {
        const msg = teAutoPyStopReasonKo(code, detail);
        const payload = Object.assign({}, base || {}, extra || {}, {
          decision: "auto_stopped",
          action: "stop",
          reason: code || "unknown_stop",
          stop_reason: code || "unknown_stop",
          stop_reason_ko: msg,
          autoplay_stop_reason: code || "unknown_stop",
          autoplay_stop_reason_ko: msg
        });
        await teAutoPyCycle(stage || "S99_AUTOPLAY_STOPPED", payload);
        try { await teSendToTargetTabResponse({ type: "igblue_py_state", value: { state: "auto_stopped", message: msg, stop_reason: code || "unknown_stop", stop_reason_ko: msg, cycle: payload } }, "", 900); } catch {}
        n("자동 중단: " + msg);
        return payload;
      }
      function teAutoPyCurrentGeometrySignature() {
        try { return String(teWindowMoveState && teWindowMoveState.geometry_signature || ""); } catch (_) { return ""; }
      }
      async function teAutoPyStartOnceFromUi() {
        if (teAutoPyRunning) return void n("자동시작: 이미 실행중");
        const seq = ++teAutoPyRunSeq;
        teAutoPyFeatureCheckReset(seq);
        await teAutoPyFeatureCheck("AD-01", "[체크시작]", "자동 시작 상태 확인", { cycle_id: `run_${seq}` });
        if (!teAutoPyEnabled()) {
          await teAutoPyFeatureCheck("AD-01", "[체크실패]", "자동 시작 체크가 꺼져 있음 / 다음점검=Side 자동 시작 체크", { cycle_id: `run_${seq}`, error_code: "auto_py_checkbox_off" });
          return void n("자동시작: 체크 필요");
        }
        teAutoPyRunning = true;
        await teAutoPyFeatureCheck("AD-01", "[체크완료]", "자동 시작 활성화", { cycle_id: `run_${seq}` });
        teAutoPyStopping = false;
        teTouchTabSession(true, "auto_start");
        teAutoPyUpdateButtons(teAutoPyLastPyOk || true);
        try {
          const accountPrep = await teAutoPyPrepareAccountQueueForStart(seq);
          if (!(accountPrep && accountPrep.ok) || seq !== teAutoPyRunSeq) {
            await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { cycle_id: teBuildCycleId("ACCOUNT_PREP_FAIL"), run_seq: seq, account_queue_result: accountPrep || null }, "account_prepare_failed", String(accountPrep && accountPrep.error || "account_prepare_failed"));
            return;
          }
          teProgressResetForContext && teProgressResetForContext("auto_start_new_run");
          try { await teLoadDownTxtAiwk(); } catch (_) { try { teLoadDownTxtLocal(teDownTxtAccount()); } catch (__) {} }
          const startPlan = teAutoPyResolveStartNo();
          const targetNo = Number(startPlan && startPlan.no || 0) || 0;
          const filterLoadNo = targetNo > 0 ? targetNo : (teAutoPyAnyFilterActive() ? 1 : 0);
          teAutoPyStatsStart(seq, targetNo || 0, 0, "auto_start_clicked");
          teAutoPyAnnounce("자동다운로드 준비: OS 마우스·원점찾기 없이 Chrome 직접 다운로드를 시작합니다.");
          let loadPlan = null;
          if (filterLoadNo > 0) {
            loadPlan = await teAutoPyEnsureDownloadPointLoaded(filterLoadNo, seq, startPlan && startPlan.source || (targetNo > 0 ? "auto_start" : "auto_start_filter_scan"));
            if (!(loadPlan && loadPlan.ok) || seq !== teAutoPyRunSeq) {
              const loadErr = String(loadPlan && loadPlan.error || "download_point_not_loaded");
              if (loadErr === "current_url_end_reached") {
                const filterEndEmpty = !!(loadPlan && loadPlan.filter_active && loadPlan.filter_result_empty);
                const adv = filterEndEmpty
                  ? await teAutoPySkipCurrentAccountAndAdvanceIfAny(seq, "filter_result_empty_page_end", "필터 결과 0개이며 현재 URL 끝까지 확인됨: 다음 URL 이동", { filter_active: true, filter_result_empty: true, load_result: loadPlan || null })
                  : await teAutoPyAdvanceAccountQueueIfAny(seq, "current_account_end_reached_direct_download");
                if (adv && adv.advanced) return;
                if (adv && adv.completed_all) return;
              }
              await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { cycle_id: teBuildCycleId("LOAD_POINT_FAIL"), run_seq: seq, rank_no: filterLoadNo, rank_label: teAutoPyNoLabel(filterLoadNo), load_result: loadPlan || null }, loadErr === "current_url_end_reached" ? "current_url_end_reached" : "download_point_not_loaded", String(loadPlan && loadPlan.error || "download_point_load_failed"));
              return;
            }
          }
          const targetLimit = Math.max(1, Number(teAutoPyTargetCount && teAutoPyTargetCount() || 0) || 1);
          const requested = Math.max(1, targetLimit, targetNo || 0);
          const exhaustedEmpty = !!(loadPlan && loadPlan.reason === "date_filter_range_exhausted_empty" && loadPlan.filter_result_empty);
          if (exhaustedEmpty) {
            teAutoPyAnnounce("필터 결과 0개입니다.", "현재 URL을 종료하고 다음 URL을 확인합니다.");
            return await teAutoPyClickFirstTarget([], seq, Object.assign({}, startPlan || {}, { filter_result_empty: true, filter_empty_reason: "date_filter_range_exhausted_empty" }));
          }
          const finalPlan = teAutoPyBuildDynamicRankPlan(Math.max(1, requested));
          const filterActive = !!(finalPlan && finalPlan.filter_active || loadPlan && loadPlan.filter_active);
          const rawList = filterActive
            ? (Array.isArray(finalPlan && finalPlan.list) ? finalPlan.list : Array.isArray(loadPlan && loadPlan.list) ? loadPlan.list : [])
            : (finalPlan && finalPlan.list && finalPlan.list.length ? finalPlan.list : (loadPlan && loadPlan.list && loadPlan.list.length ? loadPlan.list : teSortedAllTopN(Math.max(1, requested))));
          const list = rawList.slice(0, targetLimit);
          if (!list.length) {
            await teAutoPyFeatureCheck("AD-02", "[체크실패]", "조건에 맞는 대상 게시물이 없음 / 다음점검=정렬·필터·Top 조건", { cycle_id: `run_${seq}`, candidate_count: 0, target_count: targetLimit, error_code: "no_matching_media" });
          } else {
            const first = list[0] || {};
            await teAutoPyFeatureCheck("AD-02", "[체크완료]", `대상 게시물 선정 / 후보=${list.length} 첫대상=${String(first.code || first.shortcode || "?")}`, { cycle_id: `run_${seq}`, candidate_count: list.length, target_count: targetLimit, shortcode: String(first.code || first.shortcode || "") });
          }
          teAutoPyAnnounce(`자동다운로드 시작합니다. 목표 Top ${targetLimit} / 후보 ${list.length}개`);
          return await teAutoPyClickFirstTarget(list, seq, Object.assign({}, startPlan || {}, { filter_active: filterActive, filter_result_empty: !!(filterActive && list.length === 0) }));
        } finally {
          if (seq === teAutoPyRunSeq || teAutoPyStopping) {
            const statsStopReason = teAutoPyStopping ? "auto_stopped" : "auto_finished";
            teAutoPyRunning = false;
            teAutoPyStopping = false;
            teAutoPyStatsFinish(statsStopReason);
            teTouchTabSession(false, statsStopReason);
            teAutoPyUpdateButtons(teAutoPyLastPyOk);
          }
        }
      }
      async function teAutoPyClickFirstTarget(list, runSeq, startPlan) {
        const mySeq = Number(runSeq || teAutoPyRunSeq || 0);
        const arr = Array.isArray(list) ? list.filter(Boolean) : [];
        if (!arr.length) {
          const filterEmpty = !!(startPlan && startPlan.filter_result_empty || teAutoPyAnyFilterActive());
          const emptyReason = filterEmpty ? "filter_result_empty" : "no_matching_media";
          const emptyDetail = filterEmpty
            ? "필터 결과 0개: 현재 URL 정상 종료 후 다음 URL 이동"
            : "조건에 맞는 Top media 0개 / empty_list";
          const adv = await teAutoPySkipCurrentAccountAndAdvanceIfAny(mySeq, emptyReason, emptyDetail, {
            top_sorted_count: 0,
            filter_active: filterEmpty,
            filter_result_empty: filterEmpty,
            filter_empty_reason: String(startPlan && startPlan.filter_empty_reason || ""),
            target_count: Number(teAutoPyTargetCount && teAutoPyTargetCount() || 0) || 0
          });
          if (adv && (adv.advanced || adv.completed_all)) return;
          await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", {
            cycle_id: teBuildCycleId("EMPTY"),
            run_seq: mySeq,
            filter_active: filterEmpty,
            filter_result_empty: filterEmpty,
            filter_empty_reason: String(startPlan && startPlan.filter_empty_reason || "")
          }, filterEmpty ? "filter_result_empty" : "target_incomplete", filterEmpty ? "filter_result_empty_no_next_url" : "empty_list");
          return;
        }
        const pyStable = await tePyPingStable("before_auto_snapshot", 4, 900, false);
        const pyOk = !!(pyStable && pyStable.ok);
        if (!pyOk || (mySeq && mySeq !== teAutoPyRunSeq)) {
          await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { cycle_id: teBuildCycleId("PY_OFF"), run_seq: mySeq, ping_result: pyStable || null }, !pyOk ? "py_off" : "run_seq_changed", !pyOk ? String(pyStable && pyStable.reason || "py_ping_failed") : "run_seq_changed_before_labels");
          return;
        }
        const ctx = teBuildRankContext(arr);
        const autoSnapshotIndex = teAutoPyBuildNoSnapshotIndex(arr, ctx, "auto_start");
        try { ctx.auto_snapshot_index = autoSnapshotIndex; } catch (_) {}
        const labels = teBuildMainRankLabels(arr, ctx);
        if (!labels.length) {
          const adv = await teAutoPySkipCurrentAccountAndAdvanceIfAny(mySeq, "no_rank_labels", "조건에 맞는 rank label 없음: 현재 계정 건너뛰고 다음 URL 이동", { top_sorted_count: arr.length, target_count: Number(teAutoPyTargetCount && teAutoPyTargetCount() || 0) || 0 });
          if (adv && (adv.advanced || adv.completed_all)) return;
          await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { cycle_id: teBuildCycleId("NO_LABELS"), run_seq: mySeq }, "target_incomplete", "no_rank_labels");
          return;
        }
        teScheduleMainRankLabels(arr, ctx, "auto_py_snapshot_ready");
        const requestedStartForMain = Math.max(1, Number(startPlan && startPlan.no || 0) || Number(teAutoPyResolveStartNo && teAutoPyResolveStartNo().no || 1) || 1);
        const mainReady = await teAutoPyEnsureMainInjectionReady(labels, requestedStartForMain, mySeq);
        await teAutoPyCycle("S00_MAIN_INJECTION_READY", { cycle_id: teBuildCycleId("MAIN_INJECT"), run_seq: mySeq, requested_rank_no: requestedStartForMain, decision: mainReady && mainReady.ok ? "main_ready" : "main_not_ready", action: "verify_main_right_panel_and_No_labels", reason: mainReady && mainReady.ok ? "main_injection_ready" : String(mainReady && mainReady.error || "main_injection_not_ready"), main_injection_state: mainReady || null });
        if (!(mainReady && mainReady.ok)) {
          await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { cycle_id: teBuildCycleId("MAIN_INJECT_FAIL"), run_seq: mySeq, requested_rank_no: requestedStartForMain, main_injection_state: mainReady || null }, "target_incomplete", String(mainReady && mainReady.error || "main_injection_not_ready"));
          return;
        }
        const maxRank = Math.min(labels.length, arr.length, Number(autoSnapshotIndex && autoSnapshotIndex.max_rank_no || labels.length) || labels.length);
        try { teAutoPyStatsStart(mySeq, Number(startPlan && startPlan.no || 0) || 0, maxRank, "auto_snapshot_ready"); } catch (_) {}
        await teAutoPyCycle("S00_AUTO_SNAPSHOT_READY", {
          cycle_id: teBuildCycleId("AUTO_SNAPSHOT"),
          run_seq: mySeq,
          rank_no: 0,
          rank_label: "SNAPSHOT",
          decision: autoSnapshotIndex && autoSnapshotIndex.count ? "snapshot_ready" : "snapshot_empty",
          action: "build_No000_shortcode_snapshot_index",
          reason: "auto_py_download_order_lock",
          snapshot_id: autoSnapshotIndex && autoSnapshotIndex.snapshot_id || "",
          snapshot_count: autoSnapshotIndex && autoSnapshotIndex.count || 0,
          snapshot_max_rank_no: autoSnapshotIndex && autoSnapshotIndex.max_rank_no || 0,
          sort_field: autoSnapshotIndex && autoSnapshotIndex.sort_field || "",
          threshold: autoSnapshotIndex && autoSnapshotIndex.threshold || 0
        });
        n(`자동시작 연속 준비: 현재 No부터 / 후보 ${labels.length}`);
        await teSleep(1200);

        const resolvedStart = startPlan && Number(startPlan.no || 0) > 0 ? startPlan : teAutoPyResolveStartNo();
        const startNo = Number(resolvedStart && resolvedStart.no || 0) || 0;
        const startSource = String(resolvedStart && resolvedStart.source || "default_current");
        let startNoCacheDownloaded = false;
        if (startNo > 0) {
          const hashBase = { cycle_id: teBuildCycleId("HASH_START"), run_seq: mySeq, rank_no: startNo, rank_label: teAutoPyNoLabel(startNo), max_rank_no: Math.max(maxRank, startNo), requested_rank_no: startNo, hash_start_rank: startNo, hash_start_source: startSource, start_source: startSource, loaded_ui_count: teAutoPyLoadedUiCount(), hash_start_url: teCurrentTargetUrl() || "" };
          teAutoPySetLastRank(startNo, startSource + "_request");
          n(`${teAutoPyNoLabel(startNo)} 로딩 확보 후 현재 No 이동`);
          const preload = await teAutoPyPreloadRequestedNo(startNo, Math.max(maxRank, startNo), hashBase, mySeq, arr, ctx);
          if (!(preload && preload.ok)) {
            await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", Object.assign({}, hashBase, { preload_result: preload || null }), "target_incomplete", String(preload && (preload.error || preload.reason) || "start_no_preload_failed"));
            return;
          }
          startNoCacheDownloaded = !!(preload && preload.cache_download_done);
          await teSleep(350);
        }

        const findMediaForTarget = (target, rankNo) => {
          const snapMedia = teAutoPySnapshotMediaByNo(autoSnapshotIndex, rankNo);
          if (snapMedia) return snapMedia;
          const code = String(target && target.code || "");
          if (code) {
            const entry = autoSnapshotIndex && autoSnapshotIndex.byCode instanceof Map ? autoSnapshotIndex.byCode.get(code) : null;
            if (entry && entry.media) return entry.media;
            const byCode = arr.find((m) => String(m && m.code || "") === code);
            if (byCode) return byCode;
          }
          return teAutoPyFindMediaByRankNo(rankNo, arr, ctx) || arr[Math.max(0, Math.min(arr.length - 1, Number(rankNo || 1) - 1))] || arr[0] || {};
        };

        let startRead = await teAutoPyReadCurrentNoNav(700);
        let rankNo = Number(startRead && startRead.rank_no || 0) || 0;
        if (startNo > 0 && startNo <= maxRank && rankNo && rankNo !== startNo) {
          const hashRetryBase = { cycle_id: teBuildCycleId("HASH_START_RETRY"), run_seq: mySeq, rank_no: startNo, rank_label: teAutoPyNoLabel(startNo), max_rank_no: maxRank, hash_start_rank: startNo, hash_start_source: startSource, start_source: startSource, active_rank_no: rankNo, requested_rank_no: startNo, loaded_ui_count: teAutoPyLoadedUiCount(), hash_start_url: teCurrentTargetUrl() || "" };
          if (startNoCacheDownloaded && rankNo === startNo + 1 && rankNo <= maxRank) {
            await teAutoPyCycle("S00_CACHE_START_ADVANCE", Object.assign({}, hashRetryBase, {
              decision: "continue_from_next_after_cache_download",
              action: "skip_downloaded_start_rank_without_retry",
              reason: "start_rank_downloaded_from_cache_active_is_exact_next",
              active_rank_no: rankNo,
              requested_rank_no: startNo,
              next_to_rank_no: rankNo,
              current_read: startRead || null
            }));
          } else if (startNoCacheDownloaded && rankNo > startNo + 1 && rankNo <= maxRank) {
            const nextNo = Math.min(maxRank, startNo + 1);
            await teAutoPyCycle("S00_CACHE_START_ACTIVE_AHEAD", Object.assign({}, hashRetryBase, {
              decision: "active_hash_ahead_but_keep_sequential",
              action: "go_to_exact_next_after_cached_start",
              reason: "screen_active_ahead_during_loading_not_download_order",
              active_rank_no: rankNo,
              requested_rank_no: startNo,
              next_to_rank_no: nextNo,
              current_read: startRead || null
            }));
            await teAutoPyGoNoWithConfirm(nextNo, Object.assign({}, hashRetryBase, { next_reason: "cached_start_rank_done_active_ahead_keep_sequential" }), 1400, "S00_CACHE_START_GO_EXACT_NEXT");
            await teSleep(400);
            startRead = await teAutoPyReadCurrentNoNav(700);
            rankNo = Number(startRead && startRead.rank_no || 0) || nextNo;
          } else {
            await teAutoPyCycle("S00_HASH_READ_MISMATCH", Object.assign({}, hashRetryBase, { decision: "start_rank_not_current", action: "retry_dom_go_no", reason: "current_rank_different_from_start_no" }));
            await teAutoPyGoNoWithConfirm(startNo, Object.assign({}, hashRetryBase, { next_reason: "start_No_retry" }), 1700, "S00_HASH_GO_NO_RETRY");
            await teSleep(500);
            startRead = await teAutoPyReadCurrentNoNav(700);
            rankNo = Number(startRead && startRead.rank_no || 0) || 0;
            if (rankNo && rankNo !== startNo) {
              const cacheRes = await teAutoPyTryCacheDownloadForRank(startNo, arr, ctx, hashRetryBase, "start_rank_unconfirmed");
              if (cacheRes && cacheRes.ok) {
                startNoCacheDownloaded = true;
                if (rankNo === startNo + 1 && rankNo <= maxRank) {
                  await teAutoPyCycle("S00_CACHE_START_ADVANCE", Object.assign({}, hashRetryBase, {
                    decision: "continue_from_next_after_cache_download",
                    action: "skip_downloaded_start_rank",
                    reason: "start_rank_downloaded_from_cache_active_is_exact_next",
                    active_rank_no: rankNo,
                    requested_rank_no: startNo,
                    next_to_rank_no: rankNo,
                    current_read: startRead || null,
                    cache_result: cacheRes
                  }));
                } else {
                  const oldActiveRank = rankNo;
                  rankNo = Math.min(maxRank, startNo + 1);
                  await teAutoPyCycle("S00_CACHE_START_NEXT", Object.assign({}, hashRetryBase, {
                    decision: "continue_to_exact_next_after_cache_download",
                    action: "skip_downloaded_start_rank_keep_sequential",
                    reason: oldActiveRank > startNo + 1 ? "active_ahead_but_download_order_must_not_jump" : "start_rank_downloaded_from_cache_no_valid_active_next",
                    active_rank_no: oldActiveRank || 0,
                    requested_rank_no: startNo,
                    next_to_rank_no: rankNo,
                    current_read: startRead || null,
                    cache_result: cacheRes
                  }));
                  await teAutoPyGoNoWithConfirm(rankNo, Object.assign({}, hashRetryBase, { next_reason: "cached_start_rank_done_keep_sequential" }), 1400, "S00_CACHE_START_GO_EXACT_NEXT");
                  await teSleep(400);
                  startRead = await teAutoPyReadCurrentNoNav(700);
                  rankNo = Number(startRead && startRead.rank_no || 0) || rankNo;
                }
              } else {
                await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", Object.assign({}, hashRetryBase, { active_rank_no: rankNo, current_read: startRead || null, cache_result: cacheRes || null }), "target_incomplete", "start_rank_unconfirmed");
                return;
              }
            }
          }
        }
        if (startNoCacheDownloaded && startNo > 0 && startNo < maxRank && rankNo === startNo) {
          const nextNo = startNo + 1;
          const nextBase = { cycle_id: teBuildCycleId("CACHE_START_NEXT"), run_seq: mySeq, rank_no: nextNo, rank_label: teAutoPyNoLabel(nextNo), requested_rank_no: startNo, max_rank_no: maxRank, hash_start_source: startSource, start_source: startSource };
          await teAutoPyCycle("S00_CACHE_START_NEXT", Object.assign({}, nextBase, { decision: "continue_to_next_after_cache_download", action: "go_no_after_cached_start_rank", reason: "start_rank_already_downloaded_from_cache" }));
          const goNext = await teAutoPyGoNoWithConfirm(nextNo, Object.assign({}, nextBase, { next_reason: "cached_start_rank_done" }), 1200, "S00_CACHE_START_GO_NEXT");
          await teSleep(300);
          startRead = await teAutoPyReadCurrentNoNav(500);
          rankNo = Number(startRead && startRead.rank_no || goNext && goNext.after_rank || nextNo) || nextNo;
        }
        if (!rankNo) {
          if (startNoCacheDownloaded && startNo > 0 && startNo < maxRank) {
            rankNo = startNo + 1;
            await teAutoPyCycle("S00_CACHE_START_NEXT", { cycle_id: teBuildCycleId("CACHE_START_NEXT"), run_seq: mySeq, rank_no: rankNo, rank_label: teAutoPyNoLabel(rankNo), requested_rank_no: startNo, max_rank_no: maxRank, decision: "continue_to_next_after_cache_download", action: "recover_missing_current_after_cache_download", reason: "NoNav_Current_missing_but_start_rank_cache_downloaded" });
          } else {
            await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { cycle_id: teBuildCycleId("NO_CURRENT"), run_seq: mySeq }, "target_incomplete", "NoNav_Current_missing");
            return;
          }
        }
        teAutoPySetLastRank(rankNo, "auto_start_current");
        n(`자동시작: ${teAutoPyNoLabel(rankNo)} 현재 항목부터`);
        let loopGuard = 0;
        while (rankNo > 0 && rankNo <= maxRank && loopGuard++ < Math.max(1, maxRank + 8)) {
          if (mySeq && mySeq !== teAutoPyRunSeq) {
            await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { run_seq: mySeq, rank_no: rankNo, rank_label: teAutoPyNoLabel(rankNo), max_rank_no: maxRank }, "stopped_before_next_rank", "run_seq_changed_before_next_rank");
            return;
          }
          if (teAutoPyStopping) {
            await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { run_seq: mySeq, rank_no: rankNo, rank_label: teAutoPyNoLabel(rankNo), max_rank_no: maxRank }, "user_stop", "stop_requested_before_next_rank");
            return;
          }

          const autoCycleStartedAt = Date.now();
          teAutoPyStatsSetRank(rankNo, "cycle_attempt");
          teAutoPySetLastRank(rankNo, "cycle_attempt");
          const cycleId = teBuildCycleId(teAutoPyNoLabel(rankNo));
          const cycleBase = { cycle_id: cycleId, run_seq: mySeq, rank_no: rankNo, rank_label: teAutoPyNoLabel(rankNo), max_rank_no: maxRank, auto_snapshot: teAutoPySnapshotMediaSummary(autoSnapshotIndex, rankNo) };
          await teAutoPyCycle("S01_DOWNLOAD_POINT_CHECK", Object.assign({}, cycleBase, { decision: "check", action: "ensure_rank_loaded_before_cycle", reason: "every_cycle_start_requires_download_point" }));
          const cycleState = await teAutoPyReadLoadedRankState(160, rankNo);
          const cycleSnap = teAutoPyCountsSnapshot(rankNo, cycleState);
          if (!((Number(cycleSnap.top_sorted_count || cycleSnap.ready_count || 0) || 0) >= rankNo)) {
            teAutoPyAnnounce(`${teAutoPyNoLabel(rankNo)} 다운로드 지점 확보 중입니다.`);
            const cyclePreload = await teAutoPyPreloadRequestedNo(rankNo, Math.max(maxRank, rankNo), Object.assign({}, cycleBase, cycleSnap, { start_source: "cycle_download_point_check" }), mySeq, arr, ctx);
            if (cyclePreload && cyclePreload.cache_download_done) {
              const hasNextRank = rankNo < maxRank;
              const nextRank = hasNextRank ? rankNo + 1 : 0;
              await teAutoPyCycle("S01_CACHE_FALLBACK_ADVANCE", Object.assign({}, cycleBase, cycleSnap, {
                decision: hasNextRank ? "cache_download_then_advance" : "cache_download_last_done",
                action: "skip_dom_click_for_requested_rank_after_download_point_fail",
                reason: "requested_rank_downloaded_from_side_cache_after_download_point_not_loaded",
                requested_rank_no: rankNo,
                next_to_rank_no: nextRank,
                next_to_rank_label: nextRank > 0 ? teAutoPyNoLabel(nextRank) : "END",
                cache_result: cyclePreload
              }));
              await teAutoPyCycle("S99_CYCLE_END", Object.assign({}, cycleBase, {
                decision: "cycle_done",
                action: hasNextRank ? "cache_fallback_next_prepared" : "cache_fallback_last_rank_done",
                reason: hasNextRank ? "next_rank_after_cache_fallback" : "last_rank_downloaded_from_side_cache",
                next_to_rank_no: nextRank,
                next_to_rank_label: nextRank > 0 ? teAutoPyNoLabel(nextRank) : "END",
                next_clicked: false,
                cache_result: cyclePreload
              }));
              teAutoPyStatsRecordCycle(autoCycleStartedAt, "cache_fallback_cycle");
              if (!hasNextRank) break;
              rankNo = nextRank;
              continue;
            }
            if (!(cyclePreload && cyclePreload.ok)) {
              await teAutoPyStopCycle("S01_DOWNLOAD_POINT_FAILED", Object.assign({}, cycleBase, { preload_result: cyclePreload || null }), "download_point_not_loaded", String(cyclePreload && (cyclePreload.error || cyclePreload.reason) || "cycle_download_point_not_loaded"));
              return;
            }
          }
          await teAutoPyCycle("S01_CYCLE_START", Object.assign({}, cycleBase, { decision: "start", action: "prepare_active", cycle_log: "new_cycle" }));
          let goRes = { ok: true, action: "current_NoNav", requested_no: rankNo, after_rank: rankNo, changed: false, target_found: true };
          await teAutoPyCycle("S02_CURRENT_NO", Object.assign({}, cycleBase, { decision: "use_current", action: "read_NoNav_Current", next_button_found: true, next_clicked: false, first_current_click: loopGuard === 1, go_response: goRes }));
          if (loopGuard === 1) {
            const currentClickRes = await teAutoPyClickCurrentNoNav(Object.assign({}, cycleBase, { current_reason: "auto_start_first_cycle_activate_current_NoNav", first_current_click: true }), 1300, "S02_CURRENT_NO_CLICK");
            if (currentClickRes && currentClickRes.ok && Number(currentClickRes.after_rank || 0) > 0) {
              rankNo = Number(currentClickRes.after_rank || rankNo) || rankNo;
            }
          }
          await teSleep(loopGuard === 1 ? 1100 : 900);
          if (mySeq && mySeq !== teAutoPyRunSeq) {
            await teAutoPyStopCycle("S03_AUTOPLAY_STOPPED", cycleBase, "stopped_before_target_read", "run_seq_changed_before_target_read");
            return;
          }

          let got = await teSendToTargetTabResponse({ type: "igblue_get_active_download_target" }, "Instagram tab not found", 1800);
          let target = got && got.target;
          let currentMedia = findMediaForTarget(target, rankNo) || {};
          const expectedCodeForRank = String(currentMedia && (currentMedia.__igblue_auto_snapshot_shortcode || currentMedia.code || currentMedia.shortcode) || "");
          const targetCodeNow = String(target && (target.code || target.shortcode) || "");
          const rankOkNow = Number(target && target.rank || 0) === rankNo && !!targetCodeNow && !!expectedCodeForRank && targetCodeNow === expectedCodeForRank;
          if (!teAutoPySafeTarget(target) || !rankOkNow) {
            await teSleep(2000);
            got = await teSendToTargetTabResponse({ type: "igblue_get_active_download_target" }, "", 1800);
            target = got && got.target;
            currentMedia = findMediaForTarget(target, rankNo) || {};
          }
          let targetCheck = teAutoPyTargetReadCheck(target, currentMedia, rankNo);
          await teAutoPyCycle("S04_TARGET_READ", Object.assign({}, cycleBase, targetCheck, {
            decision: targetCheck.ok ? "target_ok" : "target_incomplete",
            action: targetCheck.ok ? "read_active_target" : "retry_read",
            reason: targetCheck.reason,
            active_rank_no: targetCheck.active_rank_no,
            requested_rank_no: targetCheck.requested_rank_no,
            active_shortcode: targetCheck.active_shortcode,
            expected_shortcode: targetCheck.expected_shortcode,
            active_target_age_ms: targetCheck.active_target_age_ms,
            shortcode: targetCheck.shortcode,
            asset_kind: targetCheck.asset_kind,
            direct_media_url_present: targetCheck.target_D_direct_media_url,
            go_response: got || null
          }));
          if (!targetCheck.ok) {
            await teAutoPyFeatureCheck("AD-03", "[체크실패]", `게시물 계정·shortcode·원본 URL 확인 실패 / 다음점검=${String(targetCheck.target_missing_fields || targetCheck.reason || "target_incomplete")}`, Object.assign({}, cycleBase, { shortcode: String(targetCheck.shortcode || targetCheck.expected_shortcode || ""), error_code: String(targetCheck.stop_reason || "target_incomplete") }));
            const advRank = Number(targetCheck.active_rank_no || 0) || 0;
            const advMedia = advRank > 0 ? (teAutoPySnapshotMediaByNo(autoSnapshotIndex, advRank) || teAutoPyFindMediaByRankNo(advRank, arr, ctx)) : null;
            const advCode = String(advMedia && (advMedia.__igblue_auto_snapshot_shortcode || advMedia.code || advMedia.shortcode) || "");
            const cacheRes = await teAutoPyTryCacheDownloadForRank(rankNo, arr, ctx, Object.assign({}, cycleBase, targetCheck, { active_rank_no: advRank || targetCheck.active_rank_no || 0, active_shortcode: targetCheck.active_shortcode || "" }), targetCheck.target_missing_fields || targetCheck.reason || "target_incomplete");
            if (cacheRes && cacheRes.ok) {
              const hasNextRank = rankNo < maxRank;
              const activeIsExactNext = !!(hasNextRank && advRank === rankNo + 1 && advRank <= maxRank && advCode && targetCheck.active_shortcode === advCode);
              const activeIsFarAhead = !!(hasNextRank && advRank > rankNo + 1 && advRank <= maxRank);
              const nextRank = hasNextRank ? (activeIsExactNext ? advRank : Math.min(maxRank, rankNo + 1)) : 0;
              await teAutoPyCycle("S04_CACHE_FALLBACK_ADVANCE", Object.assign({}, cycleBase, targetCheck, {
                decision: hasNextRank ? "cache_download_then_advance" : "cache_download_last_done",
                action: hasNextRank ? (activeIsFarAhead ? "skip_dom_click_keep_sequential_next" : "skip_dom_click_for_requested_rank") : "finish_current_account_after_last_cache_download",
                reason: hasNextRank ? (activeIsFarAhead ? "active_hash_ahead_but_download_order_must_not_jump" : "requested_rank_downloaded_from_ext_cache") : "requested_last_rank_downloaded_from_ext_cache",
                active_rank_no: advRank || targetCheck.active_rank_no || 0,
                requested_rank_no: rankNo,
                next_to_rank_no: nextRank,
                next_to_rank_label: nextRank > 0 ? teAutoPyNoLabel(nextRank) : "END",
                cache_result: cacheRes
              }));
              teAutoPyStatsRecordCycle(autoCycleStartedAt, "cache_fallback_cycle");
              if (!hasNextRank) break;
              rankNo = nextRank;
              continue;
            }
            if (targetCheck.target_missing_fields === "rank_mismatch" && advRank === rankNo + 1 && advRank <= maxRank && advCode && targetCheck.active_shortcode === advCode) {
              await teAutoPyCycle("S04_TARGET_ALREADY_ADVANCED", Object.assign({}, cycleBase, targetCheck, {
                decision: "already_advanced",
                action: "skip_to_active_rank",
                reason: "active_rank_shortcode_matches_later_rank_cache_miss",
                next_requested: true,
                next_clicked: true,
                next_to_rank_no: advRank,
                next_to_rank_label: teAutoPyNoLabel(advRank),
                cache_result: cacheRes || null
              }));
              teAutoPyStatsRecordCycle(autoCycleStartedAt, "already_advanced_cycle");
              rankNo = advRank;
              continue;
            }
            await teAutoPyStopCycle("S04_AUTOPLAY_STOPPED", Object.assign({}, cycleBase, targetCheck, { shortcode: targetCheck.shortcode, asset_kind: targetCheck.asset_kind, cache_result: cacheRes || null }), targetCheck.stop_reason, targetCheck.target_missing_fields, { active_rank_no: targetCheck.active_rank_no, requested_rank_no: targetCheck.requested_rank_no, active_shortcode: targetCheck.active_shortcode, expected_shortcode: targetCheck.expected_shortcode, active_target_age_ms: targetCheck.active_target_age_ms });
            return;
          }
          if (targetCheck.shortcode && !String(target.code || "").trim()) target.code = targetCheck.shortcode;
          if (targetCheck.rank_stale_confirmed && Number(target.rank || 0) !== rankNo) target.rank = rankNo;

          await teAutoPyFeatureCheck("AD-03", "[체크완료]", `게시물 정보 확인 / ${teAutoPyNoLabel(rankNo)} 계정=${String(teMediaOwnerName(currentMedia) || target && target.username || "?")} shortcode=${String(targetCheck.shortcode || "?")}`, Object.assign({}, cycleBase, { shortcode: String(targetCheck.shortcode || ""), owner_username: String(teMediaOwnerName(currentMedia) || target && target.username || ""), direct_video_url_present: !!targetCheck.direct_video_url_present, direct_image_url_present: !!targetCheck.direct_image_url_present }));
          await teAutoPyCycle("S05_DIRECT_DOWNLOAD_READY", Object.assign({}, cycleBase, {
            decision: "direct_download_ready",
            action: "direct_media_url_ready",
            reason: "media_url_and_rank_shortcode_confirmed",
              direct_video_url_present: !!targetCheck.direct_video_url_present,
            direct_image_url_present: !!targetCheck.direct_image_url_present
          }));

          const directPair = await teAutoPyDownloadMediaPairDirect(currentMedia, target, rankNo, cycleBase);
          if (!(directPair && directPair.ok)) {
            const failReason = String(directPair && (directPair.error_code || directPair.error) || "direct_download_failed");
            await teAutoPyFeatureCheck("AD-06", "[체크실패]", `파일 저장·후처리 실패 / 원인=${failReason}`, Object.assign({}, cycleBase, { error_code: failReason, next_check: "WS8771 직접 다운로드 결과" }));
            n("직접 다운로드 실패: " + failReason);
            await teAutoPyMediaFailAdvanceOrStop(mySeq, "S24_DIRECT_AUTOPLAY_STOPPED", Object.assign({}, cycleBase, {
              result_ok: false,
              direct_download_result: directPair || null,
              file_saved: !!(directPair && directPair.results && directPair.results.some((x) => x && x.file_saved)),
              json_saved: !!(directPair && directPair.results && directPair.results.every((x) => !x || x.skipped || x.json_saved)),
              report_saved: !!(directPair && directPair.results && directPair.results.every((x) => !x || x.skipped || x.report_saved))
            }), failReason === "direct_finalize_failed" ? "direct_finalize_failed" : "direct_download_failed", failReason);
            return;
          }
          const directSummary = directPair.results || [];
          await teAutoPyFeatureCheck("AD-06", "[체크완료]", `파일 저장·JSON·보고서 완료 / 자산=${directSummary.length}`, Object.assign({}, cycleBase, { direct_results_count: directSummary.length, shortcode: String(directPair.shortcode || "") }));
          await teAutoPyFeatureCheck("AD-07", "[체크완료]", "현재 게시물 완료 / 다음 게시물 진행 가능", Object.assign({}, cycleBase, { next_rank_no: rankNo < maxRank ? rankNo + 1 : 0 }));
          await teAutoPyCycle("S30_MEDIA_PAIR_COMPLETE", Object.assign({}, cycleBase, {
            decision: "media_pair_direct_complete",
            action: "mp4_then_jpg_file_json_report_complete",
            result_ok: true,
              direct_results: directSummary
          }));
          const delaySec = Math.max(2, Number(teDownloadDelaySec && teDownloadDelaySec() || 0) || 0);
          await teAutoPyCycle("S31_NEXT_REQUIRED", Object.assign({}, cycleBase, { decision: "next_required", action: "wait_before_next_after_finalize", next_required: true, wait_sec: delaySec, reason: "file_json_report_complete_then_next" }));
          n(`자동시작: JSON·보고서 완료 후 Next 전 ${delaySec}s 대기`);
          await teSleep(delaySec * 1000);
          const nextRes = await teAutoPyClickNextNoNav(Object.assign({}, cycleBase, { next_reason: "after_direct_media_pair_finalize", direct_download_result: directPair }), 1300, "S31_NEXT_DOM_CLICK");
          await teAutoPyCycle("S99_CYCLE_END", Object.assign({}, cycleBase, { decision: "cycle_done", action: "next_prepared", next_required: true, next_to_rank_no: Number(nextRes && nextRes.after_rank || 0) || 0, next_clicked: !!(nextRes && nextRes.ok && nextRes.changed), next_response: nextRes || null }));
          teAutoPyStatsRecordCycle(autoCycleStartedAt, "normal_cycle_done");
          if (!(nextRes && nextRes.ok && nextRes.changed && Number(nextRes.after_rank || 0) > 0)) {
            if (rankNo < maxRank) {
              const retryNo = rankNo + 1;
              await teAutoPyCycle("S13_NEXT_RETRY_GO_NO", Object.assign({}, cycleBase, { decision: "retry_go_exact_next", action: "go_no_after_next_no_change", reason: "next_click_no_change_but_target_remaining", next_to_rank_no: retryNo, next_response: nextRes || null, target_count: maxRank, completed_count: rankNo }));
              const goRes = await teAutoPyGoNoWithConfirm(retryNo, Object.assign({}, cycleBase, { next_reason: "next_click_no_change_go_exact_next" }), 1700, "S13_NEXT_GO_EXACT_NEXT");
              if (goRes && goRes.ok && Number(goRes.after_rank || retryNo) === retryNo) { rankNo = retryNo; teAutoPySetLastRank(rankNo, "next_exact_retry"); await teSleep(600); continue; }
              await teAutoPyStopCycle("S13_NEXT_UNREACHABLE", Object.assign({}, cycleBase, { next_response: nextRes || null, go_no_response: goRes || null, target_count: maxRank, completed_count: rankNo, next_to_rank_no: retryNo }), "next_rank_unreachable", "No변화 없음: 다음 목표가 남아 있어 종료하지 않고 중단");
              return;
            }
            break;
          }
          rankNo = Number(nextRes.after_rank || 0) || 0;
          teAutoPySetLastRank(rankNo, "next_prepared");
          await teSleep(600);
        }
        const accountAdvance = await teAutoPyAdvanceAccountQueueIfAny(mySeq, "current_account_target_done");
        if (accountAdvance && accountAdvance.advanced) return { ok: true, next_account_scheduled: true };
        if (accountAdvance && accountAdvance.completed_all) return { ok: true, account_queue_completed: true };
        n(`자동시작 연속 완료: 현재 목록 처리 종료`);
        return { ok: true, reason: "current_account_done" };
      }
      function teDownTxtPayload(account) {
        const acc = teSafePath(account || teDownTxtAccount());
        const href = teCurrentTargetUrl() || "https://www.instagram.com/";
        const root = tePersistDownloadRootNow();
        const merged = teDownloadAccountFolderMerge();
        return { host: teDownTxtHost(), href, root, account: merged ? "" : acc, account_folder_merge: merged, filename: merged ? (root + ".json") : (acc.split("/").pop() + ".json") };
      }
      async function teSaveDownTxtAiwk(account) {
        const payload = Object.assign(teDownTxtPayload(account), { items: teDownItems.slice(0) });
        const res = teAiwkTrackWsResult(await teAiwkWsRequest("domain_folder_json_save_hint", payload, 2500));
        teDownTxtStatus(res && res.ok ? "저장 기준 확인" : ("저장 실패 " + ((res && (res.error_code || res.error)) || "")));
        return !!(res && res.ok);
      }
      async function teLoadDownTxtAiwk() {
        const account = teDownTxtAccount();
        teDownTxtStatus("loading from yjm_win2rtc...");
        const payload = teDownTxtPayload(account);
        let res = teAiwkTrackWsResult(await teAiwkWsRequest("domain_folder_json_read", payload));
        if (res && res.ok) {
          teRenderDownList(res.items || [], account);
          teDownTxtStatus("loaded " + (Number(res.count || (res.items && res.items.length) || 0)) + " items");
        } else {
          res = teAiwkTrackWsResult(await teAiwkWsRequest("domain_down_txt_read", payload));
          if (res && res.ok) {
            teSetDownTxtText(res.text || "", account);
            teDownTxtStatus("legacy loaded");
          } else {
            teLoadDownTxtLocal(account);
            teDownTxtStatus("load failed; local shown " + ((res && (res.error_code || res.error)) || ""));
          }
        }
        try {
          const startRes = teAiwkTrackWsResult(await teAiwkWsRequest("domain_auto_start", payload, 2500));
          if (startRes && startRes.ok) teDownTxtStatus("loaded + auto_start");
        } catch {}
      }
      function teFilenameOnly(path) {
        return String(path || "").split(/[\\/]/).pop().trim();
      }
      function teNum(v) {
        const n = Number(v);
        return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
      }
      function teMysqlDateFromEpoch(v) {
        const n = Number(v);
        if (!Number.isFinite(n) || n <= 0) return "";
        const d = new Date(n * 1000);
        const p = (x) => String(x).padStart(2, "0");
        return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
      }
      function teMediaPageUrl(m, mediaUrl) {
        const code = String(m?.code || "").trim();
        if (!code) return String(mediaUrl || teCurrentTargetUrl() || "");
        const kind = teIsVideoUrl(mediaUrl) || Number(m?.media_type) === 2 ? "reel" : "p";
        return `https://www.instagram.com/${kind}/${encodeURIComponent(code)}/`;
      }
      function teBuildMediaDownloadMeta(m, mediaUrl, prefix, mediaIndex, totalMedia) {
        const views = teNum(m?.play_count || m?.view_count);
        const likes = teNum(m?.like_count);
        const comments = teNum(m?.comment_count);
        const reposts = teNum(m?.media_repost_count || m?.share_count);
        const shortcode = String(m?.code || "").trim();
        const ownerName = teOwnerAccountForMedia(m || {});
        const accountName = teSafePath(teActiveFolderAccountName() || teDownloadChannelAccountName() || "instagram");
        const originalInfo = teOriginalInstagramFileInfo(mediaUrl || "");
        const firstLoadStamp = teFirstLoadStamp(ownerName || accountName, shortcode || "no_shortcode", shortcode || "media");
        const firstLoadedAt = teFirstLoadIsoFromStamp(firstLoadStamp);
        const rankNo = teRankNoFromPrefix(prefix) || teRankNoForMedia(m, 0);
        const rankLabel = rankNo > 0 ? `No${String(rankNo).padStart(3, "0")}` : "";
        const assetKind = teIsVideoUrl(mediaUrl) ? "video" : "image";
        const rawSortField = teTopMetricField() || String(G[Z] || "");
        const sortField = teServerSortField(rawSortField);
        const sortValue = rawSortField ? teMetricNumber(m?.[rawSortField]) : 0;
        const thresholdValue = teMetricThresholdValue();
        const nowIso = new Date().toISOString();
        const filenameStem = teFilenameOnly(prefix || "").replace(/\.[^.]+$/, "");
        const rankTotal = Array.isArray(teMetricSourceList()) ? teMetricSourceList().length : 0;
        const href = teCurrentTargetUrl() || "";
        const accountQueueStatus = (typeof teAutoPyAccountQueueStatus === "function") ? teAutoPyAccountQueueStatus() : null;
        return {
          shortcode,
          asset_kind: assetKind,
          rank_no: rankNo,
          rank_label: rankLabel,
          rank_meaning: "sort_rank_at_collection_time",
          rank_total: rankTotal,
          top_n: rankTotal,
          collected_at: nowIso,
          source_type: teSourceTypeFromUrl(href),
          target_url: href,
          account_queue_index: accountQueueStatus && accountQueueStatus.has ? Number(accountQueueStatus.index || 0) : 0,
          account_queue_total: accountQueueStatus && accountQueueStatus.has ? Number(accountQueueStatus.total || 0) : 0,
          account_queue_url: accountQueueStatus && accountQueueStatus.has ? String(accountQueueStatus.url || "") : "",
          account_queue_username: accountQueueStatus && accountQueueStatus.has ? String(accountQueueStatus.account || "").replace(/^@/, "") : "",
          pass_id: `${IGBLUE_BUILD_TAG}_${teCollectDateStamp()}_${teSourceTypeFromUrl(href)}`,
          threshold_value: thresholdValue,
          threshold_label: teThresholdLabelForMetric(rawSortField, thresholdValue),
          media_url: teMediaPageUrl(m, mediaUrl),
          thumbnail_url: String(m?.img_origin || m?.thumbnail_url || m?.display_url || ""),
          video_url: assetKind === "video" ? String(mediaUrl || m?.video_url || "") : String(m?.video_url || ""),
          instagram_original_filename: originalInfo.original_filename,
          instagram_original_filename_with_ext: originalInfo.original_filename_with_ext,
          instagram_original_ext: originalInfo.original_ext,
          first_loaded_stamp: firstLoadStamp,
          first_loaded_at: firstLoadedAt,
          account_name: accountName,
          owner_username: ownerName,
          owner_missing: ownerName ? false : true,
          upload_at: teMysqlDateFromEpoch(m?.created_at || m?.taken_at || m?.caption?.created_at),
          file_name: teFilenameOnly(prefix || ""),
          filename_stem: filenameStem,
          folder_name: ownerName,
          collection_folder_name: teDownloadRoot(),
          views, likes, comments, reposts,
          sort: {
            field: sortField,
            value: sortValue,
            views,
            likes,
            comments,
            reposts,
            created_at: teNum(m?.created_at || m?.taken_at || m?.caption?.created_at),
            engagement_rate: Number(m?.engagement_rate || 0) || 0,
            captured_at: nowIso
          },
          sort_field: sortField,
          sort_value: sortValue,
          source_url: String(mediaUrl || ""),
          current_url: href,
          download_prefix: String(prefix || ""),
          media_index: Number(mediaIndex) || 0,
          total_media: Number(totalMedia) || 1,
          media_type: m?.media_type || "",
          stable_identity: {
            priority: ["shortcode", "asset_kind", "media_index", "instagram_original_filename", "sha256", "md5", "media_id", "media_url", "filename"],
            shortcode,
            asset_kind: assetKind,
            media_index: Number(mediaIndex) || 0,
            media_id: String(m?.id || m?.pk || ""),
            pk: String(m?.pk || ""),
            instagram_original_filename: originalInfo.original_filename,
            media_url: teMediaPageUrl(m, mediaUrl),
            source_url: String(mediaUrl || "")
          }
        };
      }
      function teCollectDateStamp() {
        // 파일명에는 사용하지 않는다. 수집/조회 메타용 현재 시각만 필요할 때 사용한다.
        const d = new Date(), p = (x) => String(x).padStart(2, "0");
        return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
      }
      function teUploadStampFromMedia(media) {
        const sec = Number(media?.created_at || media?.taken_at || media?.caption?.created_at || 0);
        if (!Number.isFinite(sec) || sec <= 0) return "upload_unknown";
        const d = new Date(sec * 1000), p = (x) => String(x).padStart(2, "0");
        return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
      }
      function teOwnerAccountForMedia(media) {
        return teMediaOwnerName(media || {});
      }
      function teIsSingleMediaPage() {
        try {
          const parts = String(teCurrentTargetUrl() || location.href || "").split("instagram.com/").pop().split(/[?#]/)[0].split("/").filter(Boolean);
          return !!(parts.length >= 2 && ["reel", "reels", "p"].includes(parts[0]));
        } catch (_) { return false; }
      }

      function teAutoPyAckMatches(ack, rankNo, shortcode, sinceMs) {
        try {
          if (!ack || typeof ack !== "object") return false;
          const ts = Number(ack.ts || ack.clicked_at_ms || 0) || 0;
          if (sinceMs && ts && ts < Number(sinceMs || 0)) return false;
          const ar = Number(ack.rank || ack.rank_no || 0) || 0;
          if (rankNo && ar && ar !== Number(rankNo)) return false;
          const ac = String(ack.code || ack.shortcode || "").trim();
          if (shortcode && ac && ac !== String(shortcode)) return false;
          const mt = String(ack.media_type || ack.asset_kind || "").toLowerCase();
          const bt = String(ack.button_text || "").toLowerCase();
          if (mt && !/mp4|video|download|jpg|image/.test(mt) && !bt.includes("download") && !bt.includes("down")) return false;
          return true;
        } catch (_) { return false; }
      }
      function teUseNoInFilename(totalMedia) {
        // v99: No is controlled by page type + actual rank, not by rank_total timing.
        // rank_total can be missing/0 in late main payloads, which caused _No001_ to disappear.
        return !teIsSingleMediaPage();
      }
      function teBuildLocalArchiveStem(media, target) {
        const account = teOwnerAccountForMedia(media || {});
        if (!account) return "";
        const code = teSafeName((target && target.code) || media?.code || media?.shortcode || "media");
        const upload = teUploadStampFromMedia(media || {});
        const snapshotNo = Number(media && media.__igblue_auto_snapshot_no || 0) || 0;
        const rank = Number(target && target.rank || snapshotNo || teRankNoForMedia(media || {}, 0)) || 0;
        const noPart = rank > 0 ? `_No${String(rank).padStart(3, "0")}` : `_No없음`;
        return `${account}_${upload}${noPart}_${code}`;
      }
      function teBuildAutoPyCoordinateProbe(target, collectMeta, opts) {
        const t = target && typeof target === "object" ? target : {};
        const cm = collectMeta && typeof collectMeta === "object" ? collectMeta : {};
        const vp = t.viewport && typeof t.viewport === "object" ? t.viewport : {};
        const br = t.button_rect && typeof t.button_rect === "object" ? t.button_rect : {};
        const tr = t.thumbnail_rect && typeof t.thumbnail_rect === "object" ? t.thumbnail_rect : (t.card_rect || {});
        const cr = t.card_rect && typeof t.card_rect === "object" ? t.card_rect : {};
        return {
          schema: "ig_coordinate_probe_v59",
          purpose: "dom_rect_to_rtc_screen_coordinate_calibration",
          captured_at: new Date().toISOString(),
          rank: Number(t.rank || 0) || 0,
          rank_label: cm.rank_label || (t.rank ? `No${String(Number(t.rank)).padStart(3, "0")}` : ""),
          shortcode: String(t.code || cm.shortcode || ""),
          after_nav_sleep_ms: Number(opts && opts.after_nav_sleep_ms || 0) || 0,
          run_seq: Number(opts && opts.run_seq || 0) || 0,
          href: teCurrentTargetUrl() || "",
          viewport: vp,
          rects: {
            button: br,
            thumbnail: tr,
            card: cr,
            overlay: t.overlay_rect || {},
            automation_download_buttons: Array.isArray(t.automation_button_rects) ? t.automation_button_rects : [],
            visual_download_buttons: Array.isArray(t.visual_button_rects) ? t.visual_button_rects : [],
            all_download_buttons: Array.isArray(t.automation_button_rects) && t.automation_button_rects.length ? t.automation_button_rects : (Array.isArray(t.download_button_rects) ? t.download_button_rects : [])
          },
          expected_click: { x: Number(br.cx || 0) || 0, y: Number(br.cy || 0) || 0 },
          notes: [
            "Next/Prev 뒤 좌표 재사용 금지",
            "DOM rect는 힌트이며 RTC가 window/client/monitor 기준 보정 로그를 남긴다",
            "패턴이 일정하면 DOM+offset, 불안정하면 화면인식으로 전환"
          ]
        };
      }
      function teBuildAutoPyCollectMeta(media, target) {
        const m = media || {};
        const rank = Number(target && target.rank || 0) || 0;
        const sortField = teTopMetricField();
        const sortValue = sortField ? teMetricNumber(m?.[sortField]) : 0;
        const stem = teBuildLocalArchiveStem(m, target || {});
        if (!stem) {
          return {
            schema: "ig_local_collect_v2",
            purpose: "local_duplicate_prevention_and_sort_history",
            owner_missing: true,
            owner_missing_reason: "media_owner_username_not_found",
            shortcode: String(m?.code || ""),
            rank_no: rank,
            rank_label: rank > 0 ? `No${String(rank).padStart(3, "0")}` : "",
            filename_stem: ""
          };
        }
        const base = teBuildMediaDownloadMeta(m, String(m?.video_url || m?.img_origin || m?.img_small || ""), stem, 0, 1);
        return Object.assign(base, {
          schema: "ig_local_collect_v2",
          purpose: "local_duplicate_prevention_and_sort_history",
          collected_at: new Date().toISOString(),
          rank_no: rank,
          rank_label: rank > 0 ? `No${String(rank).padStart(3, "0")}` : "",
          rank_meaning: "sort_rank_at_collection_time",
          filename_stem: stem,
          expected_files: { jpg: `${stem}.jpg`, mp4: `${stem}.mp4`, md5: `${stem}.md5` },
          sort_snapshot: {
            field: teServerSortField(sortField) || "",
            value: sortValue,
            views: teNum(m?.play_count || m?.view_count),
            likes: teNum(m?.like_count),
            comments: teNum(m?.comment_count),
            reposts: teNum(m?.media_repost_count || m?.share_count),
            engagement_rate: Number(m?.engagement_rate || 0) || 0,
            created_at: teNum(m?.created_at || m?.taken_at || m?.caption?.created_at)
          },
          archive_layout: {
            index_json_location: "parent_folder/<final_folder_name>.json",
            sidecar_location: "final_folder/<filename_stem>.md5",
            set_files: ["<filename_stem>.jpg", "<filename_stem>.mp4", "<filename_stem>.md5"]
          }
        });
      }
      function teAutoPyNoLabel(rankNo) {
        return `No${String(Number(rankNo || 0) || 0).padStart(3, "0")}`;
      }
      function teAutoPyPathExt(pathOrName) {
        const m = String(pathOrName || "").toLowerCase().match(/\.([a-z0-9]+)(?:[?#].*)?$/);
        return m ? m[1] : "";
      }
      function teAutoPyKindFromExt(pathOrName) {
        const ext = teAutoPyPathExt(pathOrName);
        if (["mp4", "mov", "webm", "m4v"].includes(ext)) return "video";
        if (["jpg", "jpeg", "png", "webp", "avif"].includes(ext)) return "image";
        return "";
      }
      function teAutoPyFirstPathFromPrecheck(pre) {
        const local = pre && pre.local && typeof pre.local === "object" ? pre.local : {};
        if (pre && pre.file_path) return String(pre.file_path);
        if (local && local.file_path) return String(local.file_path);
        const paths = [];
        if (Array.isArray(pre && pre.file_paths)) paths.push(...pre.file_paths);
        if (Array.isArray(local && local.existing_file_paths)) paths.push(...local.existing_file_paths);
        if (Array.isArray(local && local.file_paths)) paths.push(...local.file_paths);
        return paths.find(Boolean) ? String(paths.find(Boolean)) : "";
      }
      function teAutoPyDuplicateDecision(pre, expectedKind, shortcode) {
        const local = pre && pre.local && typeof pre.local === "object" ? pre.local : {};
        const filePath = teAutoPyFirstPathFromPrecheck(pre);
        const pathKind = teAutoPyKindFromExt(filePath);
        const expected = String(expectedKind || "").toLowerCase();
        const sc = String(shortcode || "").trim();
        const localExists = !!(local && (local.exists === 1 || local.exists === true || (Array.isArray(local.existing_file_paths) && local.existing_file_paths.length)));
        const exists = !!(pre && (pre.exists === 1 || pre.exists === true || localExists));
        const dupFlag = !(pre && pre.duplicate_confirmed === false) && !(local && local.duplicate_confirmed === false);
        const ext_match = !expected || (!!pathKind && pathKind === expected);
        const shortcode_match = !sc || !filePath || String(filePath).includes(sc);
        const confirmed = !!(pre && pre.ok && exists && localExists && dupFlag && ext_match && shortcode_match);
        return {
          duplicate_confirmed: confirmed,
          exists,
          local_exists: localExists,
          server_exists: !!(pre && (pre.server_exists === 1 || pre.server_exists === true)),
          dup_flag: dupFlag,
          expected_kind: expected,
          path_kind: pathKind,
          ext_match,
          shortcode_match,
          file_path: filePath,
          match_by: String((pre && pre.match_by) || (local && local.match_by) || ""),
          reason: confirmed ? "local_asset_ext_shortcode_match" : (!localExists ? "local_file_not_confirmed" : !ext_match ? "asset_ext_mismatch" : !shortcode_match ? "shortcode_path_mismatch" : !dupFlag ? "duplicate_flag_false" : "not_confirmed")
        };
      }
      let teAutoPyCycleLogLines = [];
      let teAutoPyCycleLogId = "";
      function teAutoPyCycleLogReset(cycleId) {
        teAutoPyCycleLogId = String(cycleId || "");
        teAutoPyCycleLogLines = [];
      }
      function teAutoPyCycleLogPush(stage, info) {
        try {
          const d = info || {};
          if (stage === "S01_CYCLE_START") teAutoPyCycleLogReset(d.cycle_id || "");
          const line = [
            stage || "cycle",
            d.rank_label || (d.rank_no ? teAutoPyNoLabel(d.rank_no) : ""),
            d.decision ? "decision=" + d.decision : "",
            d.action ? "action=" + d.action : "",
            d.reason ? "reason=" + d.reason : "",
            d.active_rank_no != null ? "active=" + d.active_rank_no : "",
            d.active_hash_no != null ? "hash=" + d.active_hash_no : "",
            d.requested_rank_no != null ? "req=" + d.requested_rank_no : "",
            d.loaded_ready_no != null ? "ready=" + d.loaded_ready_no : "",
            d.next_to_rank_no != null ? "next_to=" + d.next_to_rank_no : "",
            d.next_clicked != null ? "next_clicked=" + (!!d.next_clicked) : "",
            d.next_after_rank != null ? "next_after=" + d.next_after_rank : "",
            d.tab_id != null ? "tab_id=" + d.tab_id : "",
            d.active_tab_id != null ? "active_tab_id=" + d.active_tab_id : "",
            d.request_tab_id != null ? "request_tab_id=" + d.request_tab_id : "",
            d.run_seq != null ? "run_seq=" + d.run_seq : "",
            d.account_queue_index != null ? "account=" + d.account_queue_index + "/" + (d.account_queue_total || 0) : ""
          ].filter(Boolean).join("\n");
          if (line) teAutoPyCycleLogLines.push(line);
          if (teAutoPyCycleLogLines.length > 40) teAutoPyCycleLogLines = teAutoPyCycleLogLines.slice(-40);
        } catch (_) {}
      }
      async function teAutoPyReportCycleState(info) {
        try {
          const payload = Object.assign({ schema: IGBLUE_CYCLE_SCHEMA, build_version: IGBLUE_BUILD_VERSION, build_tag: IGBLUE_BUILD_TAG, href: teCurrentTargetUrl(), source: "side_auto_py" }, info || {});
          payload.cycle_log_lines = teAutoPyCycleLogLines.slice();
          payload.cycle_log_text = payload.cycle_log_lines.join("\n");
          payload.cycle_log_id = teAutoPyCycleLogId || String(payload.cycle_id || "");
          return await teAiwkWsRequest("ig_auto_cycle_state", payload, 1600);
        } catch (err) {
          dbg("auto_py", "cycle_state_report_failed", 59301, { error: String(err && err.message || err), info });
          return null;
        }
      }
      const teAutoPyLogOnceState = { map: {} };
      function teAutoPyLogOnce(message, minGapMs) {
        try {
          const msg = String(message || "");
          const now = Date.now();
          const gap = Math.max(8000, Number(minGapMs || 12000) || 12000);
          const key = msg.replace(/\d{1,2}시\s*\d{1,2}분\s*\d{1,2}초\s*/g, "").replace(/\s+/g, " ").trim();
          teAutoPyLogOnceState.map = teAutoPyLogOnceState.map || {};
          const prev = teAutoPyLogOnceState.map[key] || 0;
          if (prev && now - prev < gap) return false;
          teAutoPyLogOnceState.map[key] = now;
          n(msg);
          return true;
        } catch (_) { try { n(String(message || "")); } catch (_) {} return true; }
      }
      function teAutoPyCycleLine(stage, info) {
        const d = info || {};
        if (String(d.action || "").toLowerCase() === "stop" || String(d.decision || "").toLowerCase() === "auto_stopped") {
          const stopKo = String(d.stop_reason_ko || d.autoplay_stop_reason_ko || d.reason || stage || "자동 중단");
          teAutoPyLogOnce(`[자동다운로드][중단][체크실패] ${stopKo}`, 2500);
        }
        dbg("auto_py", stage, 59302, d);
      }
      async function teAutoPyCycle(stage, info) {
        const ctx = teCurrentWsTargetPayload(stage || "cycle", {});
        const acct = teAutoPyAccountQueueStatus ? teAutoPyAccountQueueStatus() : null;
        const payload = Object.assign({
          cycle_stage: stage,
          feature_check_mode: true,
          updated_at: new Date().toISOString(),
          tab_id: Number(ctx && ctx.tab_id || 0) || 0,
          request_tab_id: Number(ctx && ctx.tab_id || 0) || 0,
          window_id: Number(ctx && ctx.window_id || 0) || 0,
          bg_version: IGBLUE_BUILD_TAG,
          bg_build_tag: IGBLUE_BUILD_TAG,
          account_queue_index: acct && acct.has ? Number(acct.index || 0) : 0,
          account_queue_total: acct && acct.has ? Number(acct.total || 0) : 0,
          account_queue_url: acct && acct.has ? String(acct.url || "") : "",
          account_queue_username: acct && acct.has ? String(acct.account || "").replace(/^@/, "") : ""
        }, info || {});
        teAutoPyCycleLogPush(stage, payload);
        teAutoPyCycleLine(stage, payload);
        await teAutoPyReportCycleState(payload);
        return payload;
      }

      // v179a39 window movement pause state.
      let teWindowMoveState = { moving:false, stable:false, last_move_at:0, screen_x:null, screen_y:null, geometry_signature:"", recalibration_promise:null, last_reported_state:"", pending_recalibration:false };
      async function teAutoPyHandleWindowMoveState(detail) {
        const d = detail && detail.window_move ? detail.window_move : detail || {};
        const state = String(d.state || '');
        if (!state) return;
        teWindowMoveState.screen_x = Number(d.screen_x);
        teWindowMoveState.screen_y = Number(d.screen_y);
        teWindowMoveState.geometry_signature = String(d.geometry_signature || teWindowMoveState.geometry_signature || "");
        teWindowMoveState.last_move_at = Number(d.last_move_at || Date.now());
        if (state === 'WINDOW_BASELINE') {
          teWindowMoveState.last_reported_state = state;
          teWindowMoveState.moving = false; teWindowMoveState.stable = true;
          return;
        }
        if (state === 'WINDOW_MOVING' || state === 'WINDOW_STABILIZING') {
          const firstForState = teWindowMoveState.last_reported_state !== state;
          teWindowMoveState.last_reported_state = state;
          teWindowMoveState.moving = false; teWindowMoveState.stable = false;
          teWindowMoveState.pending_recalibration = false;
          if (firstForState) {
            try { await teAutoPyCycle('S_WINDOW_MOVE_DIRECT_CONTINUE', {decision:'continue', action:'direct_download_no_mouse', reason:state, window_move:d}); } catch (_) {}
          }
          return;
        }
        if (state === 'WINDOW_STABLE') {
          teWindowMoveState.last_reported_state = state;
          teWindowMoveState.moving = false; teWindowMoveState.stable = true;
          teWindowMoveState.pending_recalibration = false;
          teWindowMoveState.recalibration_promise = null;
          if (teAutoPyRunning && !teAutoPyStopping) {
            try { await teAutoPyCycle('S_WINDOW_STABLE_DIRECT_CONTINUE', {decision:'continue', action:'direct_download_no_mouse', reason:'window_stable', window_move:d}); } catch (_) {}
          }
        }
      }
      async function teAutoPyWaitWindowMoveGate() {
        if (!teWindowMoveState.moving && !teWindowMoveState.recalibration_promise) return {ok:true};
        const start=Date.now();
        while ((teWindowMoveState.moving || teWindowMoveState.recalibration_promise) && Date.now()-start<180000) {
          if (teAutoPyStopping) return {ok:false,error:'user_stop'};
          await teSleep(250);
        }
        return {ok:!teAutoPyStopping, waited_ms:Date.now()-start};
      }

      async function teAutoPyWaitOrPause(reason, limitSec, checkFn, info) {
        const code = String(reason || "wait_or_pause");
        const maxMs = Math.max(1000, Number(limitSec || 30) * 1000);
        const start = Date.now();
        const base = Object.assign({}, info || {}, { decision: "wait", action: "wait_or_pause", reason: code, limit_sec: Math.round(maxMs / 1000) });
        await teAutoPyCycle("S_WAIT_OR_PAUSE", base);
        while (Date.now() - start < maxMs) {
          if (teAutoPyStopping) return { ok: false, stopped: true, error: "user_stop" };
          try {
            const res = typeof checkFn === "function" ? await checkFn() : { ok: true };
            if (res && res.ok) {
              await teAutoPyCycle("S_WAIT_OR_PAUSE_RESUME", Object.assign({}, base, { decision: "resume", waited_ms: Date.now() - start, result: res || null }));
              return Object.assign({ ok: true, waited_ms: Date.now() - start }, res || {});
            }
          } catch (err) {
            await teAutoPyCycle("S_WAIT_OR_PAUSE_RETRY", Object.assign({}, base, { decision: "retry", waited_ms: Date.now() - start, error: String(err && err.message || err) }));
          }
          await teSleep(1000);
        }
        await teAutoPyCycle("S_WAIT_OR_PAUSE_TIMEOUT", Object.assign({}, base, { decision: "timeout", waited_ms: Date.now() - start }));
        return { ok: false, timeout: true, error: code + "_timeout", waited_ms: Date.now() - start };
      }
      async function teAutoPyReadCurrentNoNav(waitMs = 500) {
        return teSendToTargetTabResponse({ type: "igblue_auto_py_read_current_no", wait_ms: Math.max(120, Number(waitMs || 500) || 500) }, "", Math.max(1000, Number(waitMs || 500) + 700)).catch((err) => ({ ok: false, error: String(err && err.message || err) }));
      }
      async function teAutoPyClickCurrentNoNav(info, waitMs = 1300, stage = "S02_CURRENT_NO_CLICK") {
        const reqWait = Math.max(180, Number(waitMs || 1300) || 1300);
        const res = await teSendToTargetTabResponse({ type: "igblue_auto_py_current", wait_ms: reqWait }, "", Math.max(900, reqWait + 900)).catch((err) => ({ ok: false, error: String(err && err.message || err) }));
        const currentOk = !!(res && res.ok && (res.confirmed_current || res.target_found || Number(res.after_rank || 0) > 0));
        await teAutoPyCycle(stage || "S02_CURRENT_NO_CLICK", Object.assign({}, info || {}, {
          decision: currentOk ? "current_clicked" : "current_click_unconfirmed",
          action: "dom_click_NoNav_Current",
          first_current_click: !!((info || {}).first_current_click),
          current_refocus: !!((info || {}).current_refocus),
          next_requested: false,
          next_clicked: false,
          current_button_found: !!(res && res.current_button_found),
          active_rank_no: Number(res && res.after_rank || 0) || Number(res && res.before_rank || 0) || 0,
          requested_rank_no: Number(res && res.before_rank || 0) || 0,
          active_shortcode: res && res.after_shortcode || res && res.before_shortcode || "",
          current_response: res || null,
          reason: currentOk ? "NoNav_Current_clicked_and_target_refreshed" : "NoNav_Current_click_unconfirmed"
        }));
        return res;
      }
      async function teAutoPyClickNextNoNav(info, waitMs = 1300, stage = "S13_NEXT_DOM_CLICK") {
        const res = await teSendToTargetTabResponse({ type: "igblue_auto_py_next", wait_ms: Math.max(500, Number(waitMs || 1300) || 1300) }, "", Math.max(1600, Number(waitMs || 1300) + 900)).catch((err) => ({ ok: false, error: String(err && err.message || err) }));
        const nextClicked = !!(res && res.ok && res.changed && Number(res.after_rank || 0) > 0);
        await teAutoPyCycle(stage || "S13_NEXT_DOM_CLICK", Object.assign({}, info || {}, {
          decision: nextClicked ? "next_clicked" : "next_click_unconfirmed",
          action: "dom_click_NoNav_Next",
          next_requested: true,
          next_button_found: !!(res && res.next_button_found),
          next_clicked: nextClicked,
          next_to_rank_no: Number(res && res.after_rank || 0) || 0,
          next_to_rank_label: res && res.after_label || (Number(res && res.after_rank || 0) ? teAutoPyNoLabel(Number(res.after_rank || 0)) : ""),
          next_before_rank: res && res.before_rank,
          next_after_rank: res && res.after_rank,
          next_before_shortcode: res && res.before_shortcode,
          next_after_shortcode: res && res.after_shortcode,
          next_response: res || null,
          reason: nextClicked ? "NoNav_Next_changed_current" : "NoNav_Next_no_change_observed"
        }));
        return res;
      }
      async function teAutoPyGoNoWithConfirm(no, info, waitMs, stage) {
        const requestedNo = Number(no || 1) || 1;
        const res = await teSendToTargetTabResponse({ type: "igblue_auto_py_go_no", no: requestedNo, wait_ms: Math.max(300, Number(waitMs || 900) || 900) }, "", Math.max(1200, Number(waitMs || 900) + 900)).catch((err) => ({ ok: false, error: String(err && err.message || err) }));
        const nextClicked = !!(res && res.ok && (res.changed || Number(res.after_rank || 0) === requestedNo));
        await teAutoPyCycle(stage || "S09_DUPLICATE_NEXT", Object.assign({}, info || {}, {
          decision: nextClicked ? "next_clicked" : "next_requested_but_not_confirmed",
          action: "dom_go_no_next",
          next_requested: true,
          next_button_found: !!(res && res.target_found),
          next_clicked: nextClicked,
          next_to_rank_no: requestedNo,
          next_to_rank_label: teAutoPyNoLabel(requestedNo),
          next_before_rank: res && res.before_rank,
          next_after_rank: res && res.after_rank,
          next_before_shortcode: res && res.before_shortcode,
          next_after_shortcode: res && res.after_shortcode,
          next_response: res || null,
          reason: nextClicked ? "target_changed_or_requested_rank_active" : "content_go_no_no_change_observed"
        }));
        return res;
      }

      async function teMediaPrecheckAiwk(entry) {
        if (!entry || !entry.meta) return null;
        const accountFolderMerge = (typeof entry.account_folder_merge === "boolean") ? entry.account_folder_merge : !!entry.meta.account_folder_merge;
        return teAiwkTrackWsResult(await teAiwkWsRequest("ig_media_exists", {
          root: tePersistDownloadRootNow(),
          account: accountFolderMerge ? "" : (entry.account || teDownTxtAccount()),
          account_folder_merge: accountFolderMerge,
          prefix: entry.prefix || "",
          url: entry.url || "",
          href: teCurrentTargetUrl(),
          cycle: entry.cycle || null,
          meta: entry.meta
        }, 4500));
      }
      async function teNotifyMediaDownloadedAiwk(entry, downRes) {
        if (!entry) return null;
        const accountFolderMerge = (typeof entry.account_folder_merge === "boolean") ? entry.account_folder_merge : !!(entry.meta && entry.meta.account_folder_merge);
        const meta = Object.assign({ account_folder_merge: accountFolderMerge }, entry.meta || {});
        if (downRes && downRes.filename) meta.file_name = teFilenameOnly(downRes.filename);
        const notify = teAiwkTrackWsResult(await teAiwkWsRequest("ig_media_downloaded", {
          root: tePersistDownloadRootNow(),
          account: accountFolderMerge ? "" : (entry.account || teDownTxtAccount()),
          account_folder_merge: accountFolderMerge,
          prefix: entry.prefix || "",
          url: entry.url || "",
          file_path: downRes && downRes.filename ? String(downRes.filename) : "",
          download_id: downRes && downRes.id ? downRes.id : "",
          meta
        }, 20000));
        if (notify && notify.ok && notify.item) teRecordDownloadedItem(notify.item);
        return notify;
      }
      function teRecordDownloadedItem(item) {
        if (!item || typeof item !== "object") return;
        const merged = teNormalizeDownItems([item])[0];
        if (!merged) return;
        const k = teDownItemKey(merged);
        const old = teDownItems.filter(x => teDownItemKey(x) !== k);
        old.push(merged);
        teRenderDownList(old, teDownTxtAccount());
      }
      function teRecordDownloadedFile(pathOrName, item) {
        if (item && typeof item === "object") return teRecordDownloadedItem(item);
        const name = teFilenameOnly(pathOrName);
        if (!/\.mp4$/i.test(name)) return;
        const m = name.match(/_No(\d{3})_/i);
        teRecordDownloadedItem({ rank_no: m ? Number(m[1]) : 0, rank_label: m ? ("No" + m[1]) : "", asset_kind: "video", file_name: name, shortcode: "" });
      }
      function teInitDownTxtUi() {
        const ta = document.getElementById("igblue_down_file_list"), load = document.getElementById("igblue_down_txt_load_aiwk"), save = document.getElementById("igblue_down_txt_save_aiwk"), clear = document.getElementById("igblue_down_txt_clear");
        if (!ta && !load && !save && !clear) return;
        teLoadDownTxtLocal(teDownTxtAccount());
        const setupLink = document.getElementById("igblue_aiwk_setup_zip_link");
        setupLink && setupLink.addEventListener("click", (ev) => {
          ev.preventDefault();
          try { window.open(teAiwkSetupZipUrl, "_blank", "noopener,noreferrer"); } catch { location.href = teAiwkSetupZipUrl; }
        });
        load && load.addEventListener("click", () => teLoadDownTxtAiwk());
        save && save.addEventListener("click", () => teSaveDownTxtAiwk(teDownTxtAccount()));
        clear && clear.addEventListener("click", () => { teRenderDownList([], teDownTxtAccount()); teSaveDownTxtAiwk(teDownTxtAccount()); });
      }

      function teRestoreLastInfoForPopup() {
        if (!teIsPopupMode()) return;
        teRequestTargetState(!0);
      }
      function H() {
        var e,
          t = IGBLUE_PACKAGE_VERSION;
        ((document.querySelector(".version").textContent = t),
          teInitPopupUi(),
          (() => {
            const e = document.getElementById("upgrade");
            e && e.addEventListener("click", z);
          })(),
          document.getElementById("export_xlsx").addEventListener("click", () => teRunButtonOnce("export_xlsx", "export_xlsx", 1200, U)),
          teInitDownloadOptions(),
          teInitDownloadFolderUi(),
          teInitDownTxtUi(),
          teInitPyAutoUi(),
          teAutoPyRestoreAccountQueue(),
          (function(){
            const startBtn = document.getElementById("igblue_auto_py_start"), stopBtn = document.getElementById("igblue_auto_py_stop");
            if (startBtn) startBtn.addEventListener("click", () => teAutoPyStartOnceFromUi());
            if (stopBtn) stopBtn.addEventListener("click", () => teAutoPyStop("user"));
          })(),
          (function () {
            (document
              .getElementById("auto_swipe")
              .addEventListener("click", () => Y(1)),
              document
                .getElementById("auto_swipe_10")
                .addEventListener("click", () => V(10)),
              document
                .getElementById("auto_swipe_30")
                .addEventListener("click", () => V(30)),
              document
                .getElementById("auto_top")
                .addEventListener("click", () =>
                  (function () {
                    const e = `top-${Date.now()}`;
                    B(e);
                    teSendToTargetTab({ type: "ndy_top" }, "Instagram tab not found").finally(() => D(e));
                  })(),
                ),
              document
                .getElementById("down_all_medias")
                .addEventListener("click", () => {
                  teRunButtonOnce("down_all_medias", "down_all_medias", 1200, () => L(() => X("all", se), -1, !1));
                }),
              document
                .getElementById("down_filtered_medias")
                .addEventListener("click", () => {
                  teRunButtonOnce("down_filtered_medias", "down_filtered_medias", 1200, () => L(() => X("filtered", oe), -1, !1));
                }));
            const e = document.getElementById("goto");
            e &&
              e.addEventListener("click", () => {
                window.open("https://www.instagram.com/explore/", "_blank");
              });
            const t = document.getElementById("refresh");
            t &&
              t.addEventListener("click", () => {
                tePersistDownloadOptionsNow();
                const e = document.getElementById("top_n_count");
                e && (teLocalSet("filter/metric_threshold_10k", e.value || ""), teLocalSet("top_metric_units", e.value || ""), teLocalSet("top_n_count", e.value || ""));
                teRefreshRanksAndMetricCount();
                teSendToTargetTab({ type: "ndy_refresh" }, "Instagram tab not found");
              });
            const miniRefresh = document.getElementById("mini_refresh");
            miniRefresh && miniRefresh.addEventListener("click", () => {
              teRefreshRanksAndMetricCount();
              teSendToTargetTab({ type: "ndy_refresh" }, "Instagram tab not found");
            });
          })(),
          (function () {
            Promise.all([S(), M()]).catch((e) => {});
            const e = document.getElementById("settings");
            e && e.addEventListener("click", () => {
              if (!(teHasInstagramTarget || u)) return void n("인스타 홈페이지를 먼저 열어주세요");
              T();
            });
          })(),
          ((e = document.getElementById("sort_by")).value = teSelectHasValue(e, teLocalGetAny(["filter/sort_by", "sort_by"], "4")) ? teLocalGetAny(["filter/sort_by", "sort_by"], "4") : Z),
          (Z = parseInt(e.value)),
          e.addEventListener("change", function () {
            (teLocalSet("filter/sort_by", this.value),
              teLocalSet("sort_by", this.value),
              (Z = parseInt(this.value)),
              ae(se, K, Z, !1, !0),
              teRefreshRanksAndMetricCount());
          }),
          (function () {
            const e = document.getElementById("timeRange"),
              t = teLocalGetAny(["filter/timeRange", "timeRange"], e?.value || "0");
            e && teSelectHasValue(e, t) && (e.value = String(t));
            K = teRangeToTimestamp(e?.value || "0");
            e &&
              e.addEventListener("change", function () {
                (teLocalSet("filter/timeRange", this.value),
                  teLocalSet("timeRange", this.value),
                  (K = teRangeToTimestamp(this.value)),
                  ae(se, K, Z, !0, !1),
                  teRefreshRanksAndMetricCount());
              });
          })(),
          I(0, 0),
          P());
      }
      function V(e) {
        L(Y, e, !1);
      }
      function teDownloadVideoOnly() {
        const e = document.getElementById("download_video_only");
        return !!e?.checked;
      }
      function teDownloadIdDateName() {
        const e = document.getElementById("download_id_date_name");
        return !!e?.checked;
      }
      function teDownloadDelaySec() {
        const e = document.getElementById("download_delay_sec");
        let t = parseInt(e?.value ?? teLocalGetAny(["download/delay_sec", "download_delay_sec"], "0"), 10);
        if (!Number.isFinite(t) || t < 0) t = 0;
        if (t > 99) t = 99;
        if (e && String(e.value) !== String(t)) e.value = String(t).padStart(0, "");
        return t;
      }
      function tePersistDownloadOptionsNow() {
        const e = document.getElementById("download_video_only"),
          t = document.getElementById("download_id_date_name"),
          r = document.getElementById("download_delay_sec");
        e && (teLocalSet("download/video_only", e.checked ? "1" : "0"), teLocalSet("download_video_only", e.checked ? "1" : "0"));
        t && (teLocalSet("download/id_date_filename", t.checked ? "1" : "0"), teLocalSet("download_id_date_name", t.checked ? "1" : "0"));
        if (r) {
          const e = teDownloadDelaySec();
          teLocalSet("download/delay_sec", e);
          teLocalSet("download_delay_sec", e);
        }
      }
      function teSafeName(e) {
        return String(e || "unknown")
          .replace(/[\\/:*?"<>|]+/g, "_")
          .replace(/\s+/g, "_")
          .replace(/_+/g, "_")
          .replace(/^_+|_+$/g, "")
          .slice(0, 80) || "unknown";
      }
      function teDateStamp(e) {
        const t = Number(e?.created_at || e?.taken_at || e?.caption?.created_at || e?.media?.taken_at || e?.media?.caption?.created_at);
        if (!Number.isFinite(t) || t <= 0) return "nodate";
        const r = new Date(1e3 * t),
          n = (e) => String(e).padStart(2, "0");
        return `${r.getFullYear()}${n(r.getMonth() + 1)}${n(r.getDate())}_${n(r.getHours())}${n(r.getMinutes())}${n(r.getSeconds())}`;
      }
      function teIsVideoUrl(e) {
        const t = String(e || "").split("?")[0].toLowerCase();
        return t.includes(".mp4") || t.includes("/video/") || t.includes("video");
      }
      // IGblue local patch v18: do not use each post/media username for file/folder identity.
      // The visible channel account is decided by the current/pinned Instagram URL and is fixed
      // for the download session; a reposted/foreign media item may carry another username.
      function teDownloadChannelAccountName() {
        return teSafePath(teActiveFolderAccountName() || teAccountFromUrl(teCurrentTargetUrl()) || "instagram");
      }
      function teFilenameAccountName(e) {
        const t = teUrlMode(teCurrentTargetUrl());
        if (["explore", "p", "reel", "reels", "stories"].includes(t)) return teMediaOwnerName(e);
        const folderAccount = teCleanOwnerAccountName(teDownloadChannelAccountName().split("/").pop() || teDownloadChannelAccountName());
        return folderAccount || teMediaOwnerName(e);
      }
      function teDisplayAccountForMedia(e) {
        const t = teUrlMode(teCurrentTargetUrl());
        if (["explore", "p", "reel", "reels", "stories"].includes(t)) return teMediaOwnerName(e);
        const folderAccount = teCleanOwnerAccountName(teDownloadChannelAccountName().split("/").pop() || teDownloadChannelAccountName());
        return folderAccount || teMediaOwnerName(e);
      }
      function teOriginalInstagramFileInfo(mediaUrl) {
        const raw = String(mediaUrl || "").trim();
        let name = "";
        try {
          const u = new URL(raw, location.href);
          name = decodeURIComponent((u.pathname || "").split("/").filter(Boolean).pop() || "");
        } catch {
          name = raw.split(/[?#]/)[0].split("/").pop() || "";
        }
        name = name.replace(/[\/:*?"<>|]+/g, "_").replace(/\s+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
        const m = name.match(/^(.*)\.([a-z0-9]{2,5})$/i);
        const stem = String(m && m[1] ? m[1] : name || "media")
          .replace(/[\/:*?"<>|\u0000-\u001f]+/g, "_")
          .replace(/[. ]+$/g, "")
          .trim()
          .slice(0, 180) || "media";
        const ext = (m && m[2] ? m[2] : (teIsVideoUrl(raw) ? "mp4" : "jpg")).toLowerCase();
        return { original_filename: stem, original_filename_with_ext: stem + "." + ext, original_ext: ext, source_url: raw };
      }
      function teFirstLoadIsoFromStamp(stamp) {
        const m = String(stamp || "").match(/^(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})$/);
        return m ? `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}` : "";
      }
      function teFirstLoadKey(account, shortcode, originalName) {
        // v71: first-load stamp is fixed by folder account + Instagram shortcode only.
        // CDN filename must not split jpg/mp4 into different stems.
        return "download/first_load/" + teSafePath(account || "instagram") + "/" + teSafeName(shortcode || "no_shortcode");
      }
      function teFirstLoadStamp(account, shortcode, originalName) {
        const key = teFirstLoadKey(account, shortcode, originalName);
        let saved = teLocalGet(key, "");
        if (!/^\d{8}_\d{6}$/.test(saved)) {
          saved = teCollectDateStamp();
          teLocalSet(key, saved);
        }
        return saved;
      }
      function teRankNoFromPrefix(prefix) {
        const m = String(prefix || "").match(/(?:^|_)No(\d{1,6})(?:_|$)/);
        const n = m ? Number(m[1]) : 0;
        return Number.isFinite(n) && n > 0 ? n : 0;
      }
      function teRankNoForMedia(media, fallbackIndex) {
        const code = String(media?.code || "");
        try {
          const ctx = teBuildRankContext(teMetricSourceList());
          if (ctx && ctx.active && ctx.map && ctx.map.has(code)) return Number(ctx.map.get(code)) || 0;
        } catch {}
        try {
          const list = teMetricSourceList();
          if (Array.isArray(list) && code) {
            const idx = list.findIndex((x) => String(x?.code || "") === code);
            if (idx >= 0) return idx + 1;
          }
        } catch {}
        const fb = Number(fallbackIndex);
        return Number.isFinite(fb) && fb >= 0 ? fb + 1 : 0;
      }
      function teMakeDownloadPrefix(e, t, r, mediaUrl, rankNo) {
        const account = teOwnerAccountForMedia(e || {});
        if (!account) return "";
        const code = teSafeName(e?.code || "media");
        const upload = teUploadStampFromMedia(e || {});
        const no = Number(rankNo || teRankNoForMedia(e, 0)) || 0;
        const noPart = no > 0 ? `_No${String(no).padStart(3, "0")}` : `_No없음`;
        const base = `${account}_${upload}${noPart}_${code}`;
        return Number(r || 1) > 1 ? `${base}_${Number(t || 0) + 1}` : base;
      }
      // IGblue local patch: persist side-panel options as IGblue/category/name=value.
      function teLocalKey(e) {
        return `IGblue/${e}`;
      }
      function teLocalGet(e, t = "") {
        try {
          const r = localStorage.getItem(teLocalKey(e));
          return null == r ? t : r;
        } catch {
          return t;
        }
      }
      function teLocalGetAny(e, t = "") {
        for (const r of e) {
          const e = teLocalGet(r, null);
          if (null !== e) return e;
        }
        return t;
      }
      function teLocalSet(e, t) {
        try {
          localStorage.setItem(teLocalKey(e), String(t));
        } catch {}
      }
      function teLocalBool(e, t = !1) {
        const r = teLocalGet(e, t ? "1" : "0");
        return "1" === r || "true" === r || !0 === r;
      }
      function teSelectHasValue(e, t) {
        return !!e && Array.from(e.options || []).some((e) => e.value === String(t));
      }
      function teRestoreTopN() {
        // IGblue local patch v14: top_n_count is reused as threshold units.
        // Metric threshold restore is handled by teRestoreMetricThreshold().
      }
      function teRangeToTimestamp(e) {
        let t,
          r = parseInt(e);
        if (0 === r || !Number.isFinite(r)) t = 0;
        else {
          var n = new Date(),
            i = new Date(n);
          (i.setDate(i.getDate() - r),
            i.setHours(0, 0, 0, 0),
            (t = Math.floor(i.getTime() / 1e3)));
        }
        return t;
      }
      function teInitDownloadOptions() {
        const e = document.getElementById("download_video_only"),
          t = document.getElementById("download_id_date_name"),
          r = document.getElementById("download_delay_sec");
        e && (e.checked = "1" === teLocalGetAny(["download/video_only", "download_video_only"], e.checked ? "1" : "0"));
        t && (t.checked = "1" === teLocalGetAny(["download/id_date_filename", "download_id_date_name"], "1"));
        if (r) r.value = String(Math.max(0, Math.min(99, parseInt(teLocalGetAny(["download/delay_sec", "download_delay_sec"], r.value || "0"), 10) || 0)));
        e &&
          e.addEventListener("change", () => {
            teLocalSet("download/video_only", e.checked ? "1" : "0");
            teLocalSet("download_video_only", e.checked ? "1" : "0");
            if (Array.isArray(oe) && oe.length) teScheduleMainRankLabels(oe, teBuildRankContext(oe));
            N();
          });
        t &&
          t.addEventListener("change", () => {
            teLocalSet("download/id_date_filename", t.checked ? "1" : "0");
            teLocalSet("download_id_date_name", t.checked ? "1" : "0");
            if (Array.isArray(oe) && oe.length) teScheduleMainRankLabels(oe, teBuildRankContext(oe));
            N();
          });
        r && ["change", "blur"].forEach((e) => r.addEventListener(e, () => {
          const e = teDownloadDelaySec();
          teLocalSet("download/delay_sec", e);
          teLocalSet("download_delay_sec", e);
          N();
        }));
        teRestoreTopN();
        teRestoreMetricThreshold();
      }
      function teDownloadClientId(prefix = "download") {
        return `${String(prefix || "download")}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
      }
      function teDownloadSourceMode(scope) {
        return scope === "all" ? "BULK_ALL" : scope === "filtered" ? "BULK_FILTERED" : scope === "top" ? "TOP_SORTED" : "MANUAL_SINGLE";
      }
      function tePrepareDownloadFlowMeta(meta, defaults = {}) {
        const out = Object.assign({}, meta || {}, defaults || {});
        out.source_mode = String(out.source_mode || "MANUAL_SINGLE").toUpperCase();
        out.client_batch_key = String(out.client_batch_key || teDownloadClientId("batch"));
        out.client_job_key = String(out.client_job_key || teDownloadClientId("job"));
        out.client_event_id = String(out.client_event_id || out.client_job_key);
        out.batch_requested_count = Math.max(1, Number(out.batch_requested_count || 1) || 1);
        return out;
      }
      function q(e, accountFolderMergeOverride, flowMeta) {
        const accountFolderMerge = (typeof accountFolderMergeOverride === "boolean") ? accountFolderMergeOverride : teDownloadAccountFolderMerge();
        dbg("q", "entry", 58848, { media: dbgMedia(e), video_only: teDownloadVideoOnly(), id_date: teDownloadIdDateName(), active_account: teActiveFolderAccountName(), account_folder_merge: accountFolderMerge });
        if (!e || !e.code) { dbg("q", "empty_media", 58849, {media:e}); return []; }
        tePersistDownloadOptionsNow();
        const t = [],
          r = teDownloadVideoOnly();
        if ("string" == typeof e.carousel_media && e.carousel_media.trim()) {
          const parts = e.carousel_media.split(/\r?\n/).map((e) => e.trim()).filter(Boolean);
          dbg("q", "carousel_parts", 58853, {code:e.code, parts_len:parts.length, video_only:r, sample:parts.slice(0,5).map(x=>({url:String(x).slice(0,180), is_video:teIsVideoUrl(x)}))});
          parts.forEach((e) => { (!r || teIsVideoUrl(e)) && t.push(e); });
        } else if (r) {
          if (e.video_url) t.push(e.video_url);
          else dbg("q", "video_only_missing_video_url", 58862, { media: dbgMedia(e) });
        } else {
          if (e.img_origin) t.push(e.img_origin); else dbg("q", "missing_img_origin", 58863, { media: dbgMedia(e) });
          if (e.video_url) t.push(e.video_url); else dbg("q", "missing_video_url_non_video_only", 58864, { media: dbgMedia(e) });
        }
        const n = Array.from(new Set(t));
        const normalizedFlowMeta = tePrepareDownloadFlowMeta(Object.assign({}, flowMeta || {}), {
          source_mode: (flowMeta && flowMeta.source_mode) || "MANUAL_SINGLE",
          client_batch_key: (flowMeta && flowMeta.client_batch_key) || teDownloadClientId("batch-manual"),
          batch_requested_count: (flowMeta && flowMeta.batch_requested_count) || n.length || 1
        });
        const out = n.map((t, r) => {
          const prefix = teMakeDownloadPrefix(e, r, n.length, t, teRankNoForMedia(e, 0));
          if (!prefix) return null;
          const flowEntryKey = `${normalizedFlowMeta.client_batch_key}:${String(e.code || "post")}:${teIsVideoUrl(t) ? "VIDEO" : "IMAGE"}:${r}`;
          const meta = tePrepareDownloadFlowMeta(teBuildMediaDownloadMeta(e, t, prefix, r, n.length), Object.assign({}, normalizedFlowMeta, { client_job_key: flowEntryKey, client_event_id: flowEntryKey }));
          if (meta.owner_missing || !teCleanOwnerAccountName(meta.owner_username)) return null;
          return {
            url: t,
            prefix,
            account: accountFolderMerge ? "" : teSafePath(meta.owner_username || teOwnerAccountForMedia(e) || "instagram"),
            account_folder_merge: accountFolderMerge,
            meta: Object.assign({account_folder_merge: accountFolderMerge}, meta)
          };
        }).filter(Boolean);
        if (!out.length) {
          dbg("q", "owner_missing_or_no_assets", 58865, { code:e.code, owner:teOwnerAccountForMedia(e), urls:n.length });
          teDownTxtStatus("계정명을 찾지 못해 다운로드를 보류했습니다. explore/reels/p는 계정명으로 사용하지 않습니다.");
        }
        dbg("q", "output_entries", 58866, {code:e.code, count:out.length, entries:out.map(x=>({prefix:x.prefix, account:x.account, url:String(x.url||"").slice(0,220), is_video:teIsVideoUrl(x.url)}))});
        return out;
      }
      async function W(e) {
        // IGblue local patch v39: precheck remote/local DB before down, then save JSON+MySQL after Chrome down completes.
        try {
          const pre = await teMediaPrecheckAiwk(e);
          if (pre && pre.ok && pre.exists) {
            teDownTxtStatus("skip duplicate " + (pre.match_by || "exists") + " " + (e.prefix || e.meta?.shortcode || "media"));
            return true;
          }
        } catch (err) {
          dbg("W", "precheck_failed", 59082, {prefix:e&&e.prefix, error:String(err&&err.message||err)});
        }
        return await Q(e.url, e.prefix, e.account, Object.assign({ account_folder_merge: !!e.account_folder_merge }, e.meta || {}));
      }
      async function X(e, t, resumeState) {
        dbg("bulk_X", "start", 58876, {scope:e, list_len:Array.isArray(t)?t.length:null, resume:!!resumeState, resume_queue:resumeState&&Array.isArray(resumeState.queue)?resumeState.queue.length:null});
        if (resumeState && Array.isArray(resumeState.queue)) {
          teDownloadSessionAccount = resumeState.account || teActiveFolderAccountName();
          teDownloadSessionAccountFolderMerge = (typeof resumeState.accountFolderMerge === "boolean")
            ? resumeState.accountFolderMerge
            : !!(resumeState.queue[0] && resumeState.queue[0].entry && resumeState.queue[0].entry.account_folder_merge);
        } else {
          teDownloadResumeState = null;
          if (!Array.isArray(t) || 0 === t.length) return void n("empty list");
          // v213: account and merge checkbox are frozen together when a bulk download starts.
          teDownloadSessionAccount = teActiveFolderAccountName();
          teDownloadSessionAccountFolderMerge = tePersistDownloadAccountFolderMergeNow();
        }
        teUpdateDownloadFolderPath(teDownloadSessionAccount);
        teDownloadStopRequested = !1;
        teProgressState = { visible: !0, done: !1, file: "", file_path: "", file_paths: [], no_label: "", loaded: 0, total: 0, percent: 0, message: "", start_ts: Date.now(), mode: e };
        const sourceMode = teDownloadSourceMode(e);
        const batchKey = String((resumeState && resumeState.batchKey) || teDownloadClientId(`batch-${String(e || "manual")}`));
        let flat = [], totalPosts = 0, skippedPosts = 0, startIndex = 0, completedFiles = 0, processedPosts = 0, scopeLabel = "";
        if (resumeState && Array.isArray(resumeState.queue)) {
          flat = resumeState.queue;
          totalPosts = Number(resumeState.totalPosts) || 0;
          skippedPosts = Number(resumeState.skippedPosts) || 0;
          startIndex = Number(resumeState.nextIndex) || 0;
          completedFiles = Number(resumeState.completedFiles) || 0;
          processedPosts = Number(resumeState.processedPosts) || 0;
          scopeLabel = resumeState.scopeLabel || ("top" === e ? `Top ${flat.length} files` : "Down medias");
        } else {
          const flowMeta = { source_mode: sourceMode, client_batch_key: batchKey, batch_requested_count: t.length, batch_scope: String(e || ""), sort_by: teServerSortField(teTopMetricField() || String(G[Z] || "")) };
          const posts = t.map((media, idx) => ({ postIndex: idx, code: media.code, entries: q(media, teDownloadSessionAccountFolderMerge, flowMeta) }));
          totalPosts = posts.length;
          scopeLabel = "all" === e ? "All medias" : "top" === e ? `Top ${posts.length} medias` : "Filtered medias";
          posts.forEach((post) => {
            if (post.entries.length) post.entries.forEach((entry) => flat.push({ postIndex: post.postIndex, entry }));
            else skippedPosts += 1;
          });
        }
        const totalFiles = flat.length;
        flat.forEach((item) => { if (item && item.entry && item.entry.meta) item.entry.meta.batch_requested_count = totalFiles; });
        const teEnsureBulkState = () => {
          if (!p) p = { scopeLabel, statusLabel: resumeState ? "Resuming..." : "Preparing...", totalPosts, processedPosts, totalFiles, completedFiles, skippedPosts, currentFileLabel: "", start_ts: resumeState && resumeState.start_ts || Date.now() };
          return p;
        };
        p = { scopeLabel, statusLabel: resumeState ? "Resuming..." : "Preparing...", totalPosts, processedPosts, totalFiles, completedFiles, skippedPosts, currentFileLabel: "", start_ts: resumeState && resumeState.start_ts || Date.now() };
        teProgressState.start_ts = p.start_ts;
        N();
        if (totalFiles <= 0) {
          p.statusLabel = "No downloadable medias";
          N();
          n("No downloadable medias found");
          return;
        }
        B(a);
        try {
          for (let idx = startIndex; idx < flat.length; idx++) {
            const item = flat[idx];
            teEnsureBulkState();
            p.statusLabel = teDownloadStopRequested ? "Stopping..." : "Downing...";
            p.currentFileLabel = item.entry.prefix;
            p.processedPosts = Math.max(p.processedPosts, Math.min(totalPosts, item.postIndex + 1));
            N();
            dbg("bulk_X", "download_item", 58923, {idx, prefix:item.entry&&item.entry.prefix, url:String(item.entry&&item.entry.url||"").slice(0,220)});
            if (await W(item.entry)) { dbg("bulk_X", "download_item_ok", 58923, {idx, prefix:item.entry&&item.entry.prefix}); teEnsureBulkState(); p.completedFiles += 1; } else { dbg("bulk_X", "download_item_fail", 58923, {idx, prefix:item.entry&&item.entry.prefix}); }
            N();
            const teStopAndSave = () => {
              teEnsureBulkState();
              teDownloadResumeState = {
                scope: e,
                queue: flat,
                nextIndex: idx + 1,
                account: teDownloadSessionAccount,
                accountFolderMerge: !!teDownloadSessionAccountFolderMerge,
                totalPosts,
                totalFiles,
                completedFiles: p.completedFiles,
                processedPosts: p.processedPosts,
                skippedPosts: p.skippedPosts,
                scopeLabel,
                start_ts: p.start_ts || Date.now(),
                batchKey,
                sourceMode
              };
              p.statusLabel = "Stopped";
              p.currentFileLabel = "";
              teProgressState.message = "Stopped after current file.";
              N();
            };
            if (teDownloadStopRequested && idx < flat.length - 1) {
              teStopAndSave();
              return;
            }
            const delaySec = teDownloadDelaySec();
            if (delaySec > 0 && idx < flat.length - 1) {
              dbg("bulk_X", "download_delay_start", 58924, {idx, delaySec});
              teEnsureBulkState();
              p.statusLabel = `Delay ${delaySec}s...`;
              p.currentFileLabel = `Next after ${delaySec}s`;
              teProgressState.message = `Waiting ${delaySec}s before next download`;
              N();
              const delayEnd = Date.now() + delaySec * 1000;
              while (Date.now() < delayEnd) {
                if (teDownloadStopRequested) break;
                await R(Math.min(250, Math.max(0, delayEnd - Date.now())));
              }
              dbg("bulk_X", "download_delay_end", 58925, {idx, delaySec, stopped:teDownloadStopRequested});
              if (teDownloadStopRequested && idx < flat.length - 1) {
                teStopAndSave();
                return;
              }
            }
          }
          teDownloadResumeState = null;
          teEnsureBulkState();
          p.statusLabel = "Completed";
          p.currentFileLabel = "";
          teProgressState.done = !0;
          teProgressState.percent = 100;
          try { teAutoPyLastDoneText = "자동다운로드 완료\n" + teUnifiedProgressText(); n("자동다운로드 완료"); } catch (_) {}
          N();
        } finally {
          D(a);
          await R(300);
          if (!teDownloadResumeState) {
            teDownloadSessionAccount = "";
            teDownloadSessionAccountFolderMerge = null;
          }
          teDownloadStopRequested = !1;
          teUpdateDownloadControlButtons();
          P();
        }
      }
      function Y(e) {
        const t = `swipe-${Date.now()}`;
        B(t);
        teSendToTargetTab({ type: "ndy_swipe", count: e }, "Instagram tab not found").then((ok) => {
          ok || D(t);
        });
        const r = 4500 * Math.max(1, e);
        setTimeout(() => {
          D(t);
        }, r);
      }
      let K = 0,
        Z = 4;
      const G = [
        "like_count",
        "comment_count",
        "media_repost_count",
        "created_at",
        "play_count",
        "",
        "engagement_rate",
        "like_count",
      ];
      function J(e) {
        if (e)
          for (let t = 0; t < oe.length; t++) {
            const r = oe[t];
            if (r.code == e) return r;
          }
        return null;
      }
      async function Q(e, t, r, meta) {
        // v225: every manual/bulk route carries one unified MEDIA_SAVED flow identity.
        meta = tePrepareDownloadFlowMeta(meta || {}, { source_mode: (meta && meta.source_mode) || "MANUAL_SINGLE" });
        const metaOwner = meta && typeof meta === "object" ? teCleanOwnerAccountName(meta.owner_username || meta.username || "") : "";
        if (!metaOwner) {
          dbg("Q", "owner_missing_blocked", 58990, {prefix:t, account:r, meta});
          n("계정명을 찾지 못해 다운로드를 보류했습니다. explore/reels/p는 계정명으로 사용하지 않습니다.");
          return Promise.resolve(!1);
        }
        const accountFolderMerge = (meta && typeof meta.account_folder_merge === "boolean") ? meta.account_folder_merge : teDownloadAccountFolderMerge();
        const account = accountFolderMerge ? "" : teSafePath(metaOwner || r || teActiveFolderAccountName() || "instagram");
        teUpdateDownloadFolderPath(account);
        dbg("Q", "send_download_api", 58991, {prefix:t, account, account_folder_merge:accountFolderMerge, root:teDownloadRoot(), url:String(e||"").slice(0,260), is_video:teIsVideoUrl(e)});
        return teDownloadOnce(e, t, () => new Promise((resolve) => {
          try {
            chrome.runtime.sendMessage({
              type: "IGBLUE_DOWNLOAD_URL",
              url: e,
              prefix: t,
              root: tePersistDownloadRootNow(),
              account: accountFolderMerge ? "" : account,
              account_folder_merge: accountFolderMerge,
              href: teCurrentTargetUrl(),
              notify_aiwk: true,
              meta: Object.assign({href: teCurrentTargetUrl(), page_url: teCurrentTargetUrl(), account_folder_merge: accountFolderMerge}, meta || {})
            }, (res) => {
              if (chrome.runtime.lastError) { dbg("Q", "runtime_error", 59004, {prefix:t, message:chrome.runtime.lastError.message}); n("Down API failed"); return resolve(!1); }
              dbg("Q", "download_api_response", 59005, {prefix:t, res});
              if (res && res.status === "already_done") {
                const finalized = teAutoPyDirectReportResultFromResponse(res);
                n(res.message || (res.shortcode ? `이미 완료 shortcode=${res.shortcode}` : "이미 완료"));
                teHandleDownProgress({status:"already_done", file:res.file_path || res.file || t, file_path:res.file_path || "", file_paths:res.file_paths || [], json_path:finalized.json_path || res.json_path || "", report_ok:finalized.report_ok, report_path:finalized.report_path, shortcode:res.shortcode || (meta && meta.shortcode) || "", asset_kind:res.asset_kind || "", media_index:res.media_index || 0, percent:100, message:res.message || "이미 다운로드됨 / 메타데이터·보고서 갱신"});
                if (!finalized.ok) {
                  dbg("Q", "duplicate_report_finalize_failed", 59005, {prefix:t, finalized});
                  n("기존 파일 확인 후 JSON/보고서 갱신 실패");
                  return resolve(!1);
                }
                return resolve(!0);
              }
              if (!res || (res.status !== "ok" && res.status !== "timeout")) { n(res?.message || "Down failed"); return resolve(!1); }
              const finalized = teAutoPyDirectReportResultFromResponse(res);
              teRecordDownloadedFile(res.filename || t, finalized.notify && finalized.notify.item);
              if (!finalized.ok) {
                dbg("Q", "notify_media_downloaded_failed", 59006, {prefix:t, finalized});
                n("파일 저장 후 JSON/보고서 갱신 실패");
                return resolve(!1);
              }
              resolve(!0);
            });
          } catch (err) {
            dbg("Q", "exception", 59008, {prefix:t, message:String(err&&err.message||err)});
            n("Down API error");
            resolve(!1);
          }
        }));
      }
      function ee(e) {
        const t = document.getElementById("img_collection"),
          r = t.children.length,
          i = e.length;
        if (r < i)
          for (let e = r; e < i; e++) {
            const e = document.createElement("div");
            ((e.className = "post-item"),
              (e.innerHTML =
                '\n<a href="" target="_blank">\n    <img crossOrigin="anonymous">\n</a>\n<div class="overlay">\n    <div class="stat igblue-side-account">\n        <span>IG</span>\n    </div>\n    <div class="igblue-side-rank" style="display:none">\n        <span></span>\n    </div>\n    <div class="stat">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">\n            <title>Likes</title>\n            <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>\n        </svg>\n        <span></span>\n    </div>\n    <div class="stat">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">\n            <title>Comments</title>\n            <path d="M3 11.5a8.38 8.38 0 0 0 .9 3.8 8.5 8.5 0 0 0 7.6 4.7 8.38 8.38 0 0 0 3.8-.9L21 21l-1.9-5.7a8.38 8.38 0 0 0 .9-3.8 8.5 8.5 0 0 0-4.7-7.6 8.38 8.38 0 0 0-3.8-.9h-.5a8.48 8.48 0 0 0-8 8v.5z"></path>\n        </svg>\n        <span></span>\n    </div>\n    <div class="stat">\n        <svg width="16" height="16" viewBox="0 0 24 24"><title>Repost</title><path d="M4.51617 6.9986C6.13179 4.58593 8.88099 2.99979 11.9995 2.99979C15.7267 2.99979 18.9259 5.26459 20.2927 8.49676C20.5079 9.00543 21.0946 9.24341 21.6033 9.0283C22.1119 8.81318 22.3499 8.22644 22.1348 7.71777C20.466 3.7716 16.5582 0.999786 11.9995 0.999786C8.27776 0.999786 4.9897 2.84823 2.99988 5.67416V2.9986C2.99988 2.44631 2.55216 1.9986 1.99988 1.9986C1.44759 1.9986 0.999878 2.44631 0.999878 2.9986V7.9986C0.999878 8.55088 1.44759 8.9986 1.99988 8.9986H6.99988C7.55216 8.9986 7.99988 8.55088 7.99988 7.9986C7.99988 7.44631 7.55216 6.9986 6.99988 6.9986H4.51617Z" fill="currentColor"></path><path d="M2.39572 14.9713C2.90439 14.7562 3.49113 14.9942 3.70625 15.5029C5.07309 18.735 8.27228 20.9998 11.9995 20.9998C15.118 20.9998 17.8672 19.4137 19.4828 17.001H16.9991C16.4468 17.001 15.9991 16.5533 15.9991 16.001C15.9991 15.4487 16.4468 15.001 16.9991 15.001H21.9991C22.5514 15.001 22.9991 15.4487 22.9991 16.001V21.001C22.9991 21.5533 22.5514 22.001 21.9991 22.001C21.4468 22.001 20.9991 21.5533 20.9991 21.001V18.3255C19.0093 21.1514 15.7212 22.9998 11.9995 22.9998C7.44077 22.9998 3.53298 20.228 1.86419 16.2818C1.64908 15.7732 1.88705 15.1864 2.39572 14.9713Z" fill="currentColor"></path></svg>\n        <span></span>\n    </div>\n    <div class="stat">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">\n            <title>Create At</title>\n            <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>\n            <line x1="16" y1="2" x2="16" y2="6"></line>\n            <line x1="8" y1="2" x2="8" y2="6"></line>\n            <line x1="3" y1="10" x2="21" y2="10"></line>\n        </svg>\n        <span></span>\n    </div>\n    <div class="stat">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">\n            <title>Views(Reels)</title>\n            <path d="M23.441 11.819C23.413 11.74 20.542 4 12 4S.587 11.74.559 11.819a1 1 0 0 0 1.881.677 10.282 10.282 0 0 1 19.12 0 1 1 0 0 0 1.881-.677Zm-7.124 2.368a3.359 3.359 0 0 1-1.54-.1 3.56 3.56 0 0 1-2.365-2.362 3.35 3.35 0 0 1-.103-1.542.99.99 0 0 0-1.134-1.107 5.427 5.427 0 0 0-3.733 2.34 5.5 5.5 0 0 0 8.446 6.97 5.402 5.402 0 0 0 1.536-3.09.983.983 0 0 0-1.107-1.109Z" fill-rule="evenodd"></path>\n        </svg>\n        <span></span>\n    </div>\n    <div class="stat">\n        <span title="Engagement Rate(Formula is set in Settings)"></span>\n    </div>\n</div>\n<div class="overlay_download">\n    <div class="action stat icon">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n            <title>Down Image</title>\n            <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>\n            <circle cx="8.5" cy="8.5" r="2.5"></circle>\n            <path d="M21 15l-5-5L3 21"></path>\n        </svg>\n    </div>\n    <div class="action stat icon">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n            <title>Down Carousel Media</title>\n            <path fill="white" transform="scale(0.5)" d="M34.8 29.7V11c0-2.9-2.3-5.2-5.2-5.2H11c-2.9 0-5.2 2.3-5.2 5.2v18.7c0 2.9 2.3 5.2 5.2 5.2h18.7c2.8-.1 5.1-2.4 5.1-5.2zM39.2 15v16.1c0 4.5-3.7 8.2-8.2 8.2H14.9c-.6 0-.9.7-.5 1.1 1 1.1 2.4 1.8 4.1 1.8h13.4c5.7 0 10.3-4.6 10.3-10.3V18.5c0-1.6-.7-3.1-1.8-4.1-.5-.4-1.2 0-1.2.6z"></path>\n        </svg>\n    </div>\n    <div class="action stat icon">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n            <title>Down Video</title>\n            <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>\n            <polygon points="9 8 15 12 9 16"></polygon>\n        </svg>\n    </div>\n</div>\n'),
              t.appendChild(e));
            const r = e.querySelectorAll("div.action");
            (r?.[0].addEventListener("click", () => {
              let t = e.getAttribute("data-code"),
                r = J(t);
              if (!teLockOnce(`thumb-img-${t}`, 1200)) return void n("Ignored duplicate click");
              dbg("thumb_click", "image", 59030, {code:t, media:dbgMedia(r)});
              (tePersistDownloadOptionsNow(), r ? r.img_origin && Q(r.img_origin, teMakeDownloadPrefix(r, 0, 1, r.img_origin, teRankNoForMedia(r, 0)), teActiveFolderAccountName(), teBuildMediaDownloadMeta(r, r.img_origin, teMakeDownloadPrefix(r, 0, 1, r.img_origin, teRankNoForMedia(r, 0)), 0, 1)) : n("Image not found."));
            }),
              r?.[1].addEventListener("click", () => {
                let t = e.getAttribute("data-code"),
                  r = J(t);
                if (!teLockOnce(`thumb-carousel-${t}`, 1200)) return void n("Ignored duplicate click");
                if (r) {
                  const e = q(r);
                  dbg("thumb_click", "carousel_entries", 59038, {code:t, entries:e.map(x=>({prefix:x.prefix,url:String(x.url||"").slice(0,180),account:x.account}))});
                  if (e.length > 0) e.forEach((e) => Q(e.url, e.prefix, e.account || teActiveFolderAccountName(), e.meta));
                  else n("No downloadable medias found");
                } else n("Image not found.");
              }),
              r?.[2].addEventListener("click", () => {
                let t = e.getAttribute("data-code"),
                  r = J(t);
                if (!teLockOnce(`thumb-video-${t}`, 1200)) return void n("Ignored duplicate click");
                dbg("thumb_click", "video", 59046, {code:t, media:dbgMedia(r)});
                (tePersistDownloadOptionsNow(), r ? r.video_url && Q(r.video_url, teMakeDownloadPrefix(r, 0, 1, r.video_url, teRankNoForMedia(r, 0)), teActiveFolderAccountName(), teBuildMediaDownloadMeta(r, r.video_url, teMakeDownloadPrefix(r, 0, 1, r.video_url, teRankNoForMedia(r, 0)), 0, 1)) : n("Video not found."));
              }));
          }
        else if (r > i)
          for (; t.children.length > i; ) t.removeChild(t.lastChild);
        const teRankCtx = teBuildRankContext(e);
        teScheduleMainRankLabels(e, teRankCtx);
        const s = `data:image/svg+xml;base64,${btoa('\n<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">\n  <rect width="100%" height="100%" fill="#e0e0e0"/>\n</svg>\n')}`;
        Array.from(t.children).forEach((t, r) => {
          const n = e[r],
            i = t.getAttribute("data-code");
          t.setAttribute("data-code", n.code);
          const o = t.querySelector("a"),
            a = t.querySelector("img"),
            l = t.querySelector(".overlay");
          {
            const e = t.querySelectorAll("div.action");
            (n?.carousel_media?.length > 0
              ? ((e[0].style.display = "none"), (e[1].style.display = "flex"))
              : n?.img_origin
                ? ((e[0].style.display = "flex"), (e[1].style.display = "none"))
                : ((e[0].style.display = "none"),
                  (e[1].style.display = "none")),
              (e[2].style.display = null != n.video_url ? "flex" : "none"), dbg("render", "button_display", 59068, {code:n.code, img:!!n.img_origin, carousel:!!n.carousel_media, video:!!n.video_url, videoButton:e[2].style.display}));
          }
          const teHref = `https://www.instagram.com/p/${n.code}`;
          const teSrc = n?.img_b64 ? n.img_b64 : s;
          if (o.href !== teHref) o.href = teHref;
          if (a.getAttribute("src") !== teSrc) a.src = teSrc;
          const c = t.querySelectorAll(".stat");
          if (
            (te(c, n),
            teApplyViewVisual(t, n),
            teApplySideAccountLabel(c[0], n),
            teApplySideRankLabel(t.querySelector(".igblue-side-rank"), teRankCtx.active ? teRankCtx.map.get(n.code) : 0, teRankCtx.active),
            i &&
              i !== n.code &&
              (l.classList.add("highlight"),
              setTimeout(() => l.classList.remove("highlight"), 500)),
            a && l)
          ) {
            const e = l.offsetHeight;
            a.style.minHeight = e ? `${e}px` : "";
          }
        });
      }
      const te = (e, t) => {
        ((e[1].style.display = null != t.like_count ? "flex" : "none"),
          (e[2].style.display = null != t.comment_count ? "flex" : "none"));
        const r = e[3].style.display,
          n = null != t.media_repost_count ? "flex" : "none";
        ((e[3].style.display = n),
          "flex" !== r &&
            "flex" === n &&
            (e[3].classList.add("highlight"),
            setTimeout(() => e[3].classList.remove("highlight"), 500)));
        const i = e[4].style.display,
          s = null != t.created_at ? "flex" : "none";
        ((e[4].style.display = s),
          "flex" !== i &&
            "flex" === s &&
            (e[4].classList.add("highlight"),
            setTimeout(() => e[4].classList.remove("highlight"), 500)));
        const o = e[5].style.display,
          a = null != t?.play_count ? "flex" : "none";
        ((e[5].style.display = a),
          "flex" !== o &&
            "flex" === a &&
            (e[5].classList.add("highlight"),
            setTimeout(() => e[5].classList.remove("highlight"), 500)));
        const l = e[6].style.display,
          c = null != t?.engagement_rate ? "flex" : "none";
        ((e[6].style.display = c),
          "flex" !== l &&
            "flex" === c &&
            (e[6].classList.add("highlight"),
            setTimeout(() => e[6].classList.remove("highlight"), 500)));
        const u = null != t.like_count ? t.like_count.toLocaleString() : "",
          h = null != t.comment_count ? t.comment_count.toLocaleString() : "",
          f =
            null != t.media_repost_count
              ? t.media_repost_count.toLocaleString()
              : "",
          d = t.created_at ? ce(new Date(1e3 * t.created_at)) : "",
          p = null != t.play_count ? t.play_count.toLocaleString() : "",
          m =
            null != t.play_count
              ? (function (e) {
                  const t = teErPercent(e);
                  if (null == t) return "ER N/A";
                  const r = teErEmojiForMedia(e);
                  return r ? `${r.emoji} ${t.toFixed(2)}%` : `ER ${t.toFixed(2)}%`;
                })(t)
              : "";
        (e[1].querySelector("span").textContent !== u &&
          (e[1].querySelector("span").textContent = u),
          e[2].querySelector("span").textContent !== h &&
            (e[2].querySelector("span").textContent = h),
          e[3].querySelector("span").textContent !== f &&
            (e[3].querySelector("span").textContent = f),
          e[4].querySelector("span").textContent !== d &&
            (e[4].querySelector("span").textContent = d),
          teApplyDateFreshClass(e[4], t),
          teApplyMetricBars(e, t),
          e[5].querySelector("span").textContent !== p &&
            (e[5].querySelector("span").textContent = p),
          e[6].querySelector("span").textContent !== m &&
            (e[6].querySelector("span").textContent = m),
          e[6].querySelector("span").classList.toggle("igblue-er-badge", !!teErEmojiForMedia(t)),
          e[6].querySelector("span").classList.toggle("igblue-er-muted", !teErEmojiForMedia(t)));
      };
      let re = null,
        ne = null;
      function ie(e, t) {
        const r = document.getElementById("filter_status");
        if (e !== re || t !== ne) {
          const n = e !== t ? "red" : "";
          ((r.innerHTML = `Showing <span class="${n}">${e}</span> of ${t} unfiltered items`),
            r.classList.add("highlight"),
            setTimeout(() => r.classList.remove("highlight"), 500),
            (re = e),
            (ne = t));
        }
        I(e, t);
      }
      let se,
        oe = [];
      function ae(e, t, r, i, s) {
        if (e && e.length > 0) {
          let o;
          if (t > 0) {
            let r = {};
            ((o = (function (e, t, r) {
              let n = [];
              return (
                e.forEach((e) => {
                  null == e.created_at
                    ? (r.issue = !0)
                    : e.created_at > t && n.push(e);
                }),
                n
              );
            })(e, t, r)),
              i && r.issue && n("Some Medias do not have Date info"));
          } else o = e;
          let a = {};
          ((oe = (function (e, t, r, n) {
            t >= G.length && (t = 0);
            let i = G[t];
            if ("" == i) return e;
            const s = [...e];
            return (
              s.sort((e, t) => {
                const s = e[i],
                  o = t[i];
                return (
                  !r || (s && o) || (n[i] = !0),
                  void 0 === s && void 0 === o
                    ? 0
                    : void 0 === s
                      ? 1
                      : void 0 === o
                        ? -1
                        : o - s
                );
              }),
              s
            );
          })(o, r, s, a)),
            ee(oe),
            ie(oe.length, e.length),
            teUpdateMetricThresholdCount(),
            s &&
              (a.play_count
                ? n("Some Image do not have Views info")
                : a.created_at
                  ? n("Some Meidas do not have Date info")
                  : a.engagement_rate &&
                    n("ER is calculated for reels that have 'view count'.")));
        } else ((oe = []), ee([]), ie(0, 0), teUpdateMetricThresholdCount());
      }
      async function le(e) {
        return new Promise((t, r) => {
          const n = new Image();
          ((n.onload = () => t({ width: n.width, height: n.height })),
            (n.onerror = r),
            (n.src = e));
        });
      }
      function teSortDanceDebugMedia(m, idx) {
        const code = String(m && m.code || "");
        return {
          idx: Number(idx || 0) + 1,
          code,
          username: teOwnerAccountForMedia ? teOwnerAccountForMedia(m) : String(m && (m.username || m.owner_username) || ""),
          views: teMetricNumber(m && m.play_count),
          likes: teMetricNumber(m && m.like_count),
          comments: teMetricNumber(m && m.comment_count),
          reposts: teMetricNumber(m && m.media_repost_count),
          created_at: Number(m && m.created_at || 0) || 0,
          er: Number(m && m.engagement_rate || 0) || 0,
          has_img: !!(m && (m.img_origin || m.img_small || m.img_b64)),
          has_video: !!(m && m.video_url),
          has_carousel: !!(m && m.carousel_media),
          img_sig: String((m && (m.img_origin || m.img_small || m.img_b64)) || "").slice(0, 90),
          video_sig: String((m && m.video_url) || "").slice(0, 90)
        };
      }
      function teSortDanceDebugDomItems() {
        const box = document.getElementById("img_collection");
        if (!box) return [];
        return Array.from(box.children || []).slice(0, 20).map((el, idx) => {
          let overlay = "";
          try { overlay = String(el && el.innerText || "").replace(/\s+/g, " ").trim().slice(0, 240); } catch (_) {}
          return {
            idx: idx + 1,
            code: String(el && el.getAttribute && el.getAttribute("data-code") || ""),
            img_src_sig: String(el && el.querySelector && el.querySelector("img") && el.querySelector("img").getAttribute("src") || "").slice(0, 90),
            overlay
          };
        });
      }
      function teSortDanceDebugSignature(list) {
        if (!Array.isArray(list)) return "";
        return list.slice(0, 20).map((m, idx) => {
          const field = G[Z] || "";
          const val = field ? teMetricNumber(m && m[field]) : 0;
          return `${idx + 1}:${String(m && m.code || "")}:${val}`;
        }).join("|");
      }
      async function teCopyTextToClipboard(text) {
        const value = String(text || "");
        try {
          if (navigator.clipboard && navigator.clipboard.writeText) {
            await navigator.clipboard.writeText(value);
            return true;
          }
        } catch (_) {}
        try {
          const ta = document.createElement("textarea");
          ta.value = value;
          ta.setAttribute("readonly", "readonly");
          ta.style.position = "fixed";
          ta.style.left = "-9999px";
          ta.style.top = "0";
          document.body.appendChild(ta);
          ta.focus();
          ta.select();
          const ok = document.execCommand("copy");
          document.body.removeChild(ta);
          return !!ok;
        } catch (_) { return false; }
      }
      async function teBuildSortDanceDebugSnapshot(slot) {
        const sortEl = document.getElementById("sort_by");
        const rangeEl = document.getElementById("timeRange");
        const metricInput = document.getElementById("top_n_count");
        const metricCount = document.getElementById("top_metric_count");
        const targetCount = document.getElementById("igblue_auto_py_target_count");
        const rankCtx = teBuildRankContext(Array.isArray(oe) ? oe : []);
        const sortedTop = teAutoPyFilteredSortedTopN ? teAutoPyFilteredSortedTopN(20) : [];
        let loadedState = null, activeTarget = null;
        try { loadedState = await teSendToTargetTabResponse({ type: "igblue_auto_py_loaded_rank_state", wait_ms: 80, requested_no: 1 }, "", 900); } catch (e) { loadedState = { ok: false, error: String(e && e.message || e) }; }
        try { activeTarget = await teSendToTargetTabResponse({ type: "igblue_get_active_download_target" }, "", 900); } catch (e) { activeTarget = { ok: false, error: String(e && e.message || e) }; }
        const out = {
          schema: "igblue_sort_dance_debug_v179a13",
          slot: Number(slot || 0) || 0,
          copied_at: new Date().toISOString(),
          build_version: IGBLUE_BUILD_VERSION,
          build_tag: IGBLUE_BUILD_TAG,
          page: {
            side_href: String(location.href || ""),
            target_url: teCurrentTargetUrl ? teCurrentTargetUrl() : "",
            target_tab_id: Number(teCurrentTargetTabId || 0) || 0,
            channel_locked: teChannelLocked ? teChannelLocked() : false,
            pinned_target_url: String(tePinnedTargetUrl || "")
          },
          controls: {
            sort_index: Number(Z || 0),
            sort_field: String(G[Z] || ""),
            sort_text: sortEl ? String(sortEl.options[sortEl.selectedIndex] && sortEl.options[sortEl.selectedIndex].text || "") : "",
            time_range_days: Number(K || 0) || 0,
            time_range_value: rangeEl ? String(rangeEl.value || "") : "",
            threshold_10k: metricInput ? String(metricInput.value || "") : "",
            threshold_value: teMetricThresholdValue ? teMetricThresholdValue() : 0,
            top_metric_count: metricCount ? String(metricCount.dataset.count || metricCount.textContent || "") : "",
            top_target_count: targetCount ? String(targetCount.value || "") : ""
          },
          counts: {
            se_raw_count: Array.isArray(se) ? se.length : 0,
            oe_render_count: Array.isArray(oe) ? oe.length : 0,
            filtered_source_count: teAutoPyFilteredSourceList ? teAutoPyFilteredSourceList().length : 0,
            dom_thumb_count: (document.getElementById("img_collection") && document.getElementById("img_collection").children || []).length,
            page_info_all: (document.getElementById("page_info_all") || {}).textContent || "",
            page_info_filtered: (document.getElementById("page_info_filtered") || {}).textContent || "",
            filter_status: (document.getElementById("filter_status") || {}).textContent || ""
          },
          signatures: {
            se_first20: teSortDanceDebugSignature(se || []),
            oe_first20: teSortDanceDebugSignature(oe || []),
            sorted_top_first20: teSortDanceDebugSignature(sortedTop || []),
            dom_first20: teSortDanceDebugDomItems().map(x => `${x.idx}:${x.code}:${x.img_src_sig}`).join("|")
          },
          first_items: {
            se: (Array.isArray(se) ? se : []).slice(0, 20).map(teSortDanceDebugMedia),
            oe: (Array.isArray(oe) ? oe : []).slice(0, 20).map(teSortDanceDebugMedia),
            sorted_top: (Array.isArray(sortedTop) ? sortedTop : []).slice(0, 20).map(teSortDanceDebugMedia),
            dom: teSortDanceDebugDomItems()
          },
          rank_context: {
            active: !!(rankCtx && rankCtx.active),
            first10: (Array.isArray(oe) ? oe : []).slice(0, 10).map((m, idx) => ({ idx: idx + 1, code: String(m && m.code || ""), rank_no: rankCtx && rankCtx.map instanceof Map ? Number(rankCtx.map.get(m && m.code) || 0) : idx + 1 }))
          },
          content: {
            loaded_state: loadedState,
            active_target: activeTarget
          },
          note: "약 1초 간격으로 춤출 때 1번→1초후 2번→1초후 3번을 눌러 세 결과를 전달"
        };
        return JSON.stringify(out, null, 2);
      }
      async function teCopySortDanceDebug(slot) {
        const btn = document.getElementById(`igblue_sort_dance_copy_${slot}`);
        const old = btn ? btn.textContent : "";
        try {
          if (btn) { btn.disabled = true; btn.textContent = "복사중..."; }
          const text = await teBuildSortDanceDebugSnapshot(slot);
          const ok = await teCopyTextToClipboard(text);
          n(ok ? `춤춤진단${slot} 복사 완료` : `춤춤진단${slot} 복사 실패`);
          try { const st = document.getElementById("igblue_devtools_status"); if (st) st.textContent = ok ? `춤춤진단${slot} 클립보드 복사 완료` : `춤춤진단${slot} 복사 실패`; } catch (_) {}
        } catch (e) {
          n(`춤춤진단${slot} 예외`);
          try { const st = document.getElementById("igblue_devtools_status"); if (st) st.textContent = `춤춤진단${slot} 예외: ${String(e && e.message || e)}`; } catch (_) {}
        } finally {
          if (btn) { btn.disabled = false; btn.textContent = old || `춤춤복사${slot}`; }
        }
      }
      function teBindSortDanceDebugButtons() {
        [1, 2, 3].forEach((slot) => {
          const btn = document.getElementById(`igblue_sort_dance_copy_${slot}`);
          if (!btn || btn.__igblue_sort_dance_bound) return;
          btn.__igblue_sort_dance_bound = true;
          btn.addEventListener("click", () => teCopySortDanceDebug(slot));
        });
      }
      function ce(e) {
        return `${e.getFullYear()}-${String(e.getMonth() + 1).padStart(2, "0")}-${String(e.getDate()).padStart(2, "0")}`;
      }
      function teApplyDateFreshClass(e, t) {
        if (!e) return;
        e.classList.remove("igblue-date-today", "igblue-date-week", "igblue-date-month", "igblue-date-quarter", "igblue-date-border");
        e.style.removeProperty("--igblue-date-color");
        e.style.removeProperty("--igblue-date-border-width");
        const r = Number(t?.created_at || 0);
        if (!Number.isFinite(r) || r <= 0) return;
        const n = teDateBorderSettings();
        if (!n.enabled) return;
        const i = new Date(), s = new Date(1e3 * r);
        i.setHours(0, 0, 0, 0);
        s.setHours(0, 0, 0, 0);
        const o = Math.floor((i.getTime() - s.getTime()) / 86400000), a = teDateLevelByAge(o, n.levels || {});
        if (!a || !a.enabled || !a.side) return;
        e.classList.add("igblue-date-border");
        e.style.setProperty("--igblue-date-color", teSafeHexColor(a.color, "#22c55e"));
        e.style.setProperty("--igblue-date-border-width", `${Math.max(1, Math.min(9, Number(n.borderWidth) || 2))}px`);
      }

      // IGblue v179a14: Auto8700 예약 실행 요청 처리. 실제 루프는 기존 teAutoPyStartOnceFromUi()만 재사용한다.
      let teAuto8700LastStartRequestId = "";
      async function teAuto8700BringTargetAfterMouseIdle(req) {
        try {
          const d = req && typeof req === "object" ? req : {};
          const shouldActivate = !!(d.cdp_activate_before_start || /schedule|auto8700/i.test(String(d.reason || d.source || "")));
          if (!shouldActivate) return { ok: true, skipped: true, reason: "not_schedule_activation" };
          const r = await fetch("http://127.0.0.1:8707/api/cdp/ig/bring_to_front", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ reason: "auto8700_schedule", request_id: String(d.request_id || ""), timeout: 3.0 })
          });
          const j = await r.json().catch(() => ({}));
          if (!r.ok || j.ok === false) return { ok: false, error: j && (j.error || j.message) || ("HTTP " + r.status), result: j };
          return Object.assign({ ok: true }, j || {});
        } catch (err) {
          return { ok: false, error: String(err && err.message || err) };
        }
      }
      function teHandleAuto8700StartRequest(req, sendResponse) {
        try {
          const requestId = String(req && req.request_id || ("auto8700_side_" + Date.now()));
          if (requestId && requestId === teAuto8700LastStartRequestId) {
            try { sendResponse && sendResponse({ status: "duplicate", request_id: requestId }); } catch (_) {}
            return true;
          }
          teAuto8700LastStartRequestId = requestId;
          try { chrome.storage && chrome.storage.local && chrome.storage.local.set({ "IGBLUE_AUTO8700_START_REQUEST_LAST_CONSUMED": requestId }); } catch (_) {}
          const cb = document.getElementById("igblue_auto_py_down");
          if (cb && !cb.checked) {
            cb.checked = true;
            try { teLocalSet("download/auto_py", "1"); } catch (_) {}
          }
          n("Auto8700 예약 실행 요청 수신");
          setTimeout(() => {
            (async () => {
              try {
                const bring = await teAuto8700BringTargetAfterMouseIdle(req || {});
                if (bring && bring.ok === false) { try { n("CDP 탭 이동 보류/실패: " + String(bring.error || bring.reason || "unknown")); } catch (_) {} }
                try { await teAutoPyStartOnceFromUi(); } catch (err) { try { n("Auto8700 예약 실행 실패: " + String(err && err.message || err)); } catch (_) {} }
              } catch (err) {
                try { n("Auto8700 예약 실행 예외: " + String(err && err.message || err)); } catch (_) {}
              }
            })();
          }, 80);
          try { sendResponse && sendResponse({ status: "ok", started: true, request_id: requestId, cdp_tab_activation_policy: "direct_schedule_activation" }); } catch (_) {}
        } catch (err) {
          try { sendResponse && sendResponse({ status: "error", error: String(err && err.message || err) }); } catch (_) {}
        }
        return true;
      }
      function teMaybeConsumePendingAuto8700Start() {
        try {
          chrome.storage && chrome.storage.local && chrome.storage.local.get(["IGBLUE_AUTO8700_START_REQUEST", "IGBLUE_AUTO8700_START_REQUEST_LAST_CONSUMED"], (got) => {
            try {
              const req = got && got["IGBLUE_AUTO8700_START_REQUEST"];
              const consumed = String(got && got["IGBLUE_AUTO8700_START_REQUEST_LAST_CONSUMED"] || "");
              if (!req || typeof req !== "object") return;
              const rid = String(req.request_id || "");
              if (rid && rid === consumed) return;
              const age = Math.max(0, Date.now() - Number(req.ts || Date.parse(req.bg_received_at || "") || 0));
              if (age > 10 * 60 * 1000) return;
              teHandleAuto8700StartRequest(req, null);
            } catch (_) {}
          });
        } catch (_) {}
      }

      document.addEventListener("DOMContentLoaded", function () {
        (H(),
          tePruneTabSessions(),
          teUpdateTabIdBadge(),
          teMaybeConsumePendingAuto8700Start(),
          teBindSortDanceDebugButtons(),
          teApplyThumbWidth(teThumbWidthValue()),
          chrome.runtime.onMessage.addListener(function (e, t, r) {
            dbg("runtime_onMessage", "recv", 59257, {type:e&&e.type, info_count:e&&Array.isArray(e.info)?e.info.length:null, status:e&&e.status, file:e&&e.file, percent:e&&e.percent, message:e&&e.message});
            "IGBLUE_WINDOW_MOVE_STATE" === e.type
              ? (teAutoPyHandleWindowMoveState(e).catch(()=>{}), r({status:"ok"}))
              : "IGBLUE_AUTO8700_START_REQUEST" === e.type
              ? teHandleAuto8700StartRequest(e, r)
              : "ndy_side_shop" === e.type
              ? (() => {
                  const guard = teShouldAcceptSideShopPayload(e);
                  if (!(guard && guard.ok)) {
                    try { localStorage.setItem("IG_dbg/side.js/ndy_side_shop_ignored", JSON.stringify(Object.assign({ts:new Date().toISOString(), info_count:Array.isArray(e.info)?e.info.length:0}, guard || {}))); } catch (_) {}
                    r({ status: "ignored", reason: guard && guard.reason || "payload_guard" });
                    return;
                  }
                  if (!teChannelLocked() && Number(e.tabId || 0)) teCurrentTargetTabId = Number(e.tabId || 0);
                  u || ((u = !0), P());
                  c && clearTimeout(c);
                  c = setTimeout(F, 2e3);
                  se = e.info;
                  teUpdateDownloadFolderPath(teActiveFolderAccountName());
                  teTouchTabSession(!!(teAutoPyRunning && !teAutoPyStopping), "ndy_side_shop_accept");
                  ae(e.info, K, Z, !1, !1);
                  r({ status: "ok" });
                })()
              : "IGBLUE_TARGET_UPDATED" === e.type
                ? ((teChannelLocked() && tePinnedTargetUrl) || (teCurrentTargetTabId = Number(e.tabId || 0)),
                  teSetTargetLabel(e),
                  teIsPopupMode() && (!(teChannelLocked() && tePinnedTargetUrl && String(e.url || "") !== tePinnedTargetUrl)) && teApplyTargetInfo(e),
                  r({ status: "ok" }))
                : "ndy_dq" === e.type
                  ? ((d = Number(e.count) || 0), N(), r({ status: "ok" }))
                  : "ndy_down_progress" === e.type &&
                    (teHandleDownProgress(e), r({ status: "ok" }));
          }),
          teRestoreLastInfoForPopup());
      });
  })();
})();



// IGblue v86 patch: side-panel TTS level selector.
// - 0 미사용 선택을 막지 않는다.
// - 사용자가 select를 건드린 뒤에는 늦게 도착한 init/storage 값이 다시 덮지 않는다.
// - PY TTS는 py 설정창에서 별도 관리한다.
(() => {
  if (window.__IGBLUE_SIDE_TTS_LEVEL_PATCH_V86__) return;
  window.__IGBLUE_SIDE_TTS_LEVEL_PATCH_V86__ = true;
  const STORE_KEYS = ["IGBLUE/v79/tts_level", "IGBLUE/tts_level", "igblue_tts_level"];
  const labels = { "0": "0 미사용", "1": "1 중요", "2": "2 진행", "3": "3 디버그" };
  let userTouched = false;
  let bound = false;
  const clamp = (v) => {
    const n = Number(v);
    if (!Number.isFinite(n)) return "0";
    return String(Math.max(0, Math.min(3, Math.floor(n))));
  };
  function setStatus(msg) {
    try {
      const el = document.getElementById("igblue_down_txt_status");
      if (el && msg) el.textContent = "down.txt: " + msg;
    } catch (_) {}
  }
  function cancelSpeechIfZero(level) {
    if (String(level) !== "0") return;
    try { if (window.speechSynthesis) window.speechSynthesis.cancel(); } catch (_) {}
  }
  function getTargetTabId(cb) {
    try {
      chrome.runtime.sendMessage({ type: "IGBLUE_GET_TARGET_STATE" }, (res) => {
        if (chrome.runtime.lastError || !res) return cb(0);
        const id = Number(res.tabId || 0);
        cb(Number.isFinite(id) && id > 0 ? id : 0);
      });
    } catch (_) { cb(0); }
  }
  function ensureAndSend(tabId, msg, cb) {
    if (!tabId) return cb && cb(false);
    try {
      chrome.tabs.sendMessage(tabId, msg, () => {
        if (!chrome.runtime.lastError) return cb && cb(true);
        try {
          chrome.runtime.sendMessage({ type: "IGBLUE_ENSURE_CONTENT", tabId }, (res) => {
            if (chrome.runtime.lastError || !res || res.status !== "ok") return cb && cb(false);
            chrome.tabs.sendMessage(tabId, msg, () => cb && cb(!chrome.runtime.lastError));
          });
        } catch (_) { cb && cb(false); }
      });
    } catch (_) { cb && cb(false); }
  }
  function sendToInstagram(level, reason) {
    const msg = {
      type: "igblue_set_tts_level",
      value: { tts_level: Number(level), label: labels[String(level)] || String(level), reason: reason || "side", ts: Date.now() }
    };
    getTargetTabId((tabId) => {
      if (!tabId) return setStatus("TTS " + (labels[String(level)] || level) + " / Instagram tab 없음");
      ensureAndSend(tabId, msg, (ok) => {
        setStatus(ok ? ("TTS " + (labels[String(level)] || level) + " 전달") : "TTS 전달 실패");
      });
    });
  }
  function saveLevel(level, reason) {
    level = clamp(level);
    try {
      const data = {};
      STORE_KEYS.forEach((k) => { data[k] = level; });
      chrome.storage.local.set(data, () => { chrome.runtime.lastError; });
    } catch (_) {}
    try { localStorage.setItem("IGBLUE/v79/tts_level", level); } catch (_) {}
    cancelSpeechIfZero(level);
    sendToInstagram(level, reason || "change");
  }
  function applySelectValue(sel, level, reason, fromUser) {
    const v = clamp(level);
    if (!fromUser && userTouched) return;
    if (sel.value !== v) sel.value = v;
    saveLevel(v, reason);
  }
  function init() {
    const sel = document.getElementById("igblue_side_tts_level");
    if (!sel) return;
    sel.disabled = false;
    sel.style.pointerEvents = "auto";
    sel.style.userSelect = "auto";
    sel.querySelectorAll("option").forEach((op) => { try { op.disabled = false; } catch (_) {} });
    if (bound) return;
    bound = true;
    ["pointerdown", "mousedown", "touchstart", "keydown"].forEach((name) => {
      sel.addEventListener(name, (ev) => {
        userTouched = true;
        ev.stopPropagation();
      }, true);
    });
    sel.addEventListener("click", (ev) => { userTouched = true; ev.stopPropagation(); }, true);
    sel.addEventListener("change", () => {
      userTouched = true;
      applySelectValue(sel, sel.value, "user_change", true);
    });
    try {
      chrome.storage.local.get(["IGBLUE/v81/tts_default0_done"].concat(STORE_KEYS), (got) => {
        if (userTouched) return;
        if (!got || got["IGBLUE/v81/tts_default0_done"] !== "1") {
          try { chrome.storage.local.set({"IGBLUE/v81/tts_default0_done":"1"}, () => { chrome.runtime.lastError; }); } catch (_) {}
          return applySelectValue(sel, "0", "init_default_v86", false);
        }
        const saved = got && (got[STORE_KEYS[0]] ?? got[STORE_KEYS[1]] ?? got[STORE_KEYS[2]]);
        applySelectValue(sel, saved == null || saved === "" ? "0" : saved, "init", false);
      });
    } catch (_) { applySelectValue(sel, "0", "init_fallback", false); }
  }
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
  else init();
  setTimeout(init, 300);
  setTimeout(init, 1000);
})();


// IGblue v172 patch: Auto8700 8707 DevTools settings bridge.
(() => {
  if (window.__IGBLUE_AUTO8700_DEVTOOLS_SETTINGS_V172__) return;
  window.__IGBLUE_AUTO8700_DEVTOOLS_SETTINGS_V172__ = true;
  const KEYS = ["devtools_network_enabled", "devtools_console", "devtools_file_log", "devtools_capture_body"];
  const IDS = { devtools_network_enabled: "igblue_devtools_network_enabled", devtools_console: "igblue_devtools_console", devtools_file_log: "igblue_devtools_file_log", devtools_capture_body: "igblue_devtools_capture_body" };
  const DEFAULTS = { devtools_network_enabled: false, devtools_console: true, devtools_file_log: false, devtools_capture_body: false };
  function $(id){ return document.getElementById(id); }
  function setStatus(text){ try { const el = $("igblue_devtools_status"); if (el) el.textContent = text; } catch(_) {} }
  function readUi(){ const out = {}; for (const k of KEYS) { const el = $(IDS[k]); out[k] = !!(el && el.checked); } return out; }
  function applyUi(values){ for (const k of KEYS) { const el = $(IDS[k]); if (el) el.checked = !!(values && values[k]); } }
  function save(reason){
    const settings = readUi();
    settings.ig_manual_click_mode_enabled = !settings.devtools_network_enabled;
    const payload = Object.assign({}, settings, { ig_auto8700_devtools_settings: settings, ig_auto8700_devtools_settings_updated_at: Date.now(), ig_auto8700_devtools_settings_reason: reason || "side_change" });
    try { chrome.storage.local.set(payload, () => { const err = chrome.runtime.lastError; setStatus(err ? ("8707 설정 저장 실패: " + err.message) : "8707 설정 저장/전송 대기 완료"); }); }
    catch (e) { setStatus("8707 설정 저장 예외: " + String(e && e.message || e)); }
  }
  function init(){
    if (!$(IDS.devtools_network_enabled)) return;
    try { chrome.storage.local.get(KEYS.concat(["ig_auto8700_devtools_settings"]), (got) => { const merged = Object.assign({}, DEFAULTS, (got && got.ig_auto8700_devtools_settings) || {}, got || {}); applyUi(merged); setStatus("8707 설정 준비됨"); }); }
    catch (_) { applyUi(DEFAULTS); }
    for (const k of KEYS) { const el = $(IDS[k]); if (el && !el.__igblue_auto8700_bound) { el.__igblue_auto8700_bound = true; el.addEventListener("change", () => save("user_change")); } }
  }
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); else init();
  setTimeout(init, 500);
})();

// IGblue v179a13 patch: lightweight fold panels for side 공간 활용.
(() => {
  if (window.__IGBLUE_FOLD_PANELS_V179A13__) return;
  window.__IGBLUE_FOLD_PANELS_V179A13__ = true;
  const PREFIX = "IGBLUE/fold/";
  function key(el) {
    return PREFIX + String(el && (el.dataset.igblueFold || el.id) || "unknown");
  }
  function updateIcon(el) {
    try {
      const icon = el && el.querySelector && el.querySelector("[data-fold-icon]");
      if (icon) icon.textContent = el.open ? "▣" : "□";
    } catch (_) {}
  }
  function bindOne(el) {
    if (!el || el.__igblueFoldBound) return;
    el.__igblueFoldBound = true;
    try {
      const saved = localStorage.getItem(key(el));
      if (saved === "1" || saved === "0") el.open = saved === "1";
      else el.open = String(el.dataset.defaultOpen || "0") === "1";
    } catch (_) {
      el.open = String(el.dataset.defaultOpen || "0") === "1";
    }
    updateIcon(el);
    el.addEventListener("toggle", () => {
      try { localStorage.setItem(key(el), el.open ? "1" : "0"); } catch (_) {}
      updateIcon(el);
    });
    try {
      const summary = el.querySelector("summary");
      if (summary) summary.querySelectorAll("input, button, select, textarea, a").forEach((child) => {
        ["click", "mousedown", "pointerdown", "keydown"].forEach((name) => child.addEventListener(name, (ev) => ev.stopPropagation(), true));
      });
    } catch (_) {}
  }
  function init() {
    try { document.querySelectorAll("details[data-igblue-fold]").forEach(bindOne); } catch (_) {}
  }
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
  else init();
  setTimeout(init, 300);
  setTimeout(init, 1000);
})();

// IGblue v179a37: Auto8700 two-slot schedule UI. CDP is schedule-only; checked slots only run.
// Auto8700/8707가 살아 있으면 예약 저장/카운트다운을 유지하되, CDP:O일 때만 실제 예약 실행을 허용한다.
(() => {
  if (window.__IGBLUE_AUTO8700_SCHEDULE_UI_V188__) return;
  window.__IGBLUE_AUTO8700_SCHEDULE_UI_V188__ = true;
  const AUTO_URL = "http://127.0.0.1:8707";
  const $ = (id) => document.getElementById(id);
  let lastStatusOk = false;
  let cdpReady = false;
  let schedule = { schedule1_enabled:false, schedule1_time:"00:00", schedule2_enabled:false, schedule2_time:"00:00", revision:0, updated_at:"", updated_source:"" };
  let timeFocused = false;
  let scheduleDirty = false;
  let scheduleSaving = false;
  let scheduleLoaded = false;

  function pad2(n){ return String(Math.max(0, Math.min(99, Number(n)||0))).padStart(2,"0"); }
  function normalizeTime(v){
    const raw = String(v || "00:00").trim();
    const m = raw.match(/^(\d{1,2})(?::(\d{1,2}))?(?::(\d{1,2}))?$/);
    if (!m) return "00:00";
    const hh = Math.max(0, Math.min(23, parseInt(m[1]||"0",10)));
    const mm = Math.max(0, Math.min(59, parseInt(m[2]||"0",10)));
    return pad2(hh)+":"+pad2(mm);
  }
  function todayKey(d){ const x=d||new Date(); return x.getFullYear()+"-"+pad2(x.getMonth()+1)+"-"+pad2(x.getDate()); }
  function nextAt(timeText){
    const now = new Date(); const t = normalizeTime(timeText).split(":").map(Number);
    const target = new Date(now); target.setHours(t[0]||0, t[1]||0, 0, 0);
    if (target <= now) target.setDate(target.getDate()+1);
    return target;
  }
  function remainingText(ms){
    const sec = Math.max(0, Math.floor(ms/1000));
    return String(Math.floor(sec/3600)).padStart(2,"0")+"시간 "+String(Math.floor((sec%3600)/60)).padStart(2,"0")+"분 "+String(sec%60).padStart(2,"0")+"초";
  }
  function setStatus(text){ const el=$("igblue_auto8700_schedule_status"); if(el) el.textContent=text||""; }
  function setCdpBrief(on){ /* side CDP 문구 제거. IG 화면 CDP:X/O 게이트만 사용한다. */ }
  function setCountdown(text){ const el=$("igblue_auto8700_schedule_countdown"); if(el) el.textContent=text||""; }
  function setScheduleVisible(on){
    const el=$("igblue_auto8700_schedule_row");
    if(el){ el.classList.toggle("is-cdp-unavailable", !on); el.style.display = "flex"; }
  }
  function scheduleAccountContext(){
    try {
      const st = (typeof teAutoPyAccountQueueStatus === "function") ? teAutoPyAccountQueueStatus() : null;
      return {
        account_queue_disabled: (typeof teAutoPyAccountQueueDisabled === "function") ? !!teAutoPyAccountQueueDisabled() : false,
        account_queue_index: st && st.has ? Number(st.index || 0) : 0,
        account_queue_total: st && st.has ? Number(st.total || 0) : 0,
        account_queue_url: st && st.has ? String(st.url || "") : "",
        account_queue_current_page: !!(st && st.current_page)
      };
    } catch (_) { return {account_queue_disabled:false, account_queue_index:0, account_queue_total:0}; }
  }
  function scheduleInputs(){
    return { e1: $("igblue_auto8700_schedule1_enabled"), t1: $("igblue_auto8700_schedule1_time"), e2: $("igblue_auto8700_schedule2_enabled"), t2: $("igblue_auto8700_schedule2_time"), save: $("igblue_auto8700_schedule_save") };
  }
  function setEnabled(on){
    ["igblue_auto8700_schedule1_enabled","igblue_auto8700_schedule1_time","igblue_auto8700_schedule2_enabled","igblue_auto8700_schedule2_time","igblue_auto8700_schedule_save"].forEach((id)=>{ const el=$(id); if(el) el.disabled=!on; });
  }
  function normalizeSchedule(sc){
    const raw = sc || {};
    const hasNew = Object.prototype.hasOwnProperty.call(raw, "schedule1_enabled") || Object.prototype.hasOwnProperty.call(raw, "schedule2_enabled");
    return {
      schedule1_enabled: hasNew ? !!raw.schedule1_enabled : !!raw.enabled,
      schedule1_time: normalizeTime(raw.schedule1_time || raw.time || "00:00"),
      schedule2_enabled: !!raw.schedule2_enabled,
      schedule2_time: normalizeTime(raw.schedule2_time || "00:00"),
      enabled: !!(hasNew ? (raw.schedule1_enabled || raw.schedule2_enabled) : raw.enabled),
      time: normalizeTime(raw.schedule1_time || raw.time || "00:00"),
      revision: Math.max(0, Number(raw.revision || 0) || 0),
      updated_at: String(raw.updated_at || ""),
      updated_source: String(raw.updated_source || ""),
      last_run_date: String(raw.last_run_date || ""),
      last_run_date_1: String(raw.last_run_date_1 || ""),
      last_run_date_2: String(raw.last_run_date_2 || "")
    };
  }
  function applyScheduleToUi(sc, force){
    const incoming = normalizeSchedule(sc);
    schedule = Object.assign({}, schedule, incoming);
    if (!force && (scheduleDirty || scheduleSaving || timeFocused)) return false;
    const i = scheduleInputs();
    if (i.e1) i.e1.checked = !!schedule.schedule1_enabled;
    if (i.e2) i.e2.checked = !!schedule.schedule2_enabled;
    if (i.t1) i.t1.value = normalizeTime(schedule.schedule1_time || "00:00");
    if (i.t2) i.t2.value = normalizeTime(schedule.schedule2_time || "00:00");
    scheduleDirty = false;
    scheduleLoaded = true;
    return true;
  }
  function markScheduleDirty(){
    scheduleDirty = true;
    const i = scheduleInputs();
    if (i.save) i.save.disabled = !lastStatusOk || scheduleSaving;
    setStatus("예약 수정중 · 예약저장 필요");
  }
  function currentSettings(extra){
    const i = scheduleInputs();
    const s1 = normalizeTime(i.t1 && i.t1.value || schedule.schedule1_time || schedule.time || "00:00");
    const s2 = normalizeTime(i.t2 && i.t2.value || schedule.schedule2_time || "00:00");
    const e1 = !!(i.e1 && i.e1.checked), e2 = !!(i.e2 && i.e2.checked);
    return Object.assign({ enabled: !!(e1 || e2), time: s1, schedule1_enabled: e1, schedule1_time: s1, schedule2_enabled: e2, schedule2_time: s2, ig_side_heartbeat: true }, scheduleAccountContext(), extra || {});
  }
  function activeSlots(){
    const s = normalizeSchedule(schedule);
    const rows = [{id:1, enabled:!!s.schedule1_enabled, time:normalizeTime(s.schedule1_time)}, {id:2, enabled:!!s.schedule2_enabled, time:normalizeTime(s.schedule2_time)}];
    const seen = new Set(), out=[];
    for (const row of rows) { if (!row.enabled) continue; if (seen.has(row.time)) continue; seen.add(row.time); out.push(row); }
    return out;
  }
  async function fetchStatus(forceApply){
    try{
      const r = await fetch(AUTO_URL + "/status", {cache:"no-store"});
      if(!r.ok) throw new Error("HTTP "+r.status);
      const j = await r.json();
      lastStatusOk = true;
      const storage = j.schedule_storage || {};
      cdpReady = !!storage.cdp_ready;
      const applied = applyScheduleToUi(j.auto_schedule || {}, !!forceApply || !scheduleLoaded);
      try {
        chrome.storage.local.set({"IGBLUE_AUTO8700_SCHEDULE": Object.assign({schema:"AUTO8700_RECEIVED_ONLY", source:"Auto8700"}, normalizeSchedule(j.auto_schedule || {}), {cdp_ready:cdpReady, auto8700_status_at:Date.now()})});
      } catch(_) {}
      setEnabled(lastStatusOk); setScheduleVisible(cdpReady); setCdpBrief(cdpReady);
      if (scheduleDirty || scheduleSaving || timeFocused) setStatus(scheduleSaving ? "예약 저장중" : "예약 수정중 · 예약저장 필요");
      else setStatus(cdpReady ? (storage.ig_side_active ? "IG 담당" : "예약 준비") : "예약 저장 가능");
      return applied;
    }catch(e){ lastStatusOk = false; cdpReady = false; setEnabled(false); setScheduleVisible(false); setCdpBrief(false); setStatus("Auto8700 XX"); setCountdown("CDP 대기"); return false; }
  }
  async function saveScheduleFromUi(){
    if (scheduleSaving || !lastStatusOk) return false;
    const settings = currentSettings({save_source:"ig_side"});
    scheduleSaving = true;
    setEnabled(false);
    setStatus("예약 저장중");
    try{
      const r = await fetch(AUTO_URL + "/schedule", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({act:"auto8700_schedule_save", settings})});
      const j = await r.json().catch(()=>({}));
      if(!r.ok || j.ok === false) throw new Error(j.error || ("HTTP "+r.status));
      scheduleDirty = false;
      applyScheduleToUi(j.auto_schedule || {}, true);
      cdpReady = !!(j.cdp_ready || (j.schedule_state && j.schedule_state.cdp_ready));
      try { chrome.storage.local.set({"IGBLUE_AUTO8700_SCHEDULE": Object.assign({schema:"AUTO8700_RECEIVED_ONLY", source:"Auto8700"}, normalizeSchedule(j.auto_schedule || {}), {cdp_ready:cdpReady, auto8700_status_at:Date.now()})}); } catch(_) {}
      setStatus(cdpReady ? "예약 저장됨" : "저장됨");
      return true;
    }catch(e){
      setStatus("예약 저장 실패: "+String(e && e.message || e));
      scheduleSaving = false;
      await fetchStatus(true);
      return false;
    }finally{
      scheduleSaving = false;
      setEnabled(lastStatusOk);
      setScheduleVisible(cdpReady);
      setCdpBrief(cdpReady);
    }
  }
  async function postScheduleMeta(act, extra){
    const settings = Object.assign({ig_side_heartbeat:true}, scheduleAccountContext(), extra || {});
    try{
      const r = await fetch(AUTO_URL + "/schedule", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({act, settings})});
      const j = await r.json().catch(()=>({}));
      if (r.ok && j && j.auto_schedule) {
        schedule = Object.assign({}, schedule, normalizeSchedule(j.auto_schedule));
        try { chrome.storage.local.set({"IGBLUE_AUTO8700_SCHEDULE": Object.assign({schema:"AUTO8700_RECEIVED_ONLY", source:"Auto8700"}, normalizeSchedule(j.auto_schedule), {cdp_ready:!!j.cdp_ready, auto8700_status_at:Date.now()})}); } catch(_) {}
      }
      return !!(r.ok && j.ok !== false);
    }catch(_){ return false; }
  }
  async function heartbeat(){
    if (!lastStatusOk) return;
    await postScheduleMeta("auto8700_schedule_heartbeat", {ig_side_heartbeat:true});
  }
  function requestAutoStartFromIg(slot){
    const requestId = "ig_schedule_" + Date.now() + "_" + Math.floor(Math.random()*100000);
    try { chrome.runtime.sendMessage(Object.assign({type:"IGBLUE_AUTO8700_START_REQUEST", source:"IG_SIDE_SCHEDULE", reason:"ig_side_schedule", request_id:requestId, open_side:false, schedule_slot_id:slot && slot.id, schedule_time:slot && slot.time, ts:Date.now()}, scheduleAccountContext()), ()=>{ chrome.runtime.lastError; }); } catch(_) {}
    const slotId = Number(slot && slot.id || 0) || 0;
    if (slotId === 1 || slotId === 2) schedule["last_run_date_" + slotId] = todayKey(new Date());
    postScheduleMeta("auto8700_schedule_ig_run_mark", {mark_run_today:true, mark_run_slot_id:slotId, mark_run_time:slot && slot.time, ig_side_heartbeat:true});
    setStatus("예약" + (slot && slot.id || "") + " 시간 도달: IG 자동시작 요청");
  }
  function tick(){
    if (!lastStatusOk) { setCountdown("Auto8700 대기"); return; }
    const slots = activeSlots();
    if (!slots.length) { setCountdown("예약 꺼짐"); return; }
    if (!cdpReady) { setCountdown("CDP 대기"); return; }
    const now = new Date(); let next = null;
    for (const slot of slots) {
      const target = nextAt(slot.time); if (!next || target < next.target) next = {slot, target};
      const parts = normalizeTime(slot.time).split(":").map(Number); const due = new Date(now); due.setHours(parts[0]||0, parts[1]||0, 0, 0);
      const delta = now.getTime() - due.getTime();
      const last = String(schedule["last_run_date_" + Number(slot.id || 0)] || schedule.last_run_date || "");
      if (delta >= 0 && delta < 90000 && last !== todayKey(now)) requestAutoStartFromIg(slot);
    }
    if (next) setCountdown("예약" + next.slot.id + " " + remainingText(next.target.getTime() - now.getTime()));
  }
  function bind(){
    const i = scheduleInputs(); if (!i.e1 || i.e1.__igblueBound) return; i.e1.__igblueBound = true;
    [i.e1, i.e2].forEach((el)=>{ if(el) el.addEventListener("change", markScheduleDirty); });
    [i.t1, i.t2].forEach((el)=>{ if(!el) return; el.addEventListener("focus", ()=>{timeFocused=true;}); el.addEventListener("input", markScheduleDirty); el.addEventListener("change", markScheduleDirty); el.addEventListener("blur", ()=>{timeFocused=false; el.value=normalizeTime(el.value);}); });
    i.save && i.save.addEventListener("click", saveScheduleFromUi);
    fetchStatus(); setInterval(fetchStatus, 3000); setInterval(heartbeat, 5000); setInterval(tick, 1000);
  }
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", bind); else bind();
  setTimeout(bind, 500);
})();
