/* ── v4 编排:真实矢量地图 + 站点叠加层 + 切天相机 + 风格切换 ── */
const { useState: aS, useEffect: aE, useRef: aR, useMemo: aM, useCallback: aC } = React;

const DALI_CENTER = [100.19, 25.74];

/* ── 详情抽屉(右侧)── */
function StopDrawer({ stop, dayColor, onClose }) {
  if (!stop) return null;
  const meta = GKIND[stop.kind];
  const ex = (window.STOP_EXTRA || {})[stop.id] || {};
  const leg = stop.leg && TRANSPORT[stop.leg.mode];
  return (
    <div style={{ position: "absolute", top: 0, right: 0, bottom: 0, width: 380, zIndex: 40,
      background: "var(--paper)", borderLeft: "1px solid var(--line)",
      boxShadow: "-10px 0 50px rgba(80,55,30,.16)", animation: "drawerIn .32s ease both",
      display: "flex", flexDirection: "column" }}>
      <div className="scroll" style={{ flex: 1, padding: "18px 22px 28px" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
          <div style={{ display: "flex", gap: 7, alignItems: "center", flexWrap: "wrap" }}>
            <span className="chip" style={{ color: meta.color }}><GKindIcon kind={stop.kind} size={12} /> {meta.label}</span>
            <span className="chip"><GClock size={11} /> {stop.time}</span>
            <span className="chip">停留 {stop.stay}</span>
          </div>
          <button onClick={onClose} style={{ border: 0, background: "var(--paper-2)", cursor: "pointer",
            width: 30, height: 30, borderRadius: "50%", color: "var(--ink-soft)", display: "flex", flex: "none",
            alignItems: "center", justifyContent: "center" }}><GClose size={16} /></button>
        </div>

        <div style={{ height: 200, borderRadius: 18, marginTop: 14, position: "relative", overflow: "hidden",
          background: `${dayColor}18`, border: "1px solid var(--line)" }}>
          <img src={imgUrl(ex.img, ex.lock, 640, 400)} alt={stop.name}
            style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
            onError={(e) => { e.currentTarget.style.opacity = 0; }} />
          <div style={{ position: "absolute", inset: 0,
            background: "linear-gradient(180deg, rgba(0,0,0,.12), rgba(0,0,0,0) 42%, rgba(0,0,0,.3))" }} />
          {ex.badge && (
            <span style={{ position: "absolute", top: 11, left: 11, fontSize: 11.5, fontWeight: 700, color: "#fff",
              background: dayColor, padding: "4px 11px", borderRadius: 999, boxShadow: "0 2px 8px rgba(0,0,0,.3)" }}>{ex.badge}</span>
          )}
          <span style={{ position: "absolute", bottom: 10, right: 13, display: "inline-flex", alignItems: "center", gap: 4,
            fontSize: 11.5, fontWeight: 600, color: "#fff", textShadow: "0 1px 3px rgba(0,0,0,.5)" }}>
            <GHeart size={13} /> {ex.hot}</span>
        </div>

        <h2 className="serif" style={{ fontSize: 26, fontWeight: 700, margin: "14px 0 4px", lineHeight: 1.2 }}>{stop.name}</h2>
        <p className="serif" style={{ fontStyle: "italic", fontSize: 15, color: dayColor, margin: "0 0 16px", lineHeight: 1.5 }}>{stop.oneliner}</p>

        {leg && (
          <div style={{ display: "flex", gap: 10, padding: "11px 13px", borderRadius: 13, marginBottom: 14,
            background: "var(--paper-2)", border: "1px solid var(--line)" }}>
            <div style={{ color: TRANSPORT[stop.leg.mode].color, paddingTop: 1 }}>
              <TransportIcon mode={stop.leg.mode} size={20} /></div>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 13, fontWeight: 600 }}>
                怎么到达 · {leg.label} {stop.leg.min}min
                <span style={{ color: "var(--ink-faint)", fontWeight: 500 }}> · {stop.leg.dist}</span></div>
              <div style={{ fontSize: 12, color: "var(--ink-soft)", marginTop: 3, lineHeight: 1.6 }}>{stop.leg.note}</div>
            </div>
          </div>
        )}

        {stop.eat.length > 0 && (
          <div style={{ marginBottom: 16 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12.5, fontWeight: 600,
              color: "var(--c-food)", marginBottom: 8 }}><GUtensils size={14} /> 吃什么</div>
            <div style={{ display: "flex", flexWrap: "wrap", gap: 7 }}>
              {stop.eat.map((e) => <span key={e} className="chip" style={{ background: "var(--paper)" }}>{e}</span>)}
            </div>
          </div>
        )}

        <p style={{ fontSize: 13.5, lineHeight: 1.85, color: "var(--ink-soft)", margin: "0 0 14px" }}>{stop.desc}</p>

        {stop.tips && (
          <div style={{ fontSize: 12.5, lineHeight: 1.7, color: "var(--ink-soft)", background: "var(--paper-2)",
            border: "1px dashed var(--line)", borderRadius: 12, padding: "10px 13px", marginBottom: 16 }}>
            <span className="hand" style={{ color: dayColor, fontWeight: 600 }}>小贴士 · </span>{stop.tips}</div>
        )}

        <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 20 }}>
          {stop.tags.map((t) => <span key={t} style={{ fontSize: 11.5, color: "var(--ink-faint)" }}>#{t}</span>)}
        </div>
        <div style={{ display: "flex", gap: 10 }}>
          <button className="btn btn--ghost" style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6 }}>
            <GNav size={15} /> 导航</button>
          <button className="btn" style={{ flex: 1.6, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6 }}>
            <GHeart size={15} /> 加入我的行程</button>
        </div>
      </div>
    </div>
  );
}

/* ── 站点(锚在真实经纬度上,造型随风格差异化)── */
function StopMarkerContent({ stop, idx, pal, di, active, dim, compact, onSelect }) {
  const deco = pal.deco;
  const dayColor = pal.dayColors[di];
  const size = active ? 40 : compact ? 24 : 33;
  return (
    <div onClick={(e) => { e.stopPropagation(); onSelect(); }}
      style={{ position: "relative", cursor: "pointer", opacity: dim ? 0.55 : 1, transition: "opacity .3s" }}>
      {/* 节点 + 类型 logo 徽章 */}
      <div style={{ position: "relative", display: "inline-flex" }}>
        <NodeShape deco={deco} dayColor={dayColor} idx={idx} size={size} active={active} />
        {!compact && (
          <div style={{ position: "absolute", top: -7, right: -9, zIndex: 2 }}>
            <KindBadge deco={deco} kind={stop.kind} />
          </div>
        )}
        {stop.rec && (
          <div style={{ position: "absolute", bottom: -5, left: -7, width: 16, height: 16, borderRadius: "50%",
            background: dayColor, color: "#fff", display: "flex", alignItems: "center", justifyContent: "center",
            boxShadow: "0 1px 3px rgba(0,0,0,.2)" }}><GStar size={9} /></div>
        )}
      </div>
      {/* 名称小标(浮在 pin 下方,真实地图标注感)*/}
      {!compact && (
        <div className="serif" style={{ position: "absolute", top: "100%", left: "50%",
          transform: "translate(-50%, 5px)", whiteSpace: "nowrap",
          background: "var(--card-bg)", border: `1px solid ${active ? dayColor : "var(--card-line)"}`,
          borderRadius: deco.cardRadius, padding: "3px 10px", boxShadow: "var(--card-shadow)",
          fontSize: 12.5, fontWeight: 600, transition: "border-color .2s" }}>
          {stop.name}
        </div>
      )}
    </div>
  );
}

/* ── 交通段小卡(挂在真实路段中点的 marker 里)── */
function LegChipContent({ leg, pal }) {
  const t = TRANSPORT[leg.mode];
  return (
    <div style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "5px 11px",
      borderRadius: pal.deco.roundLeg, background: "var(--card-bg)", border: "1px solid var(--card-line)",
      boxShadow: "var(--card-shadow)", color: t.color, fontSize: 12, fontWeight: 600, whiteSpace: "nowrap" }}>
      <TransportIcon mode={leg.mode} size={14} />
      <span>{t.label} {leg.min}min</span>
      <span style={{ color: "var(--ink-faint)", fontWeight: 500 }}>· {leg.dist}</span>
    </div>
  );
}

/* ── 节点造型(随风格变:贴纸圆 / 朱砂方印 / 实心圆点)── */
function NodeShape({ deco, dayColor, idx, size, active }) {
  const base = { width: size, height: size, display: "flex", alignItems: "center", justifyContent: "center",
    fontWeight: 700, transition: "all .2s" };
  if (deco.node === "seal") {
    return <div style={{ ...base, borderRadius: 7, background: dayColor, color: deco.nodeText,
      fontFamily: "var(--font-display)", fontSize: size * 0.44,
      boxShadow: "inset 0 0 0 1.5px rgba(255,255,255,.4), var(--card-shadow)" }}>{idx + 1}</div>;
  }
  if (deco.node === "dot") {
    return <div style={{ ...base, borderRadius: "50%", background: dayColor, color: deco.nodeText,
      fontFamily: "var(--font-body)", fontSize: size * 0.46,
      boxShadow: active ? `0 0 0 5px ${dayColor}22, var(--card-shadow)` : "var(--card-shadow)" }}>{idx + 1}</div>;
  }
  return <div style={{ ...base, borderRadius: "50%", background: "var(--card-bg)", color: dayColor,
    border: `2.5px solid ${dayColor}`, fontFamily: "var(--font-display)", fontSize: size * 0.46,
    boxShadow: active ? `0 0 0 5px ${dayColor}22, var(--card-shadow)` : "var(--card-shadow)" }}>{idx + 1}</div>;
}

/* ── 地点类型 logo 徽章(随风格变:描边圆 / 印章方 / 圆角彩块)── */
function KindBadge({ deco, kind }) {
  const color = GKIND[kind].color;
  if (deco.badge === "stamp")
    return <div style={{ width: 18, height: 18, borderRadius: 3, background: color, color: "#fff",
      display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 1px 3px rgba(0,0,0,.2)" }}>
      <GKindIcon kind={kind} size={11} sw={2} /></div>;
  if (deco.badge === "squircle")
    return <div style={{ width: 19, height: 19, borderRadius: 6, background: color, color: "#fff",
      display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 2px 6px rgba(0,0,0,.16)" }}>
      <GKindIcon kind={kind} size={11} sw={2} /></div>;
  return <div style={{ width: 19, height: 19, borderRadius: "50%", background: "var(--card-bg)", color,
    border: `1.5px solid ${color}`, display: "flex", alignItems: "center", justifyContent: "center" }}>
    <GKindIcon kind={kind} size={11} /></div>;
}

/* ── 洱海水面手绘点缀(帆船 / 海鸥 / 日头),锚在真实经纬度上 ── */
const MOTIFS = [
  { id: "boat", at: [100.205, 25.802] },
  { id: "birds", at: [100.226, 25.716] },
  { id: "sun", at: [100.186, 25.856] },
];
function MotifContent({ type, color }) {
  const wrap = { opacity: 0.4, color, lineHeight: 0 };
  if (type === "boat")
    return <div style={wrap}><svg width="46" height="40" viewBox="0 0 46 40" fill="none" stroke={color} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
      <path d="M7 27h30l-4 7H11z" /><path d="M22 27V5l11 18z" /><path d="M22 9 13 27h9z" />
      <path d="M3 34c3 1.5 5 1.5 7 0s4-1.5 6 0 5 1.5 7 0 4-1.5 6 0 5 1.5 7 0" /></svg></div>;
  if (type === "birds")
    return <div style={wrap}><svg width="58" height="26" viewBox="0 0 58 26" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round">
      <path d="M4 14c4-6 8-6 11 0 3-6 7-6 11 0" /><path d="M30 7c3.5-5 7-5 9.5 0 2.5-5 6-5 9.5 0" transform="translate(-2,2)" />
      <path d="M20 20c3-4 6-4 8 0" /></svg></div>;
  return <div style={wrap}><svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke={color} strokeWidth="1.4" strokeLinecap="round">
    <circle cx="20" cy="20" r="8" /><path d="M20 4v4M20 32v4M4 20h4M32 20h4M8.5 8.5l2.8 2.8M28.7 28.7l2.8 2.8M31.5 8.5l-2.8 2.8M11.3 28.7l-2.8 2.8" /></svg></div>;
}

/* 真实照片(按关键词挂 Flickr,lock 固定同一张)*/
function imgUrl(kw, lock, w = 400, h = 300) {
  return `https://loremflickr.com/${w}/${h}/${encodeURIComponent(kw || "travel,china")}?lock=${lock || 1}`;
}

/* ── 小红书风「今日行程」富卡片(真实照片为主体)── */
function RailCard({ stop, idx, dayColor, deco, active, onSelect }) {
  const meta = GKIND[stop.kind];
  const ex = (window.STOP_EXTRA || {})[stop.id] || {};
  return (
    <div onClick={(e) => { e.stopPropagation(); onSelect(stop.id); }}
      style={{ flex: "0 0 auto", width: 200, cursor: "pointer",
        background: "var(--card-bg)", border: `1px solid ${active ? dayColor : "var(--card-line)"}`,
        borderRadius: deco.cardRadius + 6, overflow: "hidden", boxShadow: "var(--card-shadow)",
        transform: active ? "translateY(-8px)" : "none", transition: "transform .25s, border-color .2s" }}>
      {/* 照片 */}
      <div style={{ position: "relative", height: 120, background: `${dayColor}1c` }}>
        <img src={imgUrl(ex.img, ex.lock, 400, 300)} alt={stop.name} loading="lazy"
          style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
          onError={(e) => { e.currentTarget.style.opacity = 0; }} />
        <div style={{ position: "absolute", inset: 0,
          background: "linear-gradient(180deg, rgba(0,0,0,.22), rgba(0,0,0,0) 38%, rgba(0,0,0,.34))" }} />
        {ex.badge && (
          <span style={{ position: "absolute", top: 9, left: 9, fontSize: 10.5, fontWeight: 700,
            color: "#fff", background: dayColor, padding: "3px 9px", borderRadius: 999,
            boxShadow: "0 2px 8px rgba(0,0,0,.28)" }}>{ex.badge}</span>
        )}
        <span style={{ position: "absolute", top: 8, right: 8, display: "inline-flex", alignItems: "center", gap: 3,
          fontSize: 10.5, fontWeight: 600, color: "#fff", textShadow: "0 1px 3px rgba(0,0,0,.5)" }}>
          <GHeart size={12} /> {ex.hot}</span>
        <span style={{ position: "absolute", bottom: 8, left: 9, width: 22, height: 22, borderRadius: "50%",
          background: dayColor, color: "#fff", fontFamily: "var(--font-display)", fontWeight: 700, fontSize: 12,
          display: "flex", alignItems: "center", justifyContent: "center", border: "2px solid #fff",
          boxShadow: "0 2px 6px rgba(0,0,0,.3)" }}>{idx + 1}</span>
        <span style={{ position: "absolute", bottom: 8, right: 9, fontSize: 10.5, color: "#fff", fontWeight: 600,
          textShadow: "0 1px 3px rgba(0,0,0,.5)" }}>{stop.time}</span>
      </div>
      {/* 文字 */}
      <div style={{ padding: "10px 12px 12px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
          <span className="serif" style={{ fontSize: 15.5, fontWeight: 700, lineHeight: 1.2 }}>{stop.name}</span>
          <span style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 2,
            color: dayColor, fontWeight: 700, fontSize: 12 }}><GStar size={11} /> {ex.rating}</span>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 6, marginTop: 5, fontSize: 11 }}>
          <span style={{ display: "inline-flex", alignItems: "center", gap: 3, color: meta.color, fontWeight: 600 }}>
            <GKindIcon kind={stop.kind} size={11} /> {meta.label}</span>
          <span style={{ color: "var(--ink-faint)" }}>·</span>
          <span style={{ color: "var(--ink-soft)" }}>{ex.price}</span>
        </div>
        <div style={{ fontSize: 11, color: "var(--ink-soft)", marginTop: 6, display: "flex", alignItems: "center", gap: 4 }}>
          <span style={{ color: dayColor, fontWeight: 700 }}>招牌</span>
          <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{ex.signature}</span>
        </div>
      </div>
    </div>
  );
}

function AtlasApp() {
  const [styleId, setStyleId] = aS("journal");
  const [view, setView] = aS({ mode: "day", day: 0 }); // day | all
  const [selId, setSelId] = aS(null);
  const [ready, setReady] = aS(false);
  const [railOpen, setRailOpen] = aS(true);
  const [markersReady, setMarkersReady] = aS(false);
  const [, setTick] = aS(0);

  const mapEl = aR(null);
  const mapRef = aR(null);
  const markersRef = aR({ stops: [], legs: [], motifs: [] });
  const styleRef = aR(styleId);
  styleRef.current = styleId;

  const pal = PALETTES[styleId];
  const days = TRIP3.plan.days;

  /* 初始化地图(一次)*/
  aE(() => {
    // 整趟行程包络(含真实路线全程)→ 锁定可视/可平移范围
    const routePts = Object.values(ROUTE_GEO).flat().flatMap((l) => l.coords);
    const all = days.flatMap((d) => d.stops.map((s) => s.lnglat)).concat(routePts);
    const w = Math.min(...all.map((c) => c[0])), e = Math.max(...all.map((c) => c[0]));
    const s = Math.min(...all.map((c) => c[1])), n = Math.max(...all.map((c) => c[1]));
    // 余量放足:让最大跨度那天(Day2 环湖)的框选+边距仍落在锁定范围内,
    // 否则 MapLibre 会逐帧把镜头往回夹 → 闪烁卡顿。
    const padX = 0.14, padY = 0.11;

    const map = new maplibregl.Map({
      container: mapEl.current,
      style: "https://tiles.openfreemap.org/styles/liberty",
      center: DALI_CENTER, zoom: 11.6, attributionControl: false,
      dragRotate: false, pitchWithRotate: false,
      minZoom: 10.4, maxZoom: 16.5,
      maxBounds: [[w - padX, s - padY], [e + padX, n + padY]],
    });
    map.touchZoomRotate && map.touchZoomRotate.disableRotation();
    mapRef.current = map;
    map.on("load", () => {
      recolorMap(map, PALETTES[styleRef.current]);
      addRoutes(map, PALETTES[styleRef.current]);
      setReady(true);
    });
    return () => map.remove();
  }, []);

  /* 原生 Marker(一次创建,位置由地图引擎每帧更新,不经过 React)*/
  aE(() => {
    if (!ready) return;
    const map = mapRef.current;
    const mk = { stops: [], legs: [], motifs: [] };
    days.forEach((d, di) => {
      const legs = ROUTE_GEO[di + 1] || [];
      d.stops.forEach((s, i) => {
        const el = document.createElement("div");
        const marker = new maplibregl.Marker({ element: el, anchor: "center" }).setLngLat(s.lnglat).addTo(map);
        mk.stops.push({ key: `${di}-${s.id}`, el, marker, di, i, stop: s });
        if (i > 0 && s.leg) {
          const seg = legs[i - 1];
          const mid = seg && seg.coords.length ? seg.coords[Math.floor(seg.coords.length / 2)] : s.lnglat;
          const le = document.createElement("div");
          le.style.pointerEvents = "none"; le.style.zIndex = "12";
          const lm = new maplibregl.Marker({ element: le, anchor: "center" }).setLngLat(mid).addTo(map);
          mk.legs.push({ key: `leg-${di}-${s.id}`, el: le, marker: lm, di, leg: s.leg });
        }
      });
    });
    MOTIFS.forEach((m) => {
      const el = document.createElement("div");
      el.style.pointerEvents = "none"; el.style.zIndex = "1";
      const marker = new maplibregl.Marker({ element: el, anchor: "center" }).setLngLat(m.at).addTo(map);
      mk.motifs.push({ key: m.id, el, marker, type: m.id });
    });
    markersRef.current = mk;
    setMarkersReady(true);
    return () => {
      [...mk.stops, ...mk.legs, ...mk.motifs].forEach((x) => x.marker.remove());
      markersRef.current = { stops: [], legs: [], motifs: [] };
      setMarkersReady(false);
    };
  }, [ready]);

  /* 站点 marker 的层级:选中置顶、非当天压低 */
  aE(() => {
    if (!markersReady) return;
    markersRef.current.stops.forEach((m) => {
      m.el.style.zIndex = m.stop.id === selId ? "30"
        : (view.mode === "day" && view.day !== m.di) ? "5" : "14";
    });
  }, [selId, view, markersReady]);

  /* 路线图层(真实路网 geometry,按天拼接)*/
  function dayCoords(i) {
    const legs = (ROUTE_GEO[i + 1] || []);
    if (!legs.length) return days[i].stops.map((s) => s.lnglat);
    return legs.reduce((acc, l) => acc.concat(l.coords), []);
  }
  function addRoutes(map, p) {
    const fc = {
      type: "FeatureCollection",
      features: days.map((d, i) => ({
        type: "Feature", properties: { day: i },
        geometry: { type: "LineString", coordinates: dayCoords(i) },
      })),
    };
    if (map.getSource("routes")) { map.getSource("routes").setData(fc); return; }
    map.addSource("routes", { type: "geojson", data: fc });
    map.addLayer({
      id: "route-case", type: "line", source: "routes",
      layout: { "line-cap": "round", "line-join": "round" },
      paint: { "line-color": p.routeCase, "line-width": 9, "line-opacity": 0.9, "line-blur": 0.6 },
    });
    map.addLayer({
      id: "route-line", type: "line", source: "routes",
      layout: { "line-cap": "round", "line-join": "round" },
      paint: {
        "line-width": ["interpolate", ["linear"], ["zoom"], 11, 4, 15, 5.5],
        "line-dasharray": p.routeDash,
        "line-color": ["match", ["get", "day"],
          0, p.dayColors[0], 1, p.dayColors[1], 2, p.dayColors[2], p.dayColors[0]],
      },
    });
  }

  /* 风格切换:重上色 + 改路线配色 */
  aE(() => {
    if (!ready) return;
    const map = mapRef.current;
    recolorMap(map, pal);
    try {
      map.setPaintProperty("route-case", "line-color", pal.routeCase);
      map.setPaintProperty("route-line", "line-dasharray", pal.routeDash);
      map.setPaintProperty("route-line", "line-color", ["match", ["get", "day"],
        0, pal.dayColors[0], 1, pal.dayColors[1], 2, pal.dayColors[2], pal.dayColors[0]]);
    } catch (e) {}
    setTick((t) => t + 1);
  }, [styleId, ready]);

  /* 相机:切天 / 全程 / 选点 */
  aE(() => {
    if (!ready) return;
    const map = mapRef.current;
    const focus = view.mode === "day" ? [view.day] : days.map((_, i) => i);
    // 路线高亮:聚焦当天,虚化其它天
    try {
      map.setPaintProperty("route-line", "line-opacity",
        view.mode === "day"
          ? ["case", ["==", ["get", "day"], view.day], 1, 0.16]
          : 0.92);
      map.setPaintProperty("route-case", "line-opacity",
        view.mode === "day"
          ? ["case", ["==", ["get", "day"], view.day], 0.85, 0.12]
          : 0.7);
    } catch (e) {}
    // 选中某站 → 真正飞进去放大该目的地
    if (selId) {
      let sel = null;
      for (const d of days) { const s = d.stops.find((x) => x.id === selId); if (s) { sel = s; break; } }
      if (sel) {
        map.flyTo({ center: sel.lnglat, zoom: 15.4, duration: 900, essential: true,
          padding: { top: 90, bottom: railOpen ? 200 : 90, left: 90, right: 412 } });
        return;
      }
    }
    // 否则:框住当天/全程(含真实路线),留足边距居中
    const coords = focus.flatMap((di) => dayCoords(di));
    const b = coords.reduce((bb, c) => bb.extend(c),
      new maplibregl.LngLatBounds(coords[0], coords[0]));
    map.fitBounds(b, {
      padding: { top: 112, bottom: railOpen ? 202 : 80, left: 132, right: 132 },
      duration: 1000, maxZoom: view.mode === "day" ? 15 : 13,
      essential: true,
    });
  }, [view, ready, selId, railOpen]);

  const selStop = aM(() => {
    for (let di = 0; di < days.length; di++) {
      const s = days[di].stops.find((x) => x.id === selId);
      if (s) return { s, di };
    }
    return null;
  }, [selId]);

  const pickStop = aC((id, di) => {
    setSelId(id);
    setView((v) => (v.mode === "day" && v.day === di ? v : { mode: "day", day: di }));
  }, []);
  const gotoDay = aC((di) => { setView({ mode: "day", day: di }); setSelId(null); }, []);

  // 把 React 内容注入各 marker 容器(只在切风格/切天/选中时更新,平移时不触发)
  const portals = markersReady ? [
    ...markersRef.current.stops.map((m) => ReactDOM.createPortal(
      <StopMarkerContent stop={m.stop} idx={m.i} pal={pal} di={m.di}
        active={m.stop.id === selId}
        dim={view.mode === "day" && view.day !== m.di}
        compact={view.mode === "all" || (view.mode === "day" && view.day !== m.di)}
        onSelect={() => pickStop(m.stop.id, m.di)} />, m.el, m.key)),
    ...markersRef.current.legs.map((m) => ReactDOM.createPortal(
      (view.mode === "day" && view.day === m.di) ? <LegChipContent leg={m.leg} pal={pal} /> : null,
      m.el, m.key)),
    ...markersRef.current.motifs.map((m) => ReactDOM.createPortal(
      <MotifContent type={m.type} color={pal.map.textInk} />, m.el, m.key)),
  ] : null;

  return (
    <div style={{ height: "100vh", display: "flex", flexDirection: "column", ...pal.vars,
      fontFamily: "var(--font-body)", color: "var(--ink)", background: pal.map.paper }}>
      {portals}
      {/* 顶栏 */}
      <header style={{ flex: "none", display: "flex", alignItems: "center", justifyContent: "space-between",
        gap: 16, padding: "12px 22px", background: "var(--paper)", borderBottom: "1px solid var(--line)", zIndex: 50 }}>
        <div style={{ display: "flex", alignItems: "baseline", gap: 12, flex: "none" }}>
          <h1 className="serif" style={{ fontSize: 22, fontWeight: 700, margin: 0 }}>{TRIP3.destination}</h1>
          <span style={{ fontSize: 12.5, color: "var(--ink-faint)" }}>{TRIP3.plan.title}</span>
        </div>

        <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
          <button onClick={() => { setView({ mode: "all" }); setSelId(null); }} style={tabStyle(view.mode === "all")}>
            <GExpand size={13} /> 全程</button>
          <span style={{ width: 1, height: 18, background: "var(--line)", margin: "0 4px" }} />
          {days.map((d, i) => (
            <button key={i} onClick={() => gotoDay(i)}
              style={tabStyle(view.mode === "day" && view.day === i)}>
              <span style={{ width: 8, height: 8, borderRadius: "50%", background: pal.dayColors[i] }} />
              D{i + 1} · {d.area}</button>
          ))}
        </div>

        <div style={{ display: "flex", alignItems: "center", gap: 8, flex: "none" }}>
          <span style={{ fontSize: 11, color: "var(--ink-faint)" }}>风格</span>
          <div style={{ display: "flex", gap: 3, padding: 3, borderRadius: 999, background: "var(--paper-2)" }}>
            {Object.values(PALETTES).map((v) => (
              <button key={v.id} onClick={() => setStyleId(v.id)} style={{ cursor: "pointer", border: 0,
                borderRadius: 999, padding: "5px 11px", fontSize: 12, fontWeight: 600, fontFamily: "var(--font-body)",
                background: styleId === v.id ? "var(--ink)" : "transparent",
                color: styleId === v.id ? "var(--paper)" : "var(--ink-soft)" }}>{v.name}</button>
            ))}
          </div>
        </div>
      </header>

      {/* 地图舞台 */}
      <div style={{ flex: 1, position: "relative", overflow: "hidden" }}
        onClick={() => setSelId(null)}>
        <div ref={mapEl} style={{ position: "absolute", inset: 0 }} />

        {/* 框景渐隐(空边淡入纸色,海报装裱感)——用 radial-gradient,廉价且不闪 */}
        <div style={{ position: "absolute", inset: 0, pointerEvents: "none", zIndex: 6,
          background: `radial-gradient(125% 125% at 50% 50%, transparent 56%, ${pal.map.paper}99 84%, ${pal.map.paper} 100%)` }} />

        {/* 当天区域说明 */}
        {view.mode === "day" && (
          <div style={{ position: "absolute", top: 18, left: 18, zIndex: 18, maxWidth: 320,
            background: `${pal.map.paper}f0`, borderRadius: 16,
            border: "1px solid var(--line)", padding: "12px 16px", boxShadow: "var(--card-shadow)" }}>
            <div style={{ display: "flex", alignItems: "baseline", gap: 9 }}>
              <span className="serif" style={{ fontSize: 13, letterSpacing: ".1em", color: pal.dayColors[view.day] }}>DAY 0{view.day + 1}</span>
              <span className="serif" style={{ fontSize: 22, fontWeight: 700 }}>{days[view.day].area}</span>
            </div>
            <div className="hand" style={{ fontSize: 13, color: "var(--ink-soft)", marginTop: 5, lineHeight: 1.6 }}>
              {days[view.day].areaDesc}</div>
          </div>
        )}

        {/* 站点/交通/motif 由原生 Marker 承载(见上方 portals),此处无需叠加层 */}

        {/* 加载提示 */}
        {!ready && (
          <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center",
            zIndex: 20, color: "var(--ink-faint)", fontSize: 14 }}>地图加载中…</div>
        )}

        {/* 切天箭头 */}
        {view.mode === "day" && (
          <>
            {view.day > 0 && <NavArrow dir="l" onClick={() => gotoDay(view.day - 1)} />}
            {view.day < days.length - 1 && <NavArrow dir="r" onClick={() => gotoDay(view.day + 1)} />}
          </>
        )}

        {/* 今日行程 · 小红书富卡片栏(可收起,不占地方)*/}
        {view.mode === "day" && (
          <div style={{ position: "absolute", left: 0, right: selId ? 380 : 0, bottom: 0, zIndex: 22 }}>
            <button onClick={(e) => { e.stopPropagation(); setRailOpen((o) => !o); }}
              style={{ position: "absolute", right: 20, top: railOpen ? -42 : -36, zIndex: 2,
                display: "inline-flex", alignItems: "center", gap: 6, cursor: "pointer",
                border: "1px solid var(--line)", background: "var(--paper)", color: "var(--ink-soft)",
                borderRadius: 999, padding: "7px 13px", fontSize: 12.5, fontWeight: 600, fontFamily: "var(--font-body)",
                boxShadow: "0 4px 14px rgba(80,55,30,.16)" }}>
              {railOpen
                ? <>收起卡片 <span style={{ fontSize: 10 }}>▾</span></>
                : <>今日 {days[view.day].stops.length} 站 · 展开 <span style={{ fontSize: 10 }}>▴</span></>}
            </button>
            {railOpen && (
              <div style={{ display: "flex", alignItems: "flex-end", gap: 12, padding: "32px 20px 16px",
                overflowX: "auto", scrollbarWidth: "none",
                background: `linear-gradient(0deg, ${pal.map.paper}f5 38%, ${pal.map.paper}b0 70%, ${pal.map.paper}00)` }}>
                {days[view.day].stops.map((s, i) => (
                  <RailCard key={s.id} stop={s} idx={i} dayColor={pal.dayColors[view.day]} deco={pal.deco}
                    active={s.id === selId} onSelect={(id) => pickStop(id, view.day)} />
                ))}
                <div style={{ flex: "0 0 4px" }} />
              </div>
            )}
          </div>
        )}

        {/* 提示(仅全程视图)*/}
        {view.mode === "all" && (
          <div className="hand" style={{ position: "absolute", left: 16, bottom: 14, zIndex: 18, fontSize: 12.5,
            color: "var(--ink-soft)", background: "rgba(255,255,255,.7)", padding: "4px 11px", borderRadius: 9 }}>
            点任意站点进入当天 · 缩放有限度,锁定在行程范围内
          </div>
        )}

        {!selId && <Compass pal={pal} bottom={view.mode === "day" ? 192 : 20} />}

        <StopDrawer stop={selStop && selStop.s} dayColor={selStop ? pal.dayColors[selStop.di] : "#000"}
          onClose={() => setSelId(null)} />
      </div>
    </div>
  );
}

function tabStyle(active) {
  return { cursor: "pointer", border: 0, borderRadius: 10, padding: "7px 11px", fontSize: 12.5,
    fontWeight: 600, fontFamily: "var(--font-body)", display: "inline-flex", alignItems: "center", gap: 6,
    background: active ? "var(--paper-2)" : "transparent",
    color: active ? "var(--ink)" : "var(--ink-soft)",
    boxShadow: active ? "inset 0 0 0 1px var(--line)" : "none" };
}

const COMPASS_FONT = '"Newsreader","Noto Serif SC","Songti SC",serif';
function Compass({ pal, bottom = 20 }) {
  const ink = pal.map.textInk, faint = "#A89A88", north = pal.dayColors[0];
  return (
    <div style={{ position: "absolute", right: 22, bottom, zIndex: 16, opacity: 0.74, pointerEvents: "none" }}>
      <svg width="62" height="62" viewBox="0 0 62 62" fill="none">
        <circle cx="31" cy="31" r="27" stroke={ink} strokeWidth="1" strokeDasharray="1 3" opacity="0.5" />
        <circle cx="31" cy="31" r="20" stroke={faint} strokeWidth="0.8" />
        <path d="M31 9 L36 31 L31 27 L26 31 Z" fill={north} />
        <path d="M31 53 L26 31 L31 35 L36 31 Z" fill={faint} />
        <text x="31" y="8" textAnchor="middle" fontSize="9" fontFamily={COMPASS_FONT} fill={ink}>N</text>
        <text x="31" y="61.5" textAnchor="middle" fontSize="7.5" fontFamily={COMPASS_FONT} fill={faint}>S</text>
        <text x="58" y="34" textAnchor="middle" fontSize="7.5" fontFamily={COMPASS_FONT} fill={faint}>E</text>
        <text x="4" y="34" textAnchor="middle" fontSize="7.5" fontFamily={COMPASS_FONT} fill={faint}>W</text>
        <circle cx="31" cy="31" r="2" fill={ink} />
      </svg>
    </div>
  );
}

function NavArrow({ dir, onClick }) {
  return (
    <button onClick={(e) => { e.stopPropagation(); onClick(); }}
      style={{ position: "absolute", top: "50%", [dir === "l" ? "left" : "right"]: 18,
      transform: "translateY(-50%)", width: 46, height: 46, borderRadius: "50%", cursor: "pointer",
      border: "1px solid var(--line)", background: "var(--paper)", color: "var(--ink)",
      boxShadow: "0 6px 20px rgba(80,55,30,.18)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 30 }}>
      {dir === "l" ? <GChevL size={20} /> : <GChevR size={20} />}
    </button>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<AtlasApp />);
