/* 照野 — 地图引擎（MapLibre GL）
   真实暗色矢量底图 + 自定义显影 / 道标 / 锚点光层。
   暖调暗房显影为默认；冷调命运星图为可切换变体。 */

const PAL = {
  warm: { glow: "#ffb45c", glowSoft: "#ff8a2c", core: "#fff0d6", route: "#ffce85",
          photo: "#ffe2b0", waypoint: "#9cc0de", anchor: "#eaf2ff", her: "#d98ad0",
          base: { sat: -0.55, hue: -10, bmax: 0.72, op: 0.9 } },
  cool: { glow: "#7fc8ff", glowSoft: "#3f9bff", core: "#eaf6ff", route: "#9fdcff",
          photo: "#cdeeff", waypoint: "#cdbf9c", anchor: "#fbf6ec", her: "#b89bff",
          base: { sat: -0.25, hue: 14, bmax: 0.78, op: 0.95 } },
};

const ZyMap = (() => {
  let map = null, pal = PAL.warm, palName = "warm", raf = 0, t0 = 0;
  let onPlaceCb = null, mode = "world";
  const D = () => window.ZY_DATA;

  // —— 照片显影：运行时注入的真实地点 ——
  let impPlaces = [], impPhotos = [], ignites = [], igniteActive = false;
  const removedWp = new Set();
  const easeOut = (k) => 1 - Math.pow(1 - k, 3);

  // ---- geojson helpers ----
  const fc = (feats) => ({ type: "FeatureCollection", features: feats });
  const pt = (c, props) => ({ type: "Feature", properties: props, geometry: { type: "Point", coordinates: c } });

  function pushImported() {
    if (!map.getSource("imported")) return;
    map.getSource("imported").setData(fc(impPlaces.map(p =>
      pt(p.c, { id: p.id, place: p.id, b: p.bDisp || 0, scope: p.scope }))));
    map.getSource("importedPhotos").setData(fc(impPhotos.map(p =>
      pt(p.c, { id: p.id, place: p.place, op: p.opDisp || 0 }))));
  }

  function buildSources() {
    const d = D();
    map.addSource("cities", { type: "geojson", data: fc(d.developedCities.map(c => pt(c.c, { b: c.b, name: c.name, home: !!c.home }))) });
    map.addSource("her", { type: "geojson", data: fc(d.herCities.map(c => pt(c.c, { b: c.b, name: c.name }))) });
    map.addSource("routes", { type: "geojson", data: d.routes });
    map.addSource("photos", { type: "geojson", data: fc(d.photoPoints.map(p => pt(p.c, { id: p.id, place: p.place }))) });
    map.addSource("anchors", { type: "geojson", data: fc(d.anchors.map(a => pt(a.c, { id: a.id, name: a.name, place: a.place, saved: a.saved ? 1 : 0 }))) });
    map.addSource("waypoints", { type: "geojson", data: fc(d.worldWaypoints.map(w => pt(w.c, { id: w.id, name: w.name, strength: w.strength }))) });
    map.addSource("citywp", { type: "geojson", data: fc([pt(d.cityWaypoint.c, { name: d.cityWaypoint.name })]) });
    map.addSource("now", { type: "geojson", data: fc([pt(d.anchorNow.c, { label: d.anchorNow.label })]) });

    // 照片显影注入层（初始为空，由 develop.jsx 填充）
    map.addSource("imported", { type: "geojson", data: fc([]) });
    map.addSource("importedPhotos", { type: "geojson", data: fc([]) });
    map.addSource("ignite", { type: "geojson", data: fc([]) });

    // companion combined
    const me = new Map(d.developedCities.map(c => [c.id, c]));
    const her = new Map(d.herCities.map(c => [c.id, c]));
    const comp = [];
    her.forEach((c, id) => { if (me.has(id)) comp.push(pt(c.c, { kind: "both", b: Math.max(c.b, me.get(id).b), name: c.name })); });
    me.forEach((c, id) => { if (!her.has(id)) comp.push(pt(c.c, { kind: "mine", b: c.b, name: c.name })); });
    her.forEach((c, id) => { if (!me.has(id)) comp.push(pt(c.c, { kind: "hers", b: c.b, name: c.name })); });
    map.addSource("comp", { type: "geojson", data: fc(comp) });
    map.addSource("compwp", { type: "geojson", data: fc([pt(d.worldWaypoints.find(w => w.id === "kyon").c, { name: "京都北部" })]) });
  }

  const bloomR = (mult) => ["interpolate", ["linear"], ["zoom"],
    1.4, ["+", 4, ["*", ["get", "b"], 22 * mult]],
    3, ["+", 6, ["*", ["get", "b"], 34 * mult]],
    6, ["+", 8, ["*", ["get", "b"], 60 * mult]]];

  function buildLayers() {
    // 显影城市光晕（三层 bloom）
    map.addLayer({ id: "city-bloom", type: "circle", source: "cities", maxzoom: 8.5, paint: {
      "circle-radius": bloomR(1.0), "circle-color": pal.glow, "circle-blur": 1,
      "circle-opacity": ["*", 0.16, ["get", "b"]] } });
    map.addLayer({ id: "city-mid", type: "circle", source: "cities", maxzoom: 9, paint: {
      "circle-radius": bloomR(0.45), "circle-color": pal.glowSoft, "circle-blur": 0.7,
      "circle-opacity": ["*", 0.32, ["get", "b"]] } });
    map.addLayer({ id: "city-core", type: "circle", source: "cities", paint: {
      "circle-radius": ["interpolate", ["linear"], ["zoom"], 1.4, ["+", 1.6, ["*", 2.6, ["get", "b"]]], 6, ["+", 2.4, ["*", 4, ["get", "b"]]]],
      "circle-color": pal.core, "circle-blur": 0.25, "circle-opacity": ["+", 0.5, ["*", 0.45, ["get", "b"]]] } });

    // 她的地图（同行 lens / 模式）
    map.addLayer({ id: "her-bloom", type: "circle", source: "her", layout: { visibility: "none" }, paint: {
      "circle-radius": bloomR(0.8), "circle-color": pal.her, "circle-blur": 1, "circle-opacity": ["*", 0.14, ["get", "b"]] } });
    map.addLayer({ id: "her-core", type: "circle", source: "her", layout: { visibility: "none" }, paint: {
      "circle-radius": ["+", 1.4, ["*", 2.6, ["get", "b"]]], "circle-color": pal.her, "circle-blur": 0.4, "circle-opacity": 0.85 } });

    // 走过的路线
    map.addLayer({ id: "route-glow", type: "line", source: "routes", minzoom: 8.5,
      layout: { "line-cap": "round", "line-join": "round" }, paint: {
        "line-color": pal.route, "line-blur": 6,
        "line-width": ["interpolate", ["linear"], ["zoom"], 9, 5, 14, 14],
        "line-opacity": ["*", 0.4, ["get", "w"]] } });
    map.addLayer({ id: "route-line", type: "line", source: "routes", minzoom: 8.5,
      layout: { "line-cap": "round", "line-join": "round" }, paint: {
        "line-color": pal.core, "line-blur": 0.6,
        "line-width": ["interpolate", ["linear"], ["zoom"], 9, 1.4, 14, 3], "line-opacity": ["*", 0.9, ["get", "w"]] } });

    // 照片光点
    map.addLayer({ id: "photo-glow", type: "circle", source: "photos", minzoom: 8.5, paint: {
      "circle-radius": ["interpolate", ["linear"], ["zoom"], 9, 5, 14, 13], "circle-color": pal.photo, "circle-blur": 1, "circle-opacity": 0.45 } });
    map.addLayer({ id: "photo-core", type: "circle", source: "photos", minzoom: 8.5, paint: {
      "circle-radius": ["interpolate", ["linear"], ["zoom"], 9, 2.2, 14, 4.5], "circle-color": pal.core, "circle-opacity": 0.95,
      "circle-stroke-color": pal.photo, "circle-stroke-width": 1, "circle-stroke-opacity": 0.6 } });

    // 已保存锚点（环）
    map.addLayer({ id: "anchor-ring", type: "circle", source: "anchors", minzoom: 8.5, paint: {
      "circle-radius": ["interpolate", ["linear"], ["zoom"], 9, 6, 14, 11], "circle-color": "rgba(0,0,0,0)",
      "circle-stroke-color": pal.glow, "circle-stroke-width": 1.4, "circle-stroke-opacity": 0.7 } });
    map.addLayer({ id: "anchor-core", type: "circle", source: "anchors", minzoom: 8.5, paint: {
      "circle-radius": 3, "circle-color": pal.glow, "circle-opacity": 0.9 } });

    // 城市内待显影道标
    map.addLayer({ id: "citywp-ring", type: "circle", source: "citywp", minzoom: 8.5, paint: {
      "circle-radius": 9, "circle-color": "rgba(0,0,0,0)", "circle-stroke-color": pal.waypoint, "circle-stroke-width": 1.3, "circle-stroke-opacity": 0.7 } });
    map.addLayer({ id: "citywp-core", type: "circle", source: "citywp", minzoom: 8.5, paint: {
      "circle-radius": 2.4, "circle-color": pal.waypoint, "circle-opacity": 0.8 } });

    // 远方道标（脉冲）
    map.addLayer({ id: "wp-pulse", type: "circle", source: "waypoints", paint: {
      "circle-radius": 10, "circle-color": "rgba(0,0,0,0)", "circle-stroke-color": pal.waypoint, "circle-stroke-width": 1.4, "circle-stroke-opacity": 0.5 } });
    map.addLayer({ id: "wp-ring", type: "circle", source: "waypoints", paint: {
      "circle-radius": 7, "circle-color": ["match", ["get", "strength"], "common", "rgba(156,192,222,0.12)", "from-her", "rgba(217,138,208,0.12)", "rgba(0,0,0,0)"],
      "circle-stroke-color": ["match", ["get", "strength"], "from-her", pal.her, pal.waypoint], "circle-stroke-width": 1.4, "circle-stroke-opacity": 0.85 } });
    map.addLayer({ id: "wp-core", type: "circle", source: "waypoints", paint: {
      "circle-radius": 2, "circle-color": pal.waypoint, "circle-opacity": 0.7 } });

    // 此刻锚点（呼吸光环）
    map.addLayer({ id: "now-pulse", type: "circle", source: "now", paint: {
      "circle-radius": 14, "circle-color": "rgba(0,0,0,0)", "circle-stroke-color": pal.anchor, "circle-stroke-width": 1.6, "circle-stroke-opacity": 0.4 } });
    map.addLayer({ id: "now-halo", type: "circle", source: "now", paint: {
      "circle-radius": 8, "circle-color": pal.anchor, "circle-blur": 1.1, "circle-opacity": 0.28 } });
    map.addLayer({ id: "now-core", type: "circle", source: "now", paint: {
      "circle-radius": 4, "circle-color": pal.anchor, "circle-opacity": 1,
      "circle-stroke-color": "rgba(255,255,255,0.5)", "circle-stroke-width": 1.5 } });

    // 同行 comp 层
    map.addLayer({ id: "comp-bloom", type: "circle", source: "comp", layout: { visibility: "none" }, paint: {
      "circle-radius": bloomR(0.8), "circle-blur": 1,
      "circle-color": ["match", ["get", "kind"], "hers", pal.her, "both", pal.glow, pal.glow],
      "circle-opacity": ["*", 0.16, ["get", "b"]] } });
    map.addLayer({ id: "comp-core", type: "circle", source: "comp", layout: { visibility: "none" }, paint: {
      "circle-radius": ["+", 1.6, ["*", 3, ["get", "b"]]],
      "circle-color": ["match", ["get", "kind"], "hers", pal.her, "both", pal.core, pal.core],
      "circle-blur": 0.3, "circle-opacity": 0.92 } });
    map.addLayer({ id: "compwp-pulse", type: "circle", source: "compwp", layout: { visibility: "none" }, paint: {
      "circle-radius": 12, "circle-color": "rgba(0,0,0,0)", "circle-stroke-color": pal.waypoint, "circle-stroke-width": 1.6, "circle-stroke-opacity": 0.5 } });
    map.addLayer({ id: "compwp-ring", type: "circle", source: "compwp", layout: { visibility: "none" }, paint: {
      "circle-radius": 8, "circle-color": "rgba(156,192,222,0.14)", "circle-stroke-color": pal.waypoint, "circle-stroke-width": 1.5, "circle-stroke-opacity": 0.9 } });

    // —— 照片显影：注入的真实地点（与显影城市同一套三层 bloom，b 由 0 缓升 = 在显影液里浮现）——
    map.addLayer({ id: "imported-bloom", type: "circle", source: "imported", maxzoom: 8.5, paint: {
      "circle-radius": bloomR(1.0), "circle-color": pal.glow, "circle-blur": 1, "circle-opacity": ["*", 0.16, ["get", "b"]] } });
    map.addLayer({ id: "imported-mid", type: "circle", source: "imported", maxzoom: 9, paint: {
      "circle-radius": bloomR(0.45), "circle-color": pal.glowSoft, "circle-blur": 0.7, "circle-opacity": ["*", 0.32, ["get", "b"]] } });
    map.addLayer({ id: "imported-core", type: "circle", source: "imported", paint: {
      "circle-radius": ["interpolate", ["linear"], ["zoom"], 1.4, ["+", 1.6, ["*", 2.6, ["get", "b"]]], 6, ["+", 2.4, ["*", 4, ["get", "b"]]]],
      "circle-color": pal.core, "circle-blur": 0.25, "circle-opacity": ["*", ["+", 0.5, ["*", 0.45, ["get", "b"]]], ["min", 1, ["*", 4, ["get", "b"]]]] } });

    // 注入地点的照片光点（街区尺度，可点开记忆卡）
    map.addLayer({ id: "iphoto-glow", type: "circle", source: "importedPhotos", minzoom: 8.5, paint: {
      "circle-radius": ["interpolate", ["linear"], ["zoom"], 9, 5, 14, 13], "circle-color": pal.photo, "circle-blur": 1, "circle-opacity": ["*", 0.45, ["get", "op"]] } });
    map.addLayer({ id: "iphoto-core", type: "circle", source: "importedPhotos", minzoom: 8.5, paint: {
      "circle-radius": ["interpolate", ["linear"], ["zoom"], 9, 2.2, 14, 4.5], "circle-color": pal.core, "circle-opacity": ["*", 0.95, ["get", "op"]],
      "circle-stroke-color": pal.photo, "circle-stroke-width": 1, "circle-stroke-opacity": ["*", 0.6, ["get", "op"]] } });

    // 道标 → 显影的点燃环（一次性，从冷脉冲处暖暖亮开）
    map.addLayer({ id: "ignite-ring", type: "circle", source: "ignite", paint: {
      "circle-radius": ["get", "r"], "circle-color": "rgba(0,0,0,0)",
      "circle-stroke-color": pal.glow, "circle-stroke-width": 2, "circle-stroke-opacity": ["get", "op"] } });

    wireClicks();
  }

  function wireClicks() {
    ["photo-core", "anchor-ring", "anchor-core", "imported-core", "iphoto-core"].forEach(id => {
      map.on("click", id, (e) => {
        const f = e.features[0]; const place = f.properties.place;
        if (place && onPlaceCb) onPlaceCb(place);
      });
      map.on("mouseenter", id, () => map.getCanvas().style.cursor = "pointer");
      map.on("mouseleave", id, () => map.getCanvas().style.cursor = "");
    });
  }

  // ---- palette ----
  function applyPalette(name) {
    palName = name; pal = PAL[name];
    const b = pal.base;
    map.setPaintProperty("basemap", "raster-saturation", b.sat);
    map.setPaintProperty("basemap", "raster-hue-rotate", b.hue);
    map.setPaintProperty("basemap", "raster-brightness-max", b.bmax);
    map.setPaintProperty("basemap", "raster-opacity", b.op);
    const set = (l, p, v) => map.getLayer(l) && map.setPaintProperty(l, p, v);
    set("city-bloom", "circle-color", pal.glow); set("city-mid", "circle-color", pal.glowSoft); set("city-core", "circle-color", pal.core);
    set("her-bloom", "circle-color", pal.her); set("her-core", "circle-color", pal.her);
    set("route-glow", "line-color", pal.route); set("route-line", "line-color", pal.core);
    set("photo-glow", "circle-color", pal.photo); set("photo-core", "circle-stroke-color", pal.photo);
    set("anchor-ring", "circle-stroke-color", pal.glow); set("anchor-core", "circle-color", pal.glow);
    set("citywp-ring", "circle-stroke-color", pal.waypoint); set("citywp-core", "circle-color", pal.waypoint);
    set("wp-pulse", "circle-stroke-color", pal.waypoint); set("wp-core", "circle-color", pal.waypoint);
    set("now-pulse", "circle-stroke-color", pal.anchor); set("now-halo", "circle-color", pal.anchor); set("now-core", "circle-color", pal.anchor);
    set("comp-core", "circle-color", ["match", ["get", "kind"], "hers", pal.her, pal.core]);
    set("comp-bloom", "circle-color", ["match", ["get", "kind"], "hers", pal.her, pal.glow]);
    set("imported-bloom", "circle-color", pal.glow); set("imported-mid", "circle-color", pal.glowSoft); set("imported-core", "circle-color", pal.core);
    set("iphoto-glow", "circle-color", pal.photo); set("iphoto-core", "circle-color", pal.core); set("iphoto-core", "circle-stroke-color", pal.photo);
    set("ignite-ring", "circle-stroke-color", pal.glow);
    document.querySelectorAll("[data-palette]").forEach(el => el.setAttribute("data-palette", name));
  }

  function setDensity(d) { // 'quiet' | 'public'
    if (!map.getLayer("labels")) return;
    map.setLayoutProperty("labels", "visibility", d === "public" ? "visible" : "none");
  }

  // ---- lens emphasis (world) ----
  function setLens(lens) {
    if (mode !== "world") return;
    const dim = (l, o) => map.getLayer(l) && map.setPaintProperty(l, "circle-opacity", o);
    const cityBase = ["*", 0.16, ["get", "b"]], cityMidBase = ["*", 0.32, ["get", "b"]];
    // imported-core 始终带 min(1,4b) 渐入护栏，避免 b=0 时核心点跳出来
    const impCore = (mul) => ["*", mul, ["min", 1, ["*", 4, ["get", "b"]]]];
    if (lens === "waypoint") {
      dim("city-bloom", ["*", 0.05, ["get", "b"]]); dim("city-mid", ["*", 0.1, ["get", "b"]]);
      dim("imported-bloom", ["*", 0.05, ["get", "b"]]); dim("imported-mid", ["*", 0.1, ["get", "b"]]);
      map.setPaintProperty("city-core", "circle-opacity", ["*", 0.4, ["get", "b"]]);
      map.setPaintProperty("imported-core", "circle-opacity", impCore(["*", 0.4, ["get", "b"]]));
      map.setPaintProperty("wp-ring", "circle-stroke-opacity", 1);
    } else if (lens === "now") {
      dim("city-bloom", ["*", 0.1, ["get", "b"]]); dim("city-mid", ["*", 0.2, ["get", "b"]]);
      dim("imported-bloom", ["*", 0.1, ["get", "b"]]); dim("imported-mid", ["*", 0.2, ["get", "b"]]);
      flyTo([121.0, 30.5], 3.4, 0);
    } else { // develop / time
      dim("city-bloom", cityBase); dim("city-mid", cityMidBase);
      dim("imported-bloom", cityBase); dim("imported-mid", cityMidBase);
      map.setPaintProperty("city-core", "circle-opacity", ["+", 0.5, ["*", 0.45, ["get", "b"]]]);
      map.setPaintProperty("imported-core", "circle-opacity", impCore(["+", 0.5, ["*", 0.45, ["get", "b"]]]));
      map.setPaintProperty("wp-ring", "circle-stroke-opacity", 0.85);
      if (lens === "develop") flyTo(D().worldView.center, D().worldView.zoom, 0);
    }
  }

  // ---- scene ----
  function flyTo(center, zoom, speed = 1.1) { map.flyTo({ center, zoom, speed, curve: 1.5, essential: true }); }
  const vis = (l, on) => map.getLayer(l) && map.setLayoutProperty(l, "visibility", on ? "visible" : "none");
  const WORLD_LAYERS = ["city-bloom", "city-mid", "city-core", "wp-pulse", "wp-ring", "wp-core",
    "imported-bloom", "imported-mid", "imported-core", "ignite-ring"];
  const CITY_LAYERS = ["route-glow", "route-line", "photo-glow", "photo-core", "anchor-ring", "anchor-core", "citywp-ring", "citywp-core",
    "iphoto-glow", "iphoto-core"];
  const COMP_LAYERS = ["comp-bloom", "comp-core", "compwp-pulse", "compwp-ring"];

  function gotoWorld() {
    mode = "world";
    WORLD_LAYERS.forEach(l => vis(l, true)); COMP_LAYERS.forEach(l => vis(l, false));
    vis("her-bloom", false); vis("her-core", false);
    flyTo(D().worldView.center, D().worldView.zoom);
  }
  function gotoCity() {
    mode = "city";
    flyTo(D().cityView.center, D().cityView.zoom, 0.9);
  }
  function spotlight(region) {
    const map2 = { asia: [[112, 31], 2.4], europe: [[5, 45], 3], kyoto: [[135.6, 35.1], 5.2], shanghai: D().cityView ? [[121.46, 31.235], 9.5] : null };
    const r = map2[region]; if (r) flyTo(r[0], r[1], 0.8);
  }

  function enterCompanion(view) {
    mode = "comp";
    WORLD_LAYERS.forEach(l => vis(l, false)); CITY_LAYERS.forEach(l => vis(l, false));
    COMP_LAYERS.forEach(l => vis(l, false));
    vis("comp-bloom", true); vis("comp-core", true);
    setCompanionView(view || "both");
  }
  function setCompanionView(view) {
    const isWish = view === "wish";
    vis("compwp-pulse", isWish); vis("compwp-ring", isWish);
    if (isWish) {
      map.setFilter("comp-bloom", ["==", ["get", "kind"], "both"]);
      map.setFilter("comp-core", ["==", ["get", "kind"], "both"]);
      map.setPaintProperty("comp-bloom", "circle-opacity", ["*", 0.06, ["get", "b"]]);
      map.setPaintProperty("comp-core", "circle-opacity", 0.3);
      flyTo([135.7, 35.1], 4.6, 0.8);
      return;
    }
    map.setPaintProperty("comp-bloom", "circle-opacity", ["*", 0.16, ["get", "b"]]);
    map.setPaintProperty("comp-core", "circle-opacity", 0.92);
    map.setFilter("comp-bloom", ["==", ["get", "kind"], view]);
    map.setFilter("comp-core", ["==", ["get", "kind"], view]);
    if (view === "hers") flyTo([-4, 40], 4.1, 0.9);
    else if (view === "mine") flyTo([114, 27], 2.7, 0.9);
    else flyTo([129, 34], 3.0, 0.9); // both → 亚洲重合处
  }
  function exitCompanion() { ["comp-bloom", "comp-core"].forEach(l => map.setFilter(l, null)); gotoWorld(); }

  // ---- 照片显影：注入真实地点 / 点燃道标 / 取景 ----
  function developPlaces(places, photos) {
    const base = (typeof performance !== "undefined" && performance.now) ? performance.now() : 0;
    (places || []).forEach((p, i) => {
      impPlaces.push({ id: p.id, c: p.c, bT: p.b, scope: p.scope || "city",
        t0: base + i * 170, dur: 1500, bDisp: 0, done: false });          // 错峰 = 一处一处地浮现
    });
    (photos || []).forEach((p, i) => {
      impPhotos.push({ id: p.id, c: p.c, place: p.place,
        t0: base + 220 + i * 90, dur: 900, opDisp: 0, done: false });
    });
    pushImported();
  }

  // 道标 → 显影：抹掉那枚冷脉冲，原地点燃一圈暖光
  function warmWaypoint(wpId, c, scope) {
    if (scope === "city") {
      vis("citywp-ring", false); vis("citywp-core", false);
    } else {
      removedWp.add(wpId);
      const left = D().worldWaypoints.filter(w => !removedWp.has(w.id));
      map.getSource("waypoints").setData(fc(left.map(w => pt(w.c, { id: w.id, name: w.name, strength: w.strength }))));
      if (wpId === "kyon") { vis("compwp-pulse", false); vis("compwp-ring", false); }  // 同行共同道标也熄掉
    }
    ignites.push({ c, t0: (typeof performance !== "undefined" && performance.now) ? performance.now() : 0, dur: 2000 });
  }

  // 取景到新显影的一组点
  function fitTo(coords) {
    const cs = (coords || []).filter(Boolean);
    if (!cs.length) return;
    if (cs.length === 1) { flyTo(cs[0], 12.2, 0.7); return; }
    let minX = 180, minY = 90, maxX = -180, maxY = -90;
    cs.forEach(c => { minX = Math.min(minX, c[0]); minY = Math.min(minY, c[1]); maxX = Math.max(maxX, c[0]); maxY = Math.max(maxY, c[1]); });
    map.fitBounds([[minX, minY], [maxX, maxY]],
      { padding: { top: 150, bottom: 300, left: 70, right: 70 }, maxZoom: 9.2, duration: 2200, curve: 1.5, essential: true });
  }

  // ---- pulse animation ----
  function tick(ts) {
    if (!t0) t0 = ts; const t = (ts - t0) / 1000;
    const ph = (t % 2.6) / 2.6;           // waypoint breathe
    const ph2 = (t % 2.0) / 2.0;          // now breathe
    if (map.getLayer("wp-pulse")) {
      map.setPaintProperty("wp-pulse", "circle-radius", 8 + ph * 22);
      map.setPaintProperty("wp-pulse", "circle-stroke-opacity", 0.5 * (1 - ph));
    }
    if (map.getLayer("compwp-pulse") && map.getLayoutProperty("compwp-pulse", "visibility") === "visible") {
      map.setPaintProperty("compwp-pulse", "circle-radius", 10 + ph * 26);
      map.setPaintProperty("compwp-pulse", "circle-stroke-opacity", 0.55 * (1 - ph));
    }
    if (map.getLayer("now-pulse")) {
      map.setPaintProperty("now-pulse", "circle-radius", 10 + ph2 * 20);
      map.setPaintProperty("now-pulse", "circle-stroke-opacity", 0.45 * (1 - ph2));
    }

    // —— 照片显影：地点在显影液里缓缓浮现 ——
    const pnow = (typeof performance !== "undefined" && performance.now) ? performance.now() : ts;
    let dirty = false;
    for (const p of impPlaces) {
      if (p.done) continue;
      const k = Math.min(1, (pnow - p.t0) / p.dur);
      if (k <= 0) continue;
      p.bDisp = p.bT * easeOut(k);
      if (k >= 1) { p.bDisp = p.bT; p.done = true; }
      dirty = true;
    }
    for (const p of impPhotos) {
      if (p.done) continue;
      const k = Math.min(1, (pnow - p.t0) / p.dur);
      if (k <= 0) continue;
      p.opDisp = easeOut(k);
      if (k >= 1) { p.opDisp = 1; p.done = true; }
      dirty = true;
    }
    if (dirty) pushImported();
    // 道标 → 显影 的点燃环
    if (ignites.length) {
      const feats = [];
      ignites = ignites.filter(g => {
        const k = (pnow - g.t0) / g.dur;
        if (k >= 1) return false;
        feats.push(pt(g.c, { r: 10 + easeOut(k) * 64, op: 0.7 * (1 - k) }));
        return true;
      });
      map.getSource("ignite").setData(fc(feats));
      igniteActive = true;
    } else if (igniteActive) {
      map.getSource("ignite").setData(fc([]));
      igniteActive = false;
    }

    raf = requestAnimationFrame(tick);
  }

  // ---- init ----
  function init(container, opts = {}) {
    const style = {
      version: 8,
      glyphs: "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
      sources: {
        carto: { type: "raster", tileSize: 256, attribution: "© OSM © CARTO", tiles: [
          "https://a.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
          "https://b.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
          "https://c.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png"] },
        cartoLabels: { type: "raster", tileSize: 256, tiles: [
          "https://a.basemaps.cartocdn.com/dark_only_labels/{z}/{x}/{y}.png",
          "https://b.basemaps.cartocdn.com/dark_only_labels/{z}/{x}/{y}.png",
          "https://c.basemaps.cartocdn.com/dark_only_labels/{z}/{x}/{y}.png"] },
      },
      layers: [
        { id: "ink", type: "background", paint: { "background-color": "#07090e" } },
        { id: "basemap", type: "raster", source: "carto", paint: {
          "raster-saturation": -0.55, "raster-hue-rotate": -10, "raster-brightness-max": 0.72, "raster-opacity": 0.9 } },
        { id: "labels", type: "raster", source: "cartoLabels", layout: { visibility: "none" },
          paint: { "raster-saturation": -1, "raster-opacity": 0.5 } },
      ],
    };
    map = new maplibregl.Map({
      container, style, center: D().worldView.center, zoom: D().worldView.zoom,
      minZoom: 1.2, maxZoom: 17, attributionControl: false, dragRotate: false,
      pitchWithRotate: false, renderWorldCopies: true, fadeDuration: 200,
    });
    map.touchZoomRotate.disableRotation();
    map.on("load", () => {
      buildSources(); buildLayers(); applyPalette(opts.palette || "warm");
      setDensity(opts.density || "quiet");
      raf = requestAnimationFrame(tick);
      opts.onReady && opts.onReady();
    });
    window.__zymap = map;
  }

  return { init, applyPalette, setDensity, setLens, gotoWorld, gotoCity, spotlight,
    enterCompanion, exitCompanion, setCompanionView,
    developPlaces, warmWaypoint, fitTo,
    onPlace: (cb) => { onPlaceCb = cb; }, get mode() { return mode; } };
})();

window.ZyMap = ZyMap;
