// ===== Admin Editor — Figma-like guide creator =====
const { useState, useRef, useEffect, useMemo, useCallback, useLayoutEffect } = React;

// ---------- helpers ----------
const cls = (...xs) => xs.filter(Boolean).join(" ");
const clone = (o) => JSON.parse(JSON.stringify(o));
const uid = () => Math.random().toString(36).slice(2, 9);
const placeCaretEnd = (el) => {
  try {
    const r = document.createRange();
    r.selectNodeContents(el);r.collapse(false);
    const s = window.getSelection();s.removeAllRanges();s.addRange(r);
  } catch (e) {}
};

function useToasts() {
  const [list, setList] = useState([]);
  const push = (msg) => {
    const id = uid();
    setList((l) => [...l, { id, msg }]);
    setTimeout(() => setList((l) => l.filter((t) => t.id !== id)), 2200);
  };
  const node =
  <div className="toasts">
      {list.map((t) =>
    <div key={t.id} className="toast">
          <I.Check size={14} stroke="var(--success)" />
          {t.msg}
        </div>
    )}
    </div>;

  return { push, node };
}

// ---------- Layer rendering on canvas ----------
function LayerRender({ layer, scale }) {
  const style = {
    left: layer.x, top: layer.y, width: layer.w, height: layer.h,
    zIndex: layer.z ?? 1,
    transform: layer.rotation ? `rotate(${layer.rotation}deg)` : undefined,
    opacity: layer.visible === false ? 0.3 : 1
  };
  if (layer.type === "bg") {
    return <div className="layer-node" style={{ ...style, background: layer.color, pointerEvents: "none" }} />;
  }
  if (layer.type === "shape") {
    return <div className="layer-node" style={{ ...style, background: layer.color, borderRadius: shapeRadiusCss(layer) }} />;
  }
  if (layer.type === "logo") {
    return (
      <div className="layer-node" style={style}>
        <div className="bn-logo-placeholder" style={{ background: layer.color || "#1a1a1a", color: "#fff", fontSize: Math.min(layer.h * 0.5, 22) }}>
          {layer.content || "LOGO"}
        </div>
      </div>);

  }
  if (layer.type === "image") {
    return (
      <div className="layer-node" style={style}>
        <ImagePlaceholder slot={layer.slot} note={layer.note} />
      </div>);

  }
  if (layer.type === "text") {
    const hasBg = !!layer.bg;
    return (
      <div className="layer-node" style={{
        ...style,
        background: hasBg ? layer.bg : "transparent",
        borderRadius: layer.radius || 0,
        padding: hasBg ? `${layer.padding || 8}px ${(layer.padding || 8) * 1.2}px` : 0,
        display: "flex",
        alignItems: layer.vAlign === "center" ? "center" : "flex-start",
        justifyContent: layer.align === "center" ? "center" : layer.align === "right" ? "flex-end" : "flex-start"
      }}>
        <div className="bn-text" style={{
          fontFamily: layer.font || "Pretendard",
          fontWeight: layer.weight || 400,
          fontSize: layer.size || 16,
          color: layer.color === "" ? "transparent" : layer.color || "#000",
          lineHeight: layer.lineHeight || 1.3,
          letterSpacing: layer.letterSpacing || 0,
          textAlign: layer.align || "left",
          width: "100%"
        }}>
          {layer.content}
        </div>
      </div>);

  }
  return null;
}

function ImagePlaceholder({ slot, note }) {
  const ref = useRef(null);
  const [size, setSize] = useState({ w: 0, h: 0 });
  useEffect(() => {
    const el = ref.current;if (!el) return;
    const update = () => setSize({ w: el.clientWidth, h: el.clientHeight });
    update();
    let ro;
    if (typeof ResizeObserver !== "undefined") {ro = new ResizeObserver(update);ro.observe(el);}
    return () => ro && ro.disconnect();
  }, []);
  const label = slot === "product" ? "상품 이미지" : slot === "deco" ? "데코" : slot === "logo" ? "로고" : "이미지";

  const { w, h } = size;
  const clamp = (v, lo, hi) => Math.max(lo, Math.min(v, hi));
  // 가로로 길고 낮은 박스는 아이콘+라벨을 가로 배치, 그 외엔 세로 배치
  const horizontal = w > 0 && h > 0 && w / h > 2.4 && h < 96;
  const basis = horizontal ? h : Math.min(w, h) || 0;
  // 영역 크기 대비 또렷하게 보이도록 비례 규칙 재설정.
  // 가로형: 높이(h)가 한 줄의 제약 → 높이에 비례. 세로형: 짧은 변(min(w,h))에 비례.
  const iconSize = horizontal ? clamp(h * 0.40, 13, 26) : clamp(basis * 0.26, 14, 40);
  const fontSize = horizontal ? clamp(h * 0.24, 11, 18) : clamp(basis * 0.13, 11, 18);
  const gap = Math.max(3, fontSize * 0.42);
  const showLabel = horizontal ? w > 84 : h >= iconSize + fontSize * 1.6 + 6 && w > 40;
  const showNote = !!note && showLabel && !horizontal && h >= iconSize + fontSize * 3.0 + 8;

  // 빈 슬롯 구분 + z-순서(가림) 정합: 채움은 '불투명'으로 둬서 앞 레이어가 뒤 레이어를
  // 실제 결과처럼 가리게 한다. 종류 톤(로고=액센트) + 점선 외곽선 + 좌상단 라벨칩으로
  // 앞에 있거나 같은 종류끼리 인접할 때의 구분은 유지한다.
  const isLogo = slot === "logo";
  const tint = isLogo ?
  { bg: "#ECEBFA", border: "rgba(78,76,219,.5)", stroke: "#4E4CDB", text: "#4E4CDB", chipBorder: "rgba(78,76,219,.28)" } :
  { bg: "#EBEBEE", border: "#cdced6", stroke: "#9a9aa3", text: "#85858f", chipBorder: "rgba(0,0,0,.06)" };
  // 좌상단 라벨칩: 슬롯이 충분히 클 때만. 칩이 보이면 중앙 라벨은 중복이라 숨김(아이콘만 유지).
  const chipFont = clamp(basis * 0.12, 9, 12);
  const showChip = !horizontal && w >= 96 && h >= 60;
  const showCenterLabel = showLabel && !showChip;
  const showCenterNote = showNote && !showChip;
  const Icon = ({ s, st }) =>
  isLogo ?
  <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={st} strokeWidth="1.6" style={{ flexShrink: 0, display: "block" }}>
        <path d="M12 3l3 3-3 3-3-3z" /><path d="M12 15l3 3-3 3-3-3z" />
        <path d="M6 9l3 3-3 3-3-3z" /><path d="M18 9l3 3-3 3-3-3z" />
      </svg> :
  <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={st} strokeWidth="1.6" style={{ flexShrink: 0, display: "block" }}>
        <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="9" cy="9" r="1.5" /><path d="M21 16l-5-5L5 21" />
      </svg>;

  return (
    <div ref={ref} style={{
      position: "absolute", inset: 0, borderRadius: "inherit",
      display: "flex", alignItems: "center", justifyContent: "center", overflow: "hidden",
      color: tint.text, background: tint.bg, border: `1.5px dashed ${tint.border}`
    }}>
      {showChip &&
      <div style={{
        position: "absolute", top: 5, left: 5, display: "flex", alignItems: "center", gap: 4,
        fontSize: chipFont, fontWeight: 700, lineHeight: 1, padding: "3px 6px", borderRadius: 4,
        background: "rgba(255,255,255,.93)", color: tint.text, border: `1px solid ${tint.chipBorder}`,
        whiteSpace: "nowrap", maxWidth: "calc(100% - 10px)", overflow: "hidden"
      }}>
          <Icon s={chipFont + 2} st={tint.stroke} />{label}
        </div>
      }
      <div style={{
        display: "flex", flexDirection: horizontal ? "row" : "column",
        alignItems: "center", justifyContent: "center", gap,
        textAlign: "center", minWidth: 0, maxWidth: "100%", padding: 4
      }}>
        <Icon s={iconSize} st={tint.stroke} />
        {showCenterLabel &&
        <div style={{ minWidth: 0, maxWidth: "100%" }}>
            <div style={{ fontSize, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: "100%", fontWeight: "700", color: tint.text }}>{label}</div>
            {showCenterNote && <div style={{ fontSize: fontSize * 0.85, color: tint.text, opacity: .7, marginTop: 2, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: "100%" }}>{note}</div>}
          </div>
        }
      </div>
    </div>);

}

// ---------- Canvas with drag/resize ----------
// Discrete zoom levels (as fractions) for the +/− controls
const ZOOM_LEVELS = [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4, 5, 6];
const zoomIn = (z) => ZOOM_LEVELS.find((l) => l > z + 1e-4) ?? ZOOM_LEVELS[ZOOM_LEVELS.length - 1];
const zoomOut = (z) => [...ZOOM_LEVELS].reverse().find((l) => l < z - 1e-4) ?? ZOOM_LEVELS[0];

// ===== 중첩 그룹 헬퍼 =====
// groups: { [gid]: { name, collapsed, parent } } — parent = 상위 그룹 gid (없으면 null/undefined)
// layer.groupId = 레이어가 직접 속한 (가장 안쪽) 그룹 id
// gid 에서 루트까지의 조상 체인 [gid, parent, grandparent, ...]
function groupAncestors(groups, gid) {
  const chain = [];
  let g = gid,guard = 0;
  while (g && groups && groups[g] && guard++ < 64) {chain.push(g);g = groups[g].parent || null;}
  return chain;
}
// gid 의 최상위(루트) 그룹
function topGroupOf(groups, gid) {
  const chain = groupAncestors(groups, gid);
  return chain.length ? chain[chain.length - 1] : gid;
}
// ancestor 가 gid 자신이거나 gid 의 조상인지
function isGroupInside(groups, gid, ancestor) {
  return groupAncestors(groups, gid).includes(ancestor);
}
// gid 그룹에 속한 모든 레이어(중첩 하위 포함)
function layersInGroup(layers, groups, gid) {
  return layers.filter((l) => l.groupId && groupAncestors(groups, l.groupId).includes(gid));
}
// gid 그룹과 그 하위 그룹 id 전체
function descendantGroupIds(groups, gid) {
  const out = [gid];
  Object.keys(groups || {}).forEach((k) => {if (k !== gid && groupAncestors(groups, k).includes(gid)) out.push(k);});
  return out;
}
// gid 의 조상 체인 중, 부모가 container 인 그룹(= container 직속 서브트리의 루트). 없으면 null
function childOfContainer(groups, gid, container) {
  let g = gid,guard = 0;
  while (g && groups && groups[g] && guard++ < 64) {
    if ((groups[g].parent || null) === (container || null)) return g;
    g = groups[g].parent || null;
  }
  return null;
}
// 선택을 대표하는 단일 그룹 gid — 선택 레이어 집합이 정확히 어느 그룹의 멤버 전체와 일치하면 그 gid. (옵션 설정 패널 트리거)
function representedGroup(guide, selectedLayers) {
  const groups = (guide && guide.groups) || {};
  const sel = (selectedLayers || []).filter((l) => l.type !== "bg");
  if (!sel.length) return null;
  const chains = sel.map((l) => l.groupId ? groupAncestors(groups, l.groupId) : []);
  if (chains.some((c) => !c.length)) return null;
  const common = chains[0].filter((g) => chains.every((c) => c.includes(g)));
  for (const gid of common) { // chain은 안→밖 순서면 → 가장 안쪽(깊은) 공통 그룹부터
    if (layersInGroup(guide.layers, groups, gid).filter((l) => l.type !== "bg").length === sel.length) return gid;
  }
  return null;
}
// 선택 집합을 "이동 단위(unit)"로 묶는다.
// C = 선택된 모든 레이어의 가장 깊은 공통 그룹(없으면 최상위). C 직속 서브그룹은 하나의 강체 단위로,
// C 직속 일반 레이어는 각각 한 단위로 취급. → 그룹과 일반 레이어를 함께 정렬/간격할 때
// 그룹 내부 레이어끼리는 절대 움직이지 않게 한다. (선택이 한 그룹 내부에만 있으면 자연히 레이어별 단위가 됨)
function selectionUnits(layers, groups, idSet) {
  groups = groups || {};
  const sel = layers.filter((l) => idSet.has(l.id) && l.type !== "bg");
  if (!sel.length) return [];
  const chains = sel.map((l) => groupAncestors(groups, l.groupId));
  let C = null;
  if (chains.every((c) => c.length)) {
    for (const cand of chains[0]) {if (chains.every((c) => c.includes(cand))) {C = cand;break;}}
  }
  const unitMap = new Map();
  sel.forEach((l) => {
    const cr = childOfContainer(groups, l.groupId, C); // C 직속 서브그룹 gid 또는 null
    const key = cr ? "g:" + cr : "l:" + l.id;
    if (!unitMap.has(key)) unitMap.set(key, []);
    unitMap.get(key).push(l);
  });
  return [...unitMap.values()].map((ls) => {
    const minX = Math.min(...ls.map((l) => l.x));
    const minY = Math.min(...ls.map((l) => l.y));
    const maxX = Math.max(...ls.map((l) => l.x + l.w));
    const maxY = Math.max(...ls.map((l) => l.y + l.h));
    return { layers: ls, minX, minY, maxX, maxY, cx: (minX + maxX) / 2, cy: (minY + maxY) / 2 };
  });
}
// 레이어 패널 트리 순서(위=앞, z 내림차순)대로 평탄화한 non-bg 레이어 id 목록
function flattenLayerOrder(layers, groups) {
  groups = groups || {};
  const groupZ = {};
  Object.keys(groups).forEach((gid) => {
    const m = layersInGroup(layers, groups, gid);
    groupZ[gid] = m.length ? Math.max(...m.map((l) => l.z ?? 0)) : 0;
  });
  const childrenOf = (gid) => {
    const subs = Object.keys(groups).filter((k) => (groups[k].parent || null) === gid && layersInGroup(layers, groups, k).length > 0).map((k) => ({ kind: "group", gid: k, z: groupZ[k] ?? 0 }));
    const lays = layers.filter((l) => l.type !== "bg" && (l.groupId || null) === gid).map((l) => ({ kind: "layer", id: l.id, z: l.z ?? 1 }));
    return [...subs, ...lays].sort((a, b) => b.z - a.z);
  };
  const out = [];
  const walk = (gid) => {childrenOf(gid).forEach((n) => {if (n.kind === "group") walk(n.gid);else out.push(n.id);});};
  walk(null);
  return out; // top→bottom
}
// 회전 커서 (양방향 곡선 화살표, 흰 외곽선) — 모서리마다 90°씩 회전시켜 해당 코너를 감싸도록 한다.
// 기준(0°)은 우상단(ne). 시계방향으로 se=90°, sw=180°, nw=270°.
const rotCursor = (deg) =>
"url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><g transform='rotate(" + deg + " 12 12)'><g stroke='%23fff' stroke-width='3.4' fill='%23fff' stroke-linejoin='round' stroke-linecap='round'><path d='M6.5 10 Q 17.5 6 17 16.5' fill='none'/><polygon points='2.8,10 8.2,7 8.2,13'/><polygon points='17,20.5 13.8,15 20.2,15'/></g><g stroke='%23111' stroke-width='1.6' fill='%23111' stroke-linejoin='round' stroke-linecap='round'><path d='M6.5 10 Q 17.5 6 17 16.5' fill='none'/><polygon points='2.8,10 8.2,7 8.2,13'/><polygon points='17,20.5 13.8,15 20.2,15'/></g></g></svg>\") 12 12, auto";
const ROT_CUR_NE = rotCursor(0);
const ROT_CUR_SE = rotCursor(90);
const ROT_CUR_SW = rotCursor(180);
const ROT_CUR_NW = rotCursor(270);
function Canvas({ guide, selectedIds, onSelect, onSelectMany, onUpdateLayer, onUpdateManyLayers, onAddLayer, onDeleteLayer, zoom, setZoom, showGrid, onToggleGrid, onUndo, canUndo, onRedo, canRedo, onGestureStart, onGestureEnd, onRevealGroups, onRenameGuide }) {
  const wrapRef = useRef(null);
  const scrollRef = useRef(null);
  const dragState = useRef(null);
  const movedRef = useRef(false);
  const panState = useRef(null);
  const drawState = useRef(null);
  const suppressDeselect = useRef(false);
  const gestureStartRef = useRef(onGestureStart);
  gestureStartRef.current = onGestureStart;
  const gestureEndRef = useRef(onGestureEnd);
  gestureEndRef.current = onGestureEnd;
  const artboardRef = useRef(null);
  const drillRef = useRef(null); // 캔버스 드릴다운 그룹 레벨(gid). 더블클릭으로 한 단계씩 깊어짐
  const [guides, setGuides] = useState({ v: null, h: null });
  const [isDragging, setIsDragging] = useState(false);
  const [tool, setTool] = useState("select"); // select | hand | text | shape | image | logo
  const [spaceDown, setSpaceDown] = useState(false); // temporary pan while space held
  const [draft, setDraft] = useState(null); // rubber-band rect (artboard coords) while drawing
  const [editId, setEditId] = useState(null); // text layer being inline-edited
  const [editEmpty, setEditEmpty] = useState(false); // 편집 중 텍스트가 비어있는지 (빈 상태=박스 숨김)
  useEffect(() => {
    if (!editId) return;
    const l = guide.layers.find((x) => x.id === editId);
    setEditEmpty(!(l && l.content || "").trim());
  }, [editId]);
  const [radiusTip, setRadiusTip] = useState(null); // live corner-radius value while dragging
  const [marquee, setMarquee] = useState(null); // rubber-band selection rect (artboard coords)
  const marqueeState = useRef(null);
  const [gridDivision, setGridDivision] = useState("full"); // split preset key (see SPLIT_SEGMENTS)
  const [editingName, setEditingName] = useState(false); // inline-editing the artboard (guide) name
  const [nameDraft, setNameDraft] = useState("");
  const [nameW, setNameW] = useState(0); // measured pixel width of the name text
  const nameMeasureRef = useRef(null);
  useLayoutEffect(() => {
    if (editingName && nameMeasureRef.current) setNameW(nameMeasureRef.current.offsetWidth);
  }, [nameDraft, editingName]);
  const panning = tool === "hand" || spaceDown;
  const creating = tool === "text" || tool === "shape" || tool === "image" || tool === "logo";
  const liveRef = useRef({});
  liveRef.current = { zoom, setZoom, onToggleGrid, gridDivision, showGrid, fit: () => fitToScreen() };

  // Fit the canvas into the available area (between top bar, side panels and bottom toolbar)
  const fitToScreen = () => {
    const el = scrollRef.current;
    if (!el) return;
    const availW = el.clientWidth - 96; // 48px breathing room each side
    const availH = el.clientHeight - 150; // reserve space for size badge (top) + floating toolbar (bottom)
    if (availW <= 0 || availH <= 0) return;
    const z = Math.min(availW / guide.width, availH / guide.height);
    setZoom(Math.max(0.25, Math.min(z, 3)));
  };
  // Re-fit when the guide size changes or the window resizes
  useEffect(() => {
    const t = setTimeout(fitToScreen, 0);
    const onR = () => fitToScreen();
    window.addEventListener("resize", onR);
    return () => {clearTimeout(t);window.removeEventListener("resize", onR);};
  }, [guide.width, guide.height]); // eslint-disable-line

  // Convert a mouse event into artboard (design) coordinates.
  const toArtboard = (e) => {
    const r = artboardRef.current.getBoundingClientRect();
    return { x: (e.clientX - r.left) / fitScale, y: (e.clientY - r.top) / fitScale };
  };

  // Spacebar = temporary hand tool (Figma-style). Ignore while typing in inputs.
  useEffect(() => {
    const isTyping = (t) => t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable);
    const kd = (e) => {
      if (e.code === "Space" && !isTyping(e.target)) {e.preventDefault();setSpaceDown(true);return;}
      if (e.key === "Escape") {setTool("select");setDraft(null);return;}
      if (isTyping(e.target) || e.metaKey || e.ctrlKey || e.altKey) return;
      const k = e.key.toLowerCase();
      const map = { v: "select", h: "hand", t: "text", r: "shape", i: "image", l: "logo" };
      if (map[k]) {e.preventDefault();setTool(map[k]);return;}
      const L = liveRef.current;
      if (k === "g" && e.shiftKey) {e.preventDefault();L.onToggleGrid?.();return;}
      if (k === "1" && e.shiftKey) {e.preventDefault();L.fit?.();return;}
      if (k === "=" || k === "+") {e.preventDefault();L.setZoom(zoomIn(L.zoom));return;}
      if (k === "-" || k === "_") {e.preventDefault();L.setZoom(zoomOut(L.zoom));return;}
    };
    const ku = (e) => {if (e.code === "Space") setSpaceDown(false);};
    window.addEventListener("keydown", kd);
    window.addEventListener("keyup", ku);
    return () => {window.removeEventListener("keydown", kd);window.removeEventListener("keyup", ku);};
  }, []);

  // Drag-to-pan on the scroll area when the hand tool is active.
  const onPanMouseDown = (e) => {
    if (!panning) return;
    e.preventDefault();e.stopPropagation();
    const el = scrollRef.current;
    if (!el) return;
    panState.current = { x: e.clientX, y: e.clientY, sl: el.scrollLeft, st: el.scrollTop };
    const move = (ev) => {
      const p = panState.current;if (!p) return;
      el.scrollLeft = p.sl - (ev.clientX - p.x);
      el.scrollTop = p.st - (ev.clientY - p.y);
    };
    const up = () => {
      panState.current = null;
      window.removeEventListener("mousemove", move);
      window.removeEventListener("mouseup", up);
    };
    window.addEventListener("mousemove", move);
    window.addEventListener("mouseup", up);
  };

  // Create-by-drawing: text = click to place + type; shape/image/logo = drag to draw.
  const onCreateMouseDown = (e) => {
    if (!creating) return;
    e.preventDefault();e.stopPropagation();
    suppressDeselect.current = true;
    const start = toArtboard(e);
    if (tool === "text") {
      const id = onAddLayer("text", { x: Math.round(start.x), y: Math.round(start.y), w: 16, h: 34 }, "");
      setTool("select");
      setEditId(id);
      // Release deselect-suppression only AFTER the trailing click fires (mousedown→mouseup→click),
      // mirroring the shape path. Releasing on mousedown lets the canvas onClick run onSelect(null)
      // and clear the just-created text, so the left layer panel never activates.
      const release = () => {
        window.removeEventListener("mouseup", release);
        setTimeout(() => {suppressDeselect.current = false;}, 0);
      };
      window.addEventListener("mouseup", release);
      return;
    }
    drawState.current = { sx: start.x, sy: start.y, type: tool, rect: null };
    setDraft({ x: start.x, y: start.y, w: 0, h: 0 });
    const move = (ev) => {
      const s = drawState.current;if (!s) return;
      const p = toArtboard(ev);
      let x = Math.min(s.sx, p.x),y = Math.min(s.sy, p.y);
      let w = Math.abs(p.x - s.sx),h = Math.abs(p.y - s.sy);
      // Shift = 정방형: 더 큰 변에 맞춰 정사각형으로, 드래그 방향 기준 시작점에 고정
      if (ev.shiftKey) {
        const d = Math.max(w, h);
        x = p.x < s.sx ? s.sx - d : s.sx;
        y = p.y < s.sy ? s.sy - d : s.sy;
        w = d;h = d;
      }
      const rect = { x, y, w, h };
      s.rect = rect;
      setDraft(rect);
    };
    const up = () => {
      window.removeEventListener("mousemove", move);
      window.removeEventListener("mouseup", up);
      const s = drawState.current;
      let rect = s && s.rect;
      if (!rect || rect.w < 6 || rect.h < 6) {
        const d = s.type === "logo" ? 80 : 160;
        rect = { x: s.sx, y: s.sy, w: d, h: d };
      }
      rect = { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.w), h: Math.round(rect.h) };
      onAddLayer(s.type, rect);
      setDraft(null);
      drawState.current = null;
      setTool("select");
      setTimeout(() => {suppressDeselect.current = false;}, 0);
    };
    window.addEventListener("mousemove", move);
    window.addEventListener("mouseup", up);
  };

  // Marquee (rubber-band) selection: drag on empty canvas with the select tool to
  // select every non-bg, unlocked layer the band overlaps.
  const onMarqueeMouseDown = (e) => {
    const start = toArtboard(e);
    const additive = e.shiftKey || e.metaKey || e.ctrlKey;
    const baseSel = additive ? [...selectedIds] : [];
    suppressDeselect.current = true;
    marqueeState.current = { sx: start.x, sy: start.y };
    setMarquee({ x: start.x, y: start.y, w: 0, h: 0 });
    let moved = false;
    const move = (ev) => {
      const s = marqueeState.current;if (!s) return;
      const p = toArtboard(ev);
      if (Math.abs(ev.clientX) >= 0 && (Math.abs(p.x - s.sx) > 2 || Math.abs(p.y - s.sy) > 2)) moved = true;
      const rect = { x: Math.min(s.sx, p.x), y: Math.min(s.sy, p.y), w: Math.abs(p.x - s.sx), h: Math.abs(p.y - s.sy) };
      setMarquee(rect);
      const hits = guide.layers.
      filter((l) => l.type !== "bg" && !l.locked &&
      l.x < rect.x + rect.w && l.x + l.w > rect.x && l.y < rect.y + rect.h && l.y + l.h > rect.y).
      map((l) => l.id);
      onSelectMany(baseSel.length ? Array.from(new Set([...baseSel, ...hits])) : hits);
    };
    const up = () => {
      window.removeEventListener("mousemove", move);
      window.removeEventListener("mouseup", up);
      if (!moved && !additive) onSelectMany([]); // plain click on empty canvas → deselect
      marqueeState.current = null;
      setMarquee(null);
      setTimeout(() => {suppressDeselect.current = false;}, 0);
    };
    window.addEventListener("mousemove", move);
    window.addEventListener("mouseup", up);
  };

  // Single dispatcher for mousedown on empty canvas.
  const onCanvasMouseDown = (e) => {
    if (panning) return onPanMouseDown(e);
    if (creating) return onCreateMouseDown(e);
    onMarqueeMouseDown(e); // select tool → rubber-band selection
  };

  const onLayerMouseDown = (e, layer) => {
    // Hand tool active → pan the canvas instead of selecting/dragging the layer
    if (panning) {onPanMouseDown(e);return;}
    // A creation tool is active → draw a new layer on top instead of selecting
    if (creating) {onCreateMouseDown(e);return;}
    e.stopPropagation();
    const additive = e.shiftKey || e.metaKey || e.ctrlKey;
    // Resolve the effective selection synchronously so we can drag the right set
    let sel,groupClick = false;
    if (additive) {
      drillRef.current = null;
      sel = selectedIds.includes(layer.id) ? selectedIds.filter((x) => x !== layer.id) : [...selectedIds, layer.id];
    } else if (selectedIds.includes(layer.id)) {
      sel = selectedIds; // keep the group so it can be dragged together
    } else if (layer.groupId) {
      // 그룹에 속한 레이어 클릭 → 현재 드릴 레벨의 그룹 전체를 선택(Figma식).
      // 드릴 전(루트)에는 최상위 그룹 전체. 더블클릭으로 한 단계씩 안으로 들어간다.
      const grps = guide.groups || {};
      let cont = drillRef.current;
      if (cont && !isGroupInside(grps, layer.groupId, cont)) cont = null; // 다른 그룹을 클릭하면 루트로 리셋
      const childGid = childOfContainer(grps, layer.groupId, cont); // cont 직속 서브그룹(이 레이어를 포함). 없으면 레이어가 cont 직속
      drillRef.current = cont;
      if (childGid) {
        const members = layersInGroup(guide.layers, grps, childGid).map((l) => l.id);
        if (members.length > 1) {sel = members;groupClick = true;} else
        sel = [layer.id];
      } else {
        sel = [layer.id];
      }
    } else {
      drillRef.current = null;
      sel = [layer.id];
    }
    onSelectMany(sel);

    if (layer.locked) return; // background / locked layers select but don't drag
    const movable = guide.layers.filter((l) => sel.includes(l.id) && l.type !== "bg" && !l.locked);
    if (!movable.length) return;
    movedRef.current = false;
    dragState.current = {
      type: "move",
      startX: e.clientX, startY: e.clientY,
      items: movable.map((l) => ({ id: l.id, origX: l.x, origY: l.y })),
      primaryId: layer.id,
      // 임시 다중선택(마퀴 등)에서 한 멤버를 클릭하면 그 멤버로 좁힌다.
      // 단, 그룹 선택(groupClick)은 클릭만으로 좁히지 않고 그룹을 유지한다.
      collapseTo: !additive && sel.length > 1 && !groupClick ? layer.id : null
    };
    gestureStartRef.current?.();
    setIsDragging(true);
    document.body.style.cursor = "move";
  };

  // 더블클릭 → 단일 레이어 선택(그룹에서 한 단계 더 들어감), 텍스트면 편집
  // 더블클릭 → 그룹을 한 단계씩 안으로 드릴다운. 더 들어갈 그룹이 없으면 단일 레이어 선택(텍스트면 편집)
  const onLayerDoubleClick = (e, layer) => {
    e.stopPropagation();
    const grps = guide.groups || {};
    if (layer.groupId) {
      let cont = drillRef.current;
      if (cont && !isGroupInside(grps, layer.groupId, cont)) cont = null;
      const childGid = childOfContainer(grps, layer.groupId, cont); // cont 직속 서브그룹(레이어 포함)
      if (childGid) {
        // 한 단계 더 깊이 들어가 그 서브그룹 안의 다음 레벨을 선택
        drillRef.current = childGid;
        // 패널에서 드릴 경로의 그룹들을 펼쳐, 선택된 그룹/레이어가 노출되게 함
        onRevealGroups?.(groupAncestors(grps, childGid));
        const innerChild = childOfContainer(grps, layer.groupId, childGid);
        if (innerChild) {
          const members = layersInGroup(guide.layers, grps, innerChild).map((l) => l.id);
          onSelectMany(members.length > 1 ? members : [layer.id]);
        } else {
          onSelectMany([layer.id]);
        }
        return;
      }
      // 더 들어갈 그룹 없음(리프) → 텍스트면 편집, 아니면 단일 선택
      if (layer.type === "text" && !layer.locked) {setEditId(layer.id);return;}
      onSelectMany([layer.id]);
      return;
    }
    if (layer.type === "text" && !layer.locked) {setEditId(layer.id);return;}
  };

  const onHandleMouseDown = (e, layer, corner) => {
    e.stopPropagation();
    movedRef.current = false;
    dragState.current = {
      type: "resize",
      corner,
      startX: e.clientX, startY: e.clientY,
      origX: layer.x, origY: layer.y,
      origW: layer.w, origH: layer.h,
      id: layer.id, primaryId: layer.id
    };
    gestureStartRef.current?.();
    setIsDragging(true);
    document.body.style.cursor = `${corner.includes("n") || corner.includes("s") ? "ns" : "ew"}-resize`;
  };

  // 모서리 바깥 회전 핸들 — 중심 기준 각도로 layer.rotation 갱신 (shift=15° 스냅)
  const onRotateMouseDown = (e, layer) => {
    e.stopPropagation();e.preventDefault();
    const node = e.currentTarget.parentElement;
    const rect = node.getBoundingClientRect();
    const cx = rect.left + rect.width / 2,cy = rect.top + rect.height / 2;
    const startA = Math.atan2(e.clientY - cy, e.clientX - cx) * 180 / Math.PI;
    const orig = layer.rotation || 0;
    movedRef.current = false;
    gestureStartRef.current?.();
    setIsDragging(true);
    document.body.style.cursor = "grabbing";
    const move = (ev) => {
      const a = Math.atan2(ev.clientY - cy, ev.clientX - cx) * 180 / Math.PI;
      let rot = orig + (a - startA);
      if (ev.shiftKey) rot = Math.round(rot / 15) * 15;
      rot = (Math.round(rot) % 360 + 360) % 360;
      onUpdateLayer(layer.id, { rotation: rot });
    };
    const up = () => {
      window.removeEventListener("mousemove", move);
      window.removeEventListener("mouseup", up);
      document.body.style.cursor = "";
      setIsDragging(false);
      gestureEndRef.current?.();
    };
    window.addEventListener("mousemove", move);
    window.addEventListener("mouseup", up);
  };

  // 멀티 선택 묶음(그룹) 바운딩 박스 계산
  const groupBBox = () => {
    const sels = guide.layers.filter((l) => selectedIds.includes(l.id));
    if (sels.length < 2) return null;
    const minX = Math.min(...sels.map((l) => l.x));
    const minY = Math.min(...sels.map((l) => l.y));
    const maxX = Math.max(...sels.map((l) => l.x + l.w));
    const maxY = Math.max(...sels.map((l) => l.y + l.h));
    return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
  };

  // 그룹 바운딩 박스 모서리 핸들 드래그 → 선택 레이어 비례 스케일
  const onGroupHandleMouseDown = (e, corner) => {
    e.stopPropagation();
    movedRef.current = false;
    const box = groupBBox();
    if (!box) return;
    const sels = guide.layers.filter((l) => selectedIds.includes(l.id));
    dragState.current = {
      type: "group-resize",
      corner,
      startX: e.clientX, startY: e.clientY,
      origBox: box,
      items: sels.map((l) => ({ id: l.id, relX: l.x - box.x, relY: l.y - box.y, w: l.w, h: l.h }))
    };
    gestureStartRef.current?.();
    setIsDragging(true);
    document.body.style.cursor = `${corner.includes("n") || corner.includes("s") ? "ns" : "ew"}-resize`;
  };
  const onRadiusMouseDown = (e, layer, corner = "tl") => {
    e.stopPropagation();e.preventDefault();
    gestureStartRef.current?.();
    const maxR = Math.floor(Math.min(layer.w, layer.h) / 2);
    const right = layer.x + layer.w,bottom = layer.y + layer.h;
    const field = { tl: "radiusTL", tr: "radiusTR", br: "radiusBR", bl: "radiusBL" }[corner];
    const move = (ev) => {
      const p = toArtboard(ev);
      // distance from the dragged corner along each axis → that corner's radius
      const dx = corner.includes("l") ? p.x - layer.x : right - p.x;
      const dy = corner.includes("t") ? p.y - layer.y : bottom - p.y;
      let r = Math.round(Math.min(dx, dy));
      r = Math.max(0, Math.min(r, maxR));
      onUpdateLayer(layer.id, { [field]: r });
      setRadiusTip(r);
    };
    const up = () => {
      window.removeEventListener("mousemove", move);
      window.removeEventListener("mouseup", up);
      setRadiusTip(null);
      gestureEndRef.current?.();
    };
    window.addEventListener("mousemove", move);
    window.addEventListener("mouseup", up);
  };

  useEffect(() => {
    const onMove = (e) => {
      const s = dragState.current;
      if (!s) return;
      const dx = (e.clientX - s.startX) / zoom;
      const dy = (e.clientY - s.startY) / zoom;
      if (Math.abs(e.clientX - s.startX) > 2 || Math.abs(e.clientY - s.startY) > 2) movedRef.current = true;

      if (s.type === "move") {
        // 다중 선택(그룹 포함): 선택 전체의 바운딩 박스를 기준으로 스냅한 뒤 동일 델타로 평행 이동
        if (s.items.length > 1) {
          const dxr = dx,dyr = dy;
          const sel = s.items.map((it) => {
            const L = guide.layers.find((l) => l.id === it.id);
            return { x: it.origX + dxr, y: it.origY + dyr, w: L ? L.w : 0, h: L ? L.h : 0 };
          });
          let bL = Infinity,bT = Infinity,bR = -Infinity,bB = -Infinity;
          sel.forEach((o) => {bL = Math.min(bL, o.x);bT = Math.min(bT, o.y);bR = Math.max(bR, o.x + o.w);bB = Math.max(bB, o.y + o.h);});
          const bCx = (bL + bR) / 2,bCy = (bT + bB) / 2;
          const snap = 4;
          const selIds = new Set(s.items.map((it) => it.id));
          const horizSnap = [0, guide.width / 2, guide.width];
          const vertSnap = [0, guide.height / 2, guide.height];
          if (guide.safeAreaShow ?? true) {
            const sa = guide.safeArea || { top: 50, right: 50, bottom: 50, left: 50 };
            horizSnap.push(sa.left, guide.width - sa.right);
            vertSnap.push(sa.top, guide.height - sa.bottom);
          }
          (SPLIT_SEGMENTS[liveRef.current.gridDivision] || []).forEach((sg) => {if (sg.o === "v") horizSnap.push(sg.at * guide.width);else vertSnap.push(sg.at * guide.height);});
          if (liveRef.current.showGrid) {
            const G = 20;
            for (let x = 0; x <= guide.width; x += G) horizSnap.push(x);
            for (let y = 0; y <= guide.height; y += G) vertSnap.push(y);
          }
          // 선택에 포함되지 않은 다른 레이어들의 엣지/중앙도 스냅 대상
          guide.layers.filter((l) => !selIds.has(l.id) && l.visible !== false).forEach((l) => {
            horizSnap.push(l.x, l.x + l.w / 2, l.x + l.w);
            vertSnap.push(l.y, l.y + l.h / 2, l.y + l.h);
          });
          let snapDX = 0,snapDY = 0,snappedV = null,snappedH = null;
          for (const v of horizSnap) {
            if (Math.abs(bL - v) < snap) {snapDX = v - bL;snappedV = v;break;}
            if (Math.abs(bR - v) < snap) {snapDX = v - bR;snappedV = v;break;}
            if (Math.abs(bCx - v) < snap) {snapDX = v - bCx;snappedV = v;break;}
          }
          for (const v of vertSnap) {
            if (Math.abs(bT - v) < snap) {snapDY = v - bT;snappedH = v;break;}
            if (Math.abs(bB - v) < snap) {snapDY = v - bB;snappedH = v;break;}
            if (Math.abs(bCy - v) < snap) {snapDY = v - bCy;snappedH = v;break;}
          }
          const patchMap = {};
          s.items.forEach((it) => {patchMap[it.id] = { x: Math.round(it.origX + dxr + snapDX), y: Math.round(it.origY + dyr + snapDY) };});
          onUpdateManyLayers(patchMap);
          setGuides({ v: snappedV, h: snappedH });
          return;
        }
        const it = s.items[0];
        const target = guide.layers.find((l) => l.id === it.id);
        if (!target) return;
        let nx = Math.round(it.origX + dx);
        let ny = Math.round(it.origY + dy);
        // Smart snap to other layers + edges
        const others = guide.layers.filter((l) => l.id !== it.id && l.visible !== false);
        const snap = 4;
        let snappedV = null,snappedH = null;
        const ownCx = nx + target.w / 2;
        const ownCy = ny + target.h / 2;

        const horizSnap = [0, guide.width / 2, guide.width];
        const vertSnap = [0, guide.height / 2, guide.height];
        // 세이프 영역 경계도 스냅 대상에 포함 (표시 중일 때)
        if (guide.safeAreaShow ?? true) {
          const sa = guide.safeArea || { top: 50, right: 50, bottom: 50, left: 50 };
          horizSnap.push(sa.left, guide.width - sa.right);
          vertSnap.push(sa.top, guide.height - sa.bottom);
        }
        // 1) Canvas-split (화면 분할/비율) guides → 분할선 (세로 → X 스납, 가로 → Y 스납)
        (SPLIT_SEGMENTS[liveRef.current.gridDivision] || []).forEach((s) => {if (s.o === "v") horizSnap.push(s.at * guide.width);else vertSnap.push(s.at * guide.height);});
        // 2) Grid overlay active → snap to the 20px grid in both axes
        if (liveRef.current.showGrid) {
          const G = 20;
          for (let x = 0; x <= guide.width; x += G) horizSnap.push(x);
          for (let y = 0; y <= guide.height; y += G) vertSnap.push(y);
        }
        others.forEach((l) => {
          horizSnap.push(l.x, l.x + l.w / 2, l.x + l.w);
          vertSnap.push(l.y, l.y + l.h / 2, l.y + l.h);
        });
        for (const v of horizSnap) {
          if (Math.abs(nx - v) < snap) {nx = v;snappedV = v;break;}
          if (Math.abs(nx + target.w - v) < snap) {nx = v - target.w;snappedV = v;break;}
          if (Math.abs(ownCx - v) < snap) {nx = v - target.w / 2;snappedV = v;break;}
        }
        for (const v of vertSnap) {
          if (Math.abs(ny - v) < snap) {ny = v;snappedH = v;break;}
          if (Math.abs(ny + target.h - v) < snap) {ny = v - target.h;snappedH = v;break;}
          if (Math.abs(ownCy - v) < snap) {ny = v - target.h / 2;snappedH = v;break;}
        }
        setGuides({ v: snappedV, h: snappedH });
        onUpdateLayer(it.id, { x: nx, y: ny });
      } else if (s.type === "group-resize") {
        const b = s.origBox;
        let nx = b.x,ny = b.y,nw = b.w,nh = b.h;
        if (s.corner.includes("e")) nw = Math.max(8, Math.round(b.w + dx));
        if (s.corner.includes("s")) nh = Math.max(8, Math.round(b.h + dy));
        if (s.corner.includes("w")) {nx = Math.round(b.x + dx);nw = Math.max(8, Math.round(b.w - dx));}
        if (s.corner.includes("n")) {ny = Math.round(b.y + dy);nh = Math.max(8, Math.round(b.h - dy));}
        const sx = nw / b.w,sy = nh / b.h;
        const patchMap = {};
        s.items.forEach((it) => {
          patchMap[it.id] = {
            x: Math.round(nx + it.relX * sx),
            y: Math.round(ny + it.relY * sy),
            w: Math.max(4, Math.round(it.w * sx)),
            h: Math.max(4, Math.round(it.h * sy))
          };
        });
        onUpdateManyLayers(patchMap);
        return;
      } else if (s.type === "resize") {
        let nx = s.origX,ny = s.origY,nw = s.origW,nh = s.origH;
        if (s.corner.includes("e")) nw = Math.max(8, Math.round(s.origW + dx));
        if (s.corner.includes("s")) nh = Math.max(8, Math.round(s.origH + dy));
        if (s.corner.includes("w")) {nx = Math.round(s.origX + dx);nw = Math.max(8, Math.round(s.origW - dx));}
        if (s.corner.includes("n")) {ny = Math.round(s.origY + dy);nh = Math.max(8, Math.round(s.origH - dy));}
        // Snap each dragged edge independently to canvas edges/center, split lines, grid, other layers
        const snap = 4;
        const horizSnap = [0, guide.width / 2, guide.width];
        const vertSnap = [0, guide.height / 2, guide.height];
        if (guide.safeAreaShow ?? true) {
          const saR = guide.safeArea || { top: 50, right: 50, bottom: 50, left: 50 };
          horizSnap.push(saR.left, guide.width - saR.right);
          vertSnap.push(saR.top, guide.height - saR.bottom);
        }
        (SPLIT_SEGMENTS[liveRef.current.gridDivision] || []).forEach((s) => {if (s.o === "v") horizSnap.push(s.at * guide.width);else vertSnap.push(s.at * guide.height);});
        if (liveRef.current.showGrid) {
          const G = 20;
          for (let x = 0; x <= guide.width; x += G) horizSnap.push(x);
          for (let y = 0; y <= guide.height; y += G) vertSnap.push(y);
        }
        guide.layers.filter((l) => l.id !== s.id && l.visible !== false).forEach((l) => {
          horizSnap.push(l.x, l.x + l.w / 2, l.x + l.w);
          vertSnap.push(l.y, l.y + l.h / 2, l.y + l.h);
        });
        const nearest = (val, arr) => {let best = null,bd = snap;for (const t of arr) {const d = Math.abs(val - t);if (d < bd) {bd = d;best = t;}}return best;};
        let sv = null,sh = null;
        if (s.corner.includes("e")) {const t = nearest(nx + nw, horizSnap);if (t != null) {nw = Math.max(8, Math.round(t - nx));sv = t;}}
        if (s.corner.includes("w")) {const t = nearest(nx, horizSnap);if (t != null) {nw = Math.max(8, Math.round(nx + nw - t));nx = Math.round(t);sv = t;}}
        if (s.corner.includes("s")) {const t = nearest(ny + nh, vertSnap);if (t != null) {nh = Math.max(8, Math.round(t - ny));sh = t;}}
        if (s.corner.includes("n")) {const t = nearest(ny, vertSnap);if (t != null) {nh = Math.max(8, Math.round(ny + nh - t));ny = Math.round(t);sh = t;}}
        setGuides({ v: sv, h: sh });
        onUpdateLayer(s.id, { x: nx, y: ny, w: nw, h: nh });
      }
    };
    const onUp = () => {
      const s = dragState.current;
      // collapse a group to the clicked member if it was a click (no drag)
      if (s && s.collapseTo && !movedRef.current) onSelectMany([s.collapseTo]);
      dragState.current = null;
      document.body.style.cursor = "";
      setGuides({ v: null, h: null });
      setIsDragging(false);
      gestureEndRef.current?.();
    };
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
    return () => {
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
    };
  }, [guide, zoom, onUpdateLayer, onUpdateManyLayers, onSelectMany]);

  // Sort layers by z (lower z first = back)
  const sortedLayers = useMemo(() => {
    return [...guide.layers].sort((a, b) => (a.z ?? 1) - (b.z ?? 1));
  }, [guide.layers]);
  // 클리핑 마스크 관계 (대상 레이어 clip, 마스크 레이어 집합)
  const maskData = useMemo(() => computeMasks(guide.layers, guide.groups || {}), [guide.layers, guide.groups]);

  const fitScale = useMemo(() => {
    return zoom;
  }, [zoom]);

  // Selection chrome (handles/outline) counter-scales so it stays a constant visual size at
  // ≥100% zoom, and grows a little when zoomed out so it stays grabbable.
  const chromeBoost = fitScale >= 1 ? 1 : 1 + (1 - fitScale) * 0.7;
  const chromeScale = chromeBoost / fitScale;

  // Keep zoom visually centered while leaving BOTH horizontal edges reachable:
  // re-center the scroll position whenever the zoom (and thus content size) changes.
  useLayoutEffect(() => {
    const el = scrollRef.current;
    if (!el) return;
    el.scrollLeft = (el.scrollWidth - el.clientWidth) / 2;
    el.scrollTop = (el.scrollHeight - el.clientHeight) / 2;
  }, [fitScale, guide.width, guide.height]);

  return (
    <div className="canvas-wrap" ref={wrapRef} onClick={(e) => {if (suppressDeselect.current) return;if (tool === "select") onSelect(null);}}>
      {/* Toolbar */}
      <CanvasToolbar zoom={zoom} setZoom={setZoom} showGrid={showGrid} onToggleGrid={onToggleGrid} tool={tool} setTool={setTool} onUndo={onUndo} canUndo={canUndo} onRedo={onRedo} canRedo={canRedo} gridDivision={gridDivision} setGridDivision={setGridDivision} onFit={fitToScreen} />

      <div className={cls("canvas-scroll", panning && "is-panning", creating && "is-creating")} ref={scrollRef} onMouseDown={onCanvasMouseDown}>
      <div className="canvas-stage">
      <div style={{
            position: "relative",
            width: guide.width * fitScale,
            height: guide.height * fitScale,
            flexShrink: 0
          }}>
        {editingName ?
            <>
            <input
                className="artboard-name-input"
                autoFocus
                style={{ width: nameW + 14 + "px" }}
                value={nameDraft}
                onChange={(e) => setNameDraft(e.target.value)}
                onMouseDown={(e) => e.stopPropagation()}
                onClick={(e) => e.stopPropagation()}
                onBlur={() => {const v = nameDraft.trim();onRenameGuide?.(v === "" ? guide.name : v);setEditingName(false);}}
                onKeyDown={(e) => {if (e.key === "Enter") {e.currentTarget.blur();} else if (e.key === "Escape") {setNameDraft(guide.name);setEditingName(false);}}} />
            <span ref={nameMeasureRef} className="artboard-name-measure" aria-hidden="true">{nameDraft || " "}</span>
          </> :

            <div
              className="artboard-name"
              title="더블클릭하여 이름 변경"
              onMouseDown={(e) => e.stopPropagation()}
              onDoubleClick={(e) => {e.stopPropagation();setNameDraft(guide.name);setEditingName(true);}}>
          {guide.width * fitScale < 90 ? "•••" : <>{guide.name}<span className="artboard-name-dim"> ({guide.width} × {guide.height})</span></>}
        </div>
            }
        <div ref={artboardRef} style={{
              position: "absolute",
              top: 0, left: 0,
              width: guide.width, height: guide.height,
              transform: `scale(${fitScale})`,
              transformOrigin: "top left"
            }}>
        <div
                className="artboard"
                style={{ width: guide.width, height: guide.height }}
                onClick={(e) => {e.stopPropagation();if (suppressDeselect.current) return;if (tool === "select") onSelect(null);}}>
              
          <div className="artboard-frame" style={{ position: "absolute", inset: 0 }}>
            {sortedLayers.map((layer) => {
                    const isSel = selectedIds.includes(layer.id);
                    const isEditingThis = editId === layer.id;
                    const showHandles = selectedIds.length === 1 && selectedIds[0] === layer.id && !layer.locked && !isEditingThis;
                    return (
                      <div
                        key={layer.id}
                        onMouseDown={(e) => onLayerMouseDown(e, layer)}
                        onClick={(e) => e.stopPropagation()}
                        onDoubleClick={(e) => onLayerDoubleClick(e, layer)}
                        className={cls("layer-node", isSel && "is-selected", selectedIds.length > 1 && isSel && "is-multi", layer.locked && "is-locked")}
                        style={{
                          position: "absolute",
                          left: layer.x, top: layer.y,
                          width: layer.w, height: layer.h,
                          zIndex: layer.z ?? 1,
                          opacity: layer.visible === false ? 0.25 : 1,
                          cursor: layer.locked ? "not-allowed" : "move",
                          transform: layer.rotation ? `rotate(${layer.rotation}deg)` : undefined,
                          outline: isEditingThis && editEmpty ? "none" : undefined,
                          outlineWidth: isSel && !(isEditingThis && editEmpty) ? `${1.5 * chromeScale}px` : undefined
                        }}>
                  
                <LayerInner
                          layer={layer}
                          editing={editId === layer.id}
                          clip={maskData.targetClip[layer.id]}
                          asMask={maskData.maskIds.has(layer.id)}
                          onResize={(txt) => {setEditEmpty(!(txt || "").trim());const s = measureTextSize({ ...layer, content: txt });onUpdateLayer(layer.id, { w: s.w, h: s.h });}}
                          onCommit={(val) => {const v = (val || "").trim();if (v === "") {onDeleteLayer?.(layer.id);setEditId(null);return;}const s = measureTextSize({ ...layer, content: v });onUpdateLayer(layer.id, { content: v, w: s.w, h: s.h });setEditId(null);}} />
                {showHandles &&
                        <>
                    {/* 모서리 바깥 회전 핸들 (대각선 회전 커서) — 리사이즈 핸들과 동일하게 chromeScale로
                               역보정하여, 모든 줌에서 핸들 대비 일정한 크기·위치(코너 중심 정렬 링)를 유지한다.
                               리사이즈 핸들(zIndex 1005)이 위에 있어 안쪽은 리사이즈, 바깥 링은 회전이 잡힌다. */}
                    <div onMouseDown={(e) => onRotateMouseDown(e, layer)} style={{ position: "absolute", top: -11, left: -11, width: 22, height: 22, cursor: ROT_CUR_NW, zIndex: 1001, transform: `scale(${chromeScale})` }} />
                    <div onMouseDown={(e) => onRotateMouseDown(e, layer)} style={{ position: "absolute", top: -11, right: -11, width: 22, height: 22, cursor: ROT_CUR_NE, zIndex: 1001, transform: `scale(${chromeScale})` }} />
                    <div onMouseDown={(e) => onRotateMouseDown(e, layer)} style={{ position: "absolute", bottom: -11, left: -11, width: 22, height: 22, cursor: ROT_CUR_SW, zIndex: 1001, transform: `scale(${chromeScale})` }} />
                    <div onMouseDown={(e) => onRotateMouseDown(e, layer)} style={{ position: "absolute", bottom: -11, right: -11, width: 22, height: 22, cursor: ROT_CUR_SE, zIndex: 1001, transform: `scale(${chromeScale})` }} />
                    <div className="handle nw" style={{ transform: `scale(${chromeScale})`, cursor: "nwse-resize", zIndex: 1005 }} onMouseDown={(e) => onHandleMouseDown(e, layer, "nw")} />
                    <div className="handle ne" style={{ transform: `scale(${chromeScale})`, cursor: "nesw-resize", zIndex: 1005 }} onMouseDown={(e) => onHandleMouseDown(e, layer, "ne")} />
                    <div className="handle sw" style={{ transform: `scale(${chromeScale})`, cursor: "nesw-resize", zIndex: 1005 }} onMouseDown={(e) => onHandleMouseDown(e, layer, "sw")} />
                    <div className="handle se" style={{ transform: `scale(${chromeScale})`, cursor: "nwse-resize", zIndex: 1005 }} onMouseDown={(e) => onHandleMouseDown(e, layer, "se")} />

                    {/* Edge resize hit-zones (가로/세로 한 방향 리사이즈) */}
                    <div onMouseDown={(e) => onHandleMouseDown(e, layer, "n")} style={{ position: "absolute", top: -4, left: 8, right: 8, height: 8, cursor: "ns-resize", zIndex: 1003 }} />
                    <div onMouseDown={(e) => onHandleMouseDown(e, layer, "s")} style={{ position: "absolute", bottom: -4, left: 8, right: 8, height: 8, cursor: "ns-resize", zIndex: 1003 }} />
                    <div onMouseDown={(e) => onHandleMouseDown(e, layer, "w")} style={{ position: "absolute", left: -4, top: 8, bottom: 8, width: 8, cursor: "ew-resize", zIndex: 1003 }} />
                    <div onMouseDown={(e) => onHandleMouseDown(e, layer, "e")} style={{ position: "absolute", right: -4, top: 8, bottom: 8, width: 8, cursor: "ew-resize", zIndex: 1003 }} />

                    {/* Corner-radius handle (shapes only) */}
                    {layer.type === "shape" && (() => {
                            const maxR = Math.floor(Math.min(layer.w, layer.h) / 2);
                            const c = shapeCorners(layer);
                            const clampOff = (r) => Math.max(Math.min(r, maxR), 11);
                            const corners = [
                            { k: "tl", left: clampOff(c.tl), top: clampOff(c.tl) },
                            { k: "tr", left: layer.w - clampOff(c.tr), top: clampOff(c.tr) },
                            { k: "bl", left: clampOff(c.bl), top: layer.h - clampOff(c.bl) },
                            { k: "br", left: layer.w - clampOff(c.br), top: layer.h - clampOff(c.br) }];

                            return (
                              <>
                          {corners.map((cn) =>
                                <div
                                  key={cn.k}
                                  className="radius-handle"
                                  onMouseDown={(e) => onRadiusMouseDown(e, layer, cn.k)}
                                  style={{ left: cn.left, top: cn.top, transform: `translate(-50%, -50%) scale(${chromeScale})` }} />
                                )}
                          {radiusTip !== null &&
                                <div className="px-badge" style={{ left: "50%", top: "100%", transform: `translate(-50%, 6px) scale(${1 / zoom})`, transformOrigin: "top center" }}>
                              반경 {radiusTip}
                            </div>
                                }
                        </>);
                          })()}

                    {/* Pixel position badge */}
                    {/* Pixel size badge — 이동(move) 중에는 숨김, idle·리사이즈에서만 표시 */}
                    {!(isDragging && dragState.current?.type === "move") &&
                          <div
                            className="px-badge px-badge-size"
                            style={{ transform: `translate(-50%, 6px) scale(${1 / zoom})`, transformOrigin: "top center" }}>
                      
                      {layer.w} × {layer.h}
                    </div>
                          }
                    {/* Edge-distance labels는 layer-node 밖(artboard-frame 레벨)에서 렌더 — smart-guide 위에 표시되도록 */}
                  </>
                        }
              </div>);

                  })}

            {/* Grid overlay */}
            {showGrid &&
                  <svg style={{ position: "absolute", inset: 0, width: "100%", height: "100%", pointerEvents: "none", zIndex: 9999 }}>
                <defs>
                  <pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
                    <path d="M 20 0 L 0 0 0 20" fill="none" stroke="rgba(124,92,255,.15)" strokeWidth="0.5" />
                  </pattern>
                </defs>
                <rect width="100%" height="100%" fill="url(#grid)" />
                <line x1={guide.width / 2} y1="0" x2={guide.width / 2} y2={guide.height} stroke="rgba(124,92,255,.35)" strokeDasharray="4 4" strokeWidth="1" />
                <line x1="0" y1={guide.height / 2} x2={guide.width} y2={guide.height / 2} stroke="rgba(124,92,255,.35)" strokeDasharray="4 4" strokeWidth="1" />
              </svg>
                  }

            {/* Safe-area boundary overlay — 경계 가이드선 (이미지를 자르는 마스크가 아님) */}
            {(guide.safeAreaShow ?? true) && (() => {
                    const sa = guide.safeArea || { top: 50, right: 50, bottom: 50, left: 50 };
                    const x = Math.max(0, sa.left),y = Math.max(0, sa.top);
                    const w = Math.max(0, guide.width - sa.left - sa.right);
                    const h = Math.max(0, guide.height - sa.top - sa.bottom);
                    return (
                      <svg style={{ position: "absolute", inset: 0, width: "100%", height: "100%", pointerEvents: "none", zIndex: 9997 }}>
                  <path d={`M0 0 H${guide.width} V${guide.height} H0 Z M${x} ${y} V${y + h} H${x + w} V${y} Z`} fillRule="evenodd" fill="rgba(46,139,107,0.05)" />
                  <rect x={x} y={y} width={w} height={h} fill="none" stroke="#2E8B6B" strokeWidth={1.5 / fitScale} strokeDasharray={`${5 / fitScale} ${4 / fitScale}`} />
                </svg>);

                  })()}

            {/* Canvas split guides (전체 / 분할 / 비율) */}
            {(SPLIT_SEGMENTS[gridDivision] || []).length > 0 &&
                  <div style={{ position: "absolute", inset: 0, pointerEvents: "none", zIndex: 9998 }}>
                {SPLIT_SEGMENTS[gridDivision].map((s, i) => {
                      const from = s.from ?? 0,to = s.to ?? 1;
                      const style = s.o === "v" ?
                      { position: "absolute", left: `${s.at * 100}%`, top: `${from * 100}%`, height: `${(to - from) * 100}%`, width: 1, marginLeft: -0.5, background: "var(--accent)", opacity: 0.6 } :
                      { position: "absolute", top: `${s.at * 100}%`, left: `${from * 100}%`, width: `${(to - from) * 100}%`, height: 1, marginTop: -0.5, background: "var(--accent)", opacity: 0.6 };
                      return <div key={i} style={style} />;
                    })}
              </div>
                  }

            {/* Smart guides — 두께를 선택 외곽선(#4)과 동일하게 화면상 1.5px로 역보정 */}
            {guides.v !== null && <div className="smart-guide v" style={{ left: guides.v - 0.75 / fitScale, width: 1.5 / fitScale }} />}
            {guides.h !== null && <div className="smart-guide h" style={{ top: guides.h - 0.75 / fitScale, height: 1.5 / fitScale }} />}

            {/* Edge-distance labels — layer-node 밖(여기, smart-guide와 같은 맥락)에서 렌더해
                                                                                                    낮은 z의 layer-node 쌓임 맥락에 갇히지 않고 가이드 선 위에 표시된다. */}
            {(() => {
                    if (!isDragging || !dragState.current?.primaryId) return null;
                    const pl = guide.layers.find((l) => l.id === dragState.current.primaryId);
                    if (!pl) return null;
                    return (
                      <div style={{ position: "absolute", left: pl.x, top: pl.y, width: pl.w, height: pl.h, zIndex: 1003, pointerEvents: "none" }}>
                  <EdgeDistanceLabels layer={pl} guide={guide} zoom={zoom} />
                </div>);

                  })()}

            {/* 멀티 선택(2개 이상) 묶음 박스 — 바운딩 영역 + 크기조절 핸들 + 크기 배지 */}
            {selectedIds.length > 1 && (() => {
                    const box = groupBBox();
                    if (!box) return null;
                    const hideSize = isDragging && dragState.current?.type === "move";
                    return (
                      <div className="group-box" style={{ position: "absolute", left: box.x, top: box.y, width: box.w, height: box.h, zIndex: 1004, pointerEvents: "none", outline: `${1.5 * chromeScale}px solid #4e4cdb` }}>
                  <div className="handle nw" style={{ transform: `scale(${chromeScale})` }} onMouseDown={(e) => onGroupHandleMouseDown(e, "nw")} />
                  <div className="handle ne" style={{ transform: `scale(${chromeScale})` }} onMouseDown={(e) => onGroupHandleMouseDown(e, "ne")} />
                  <div className="handle sw" style={{ transform: `scale(${chromeScale})` }} onMouseDown={(e) => onGroupHandleMouseDown(e, "sw")} />
                  <div className="handle se" style={{ transform: `scale(${chromeScale})` }} onMouseDown={(e) => onGroupHandleMouseDown(e, "se")} />
                  {!hideSize &&
                        <div className="px-badge px-badge-size" style={{ transform: `translate(-50%, 6px) scale(${1 / zoom})`, transformOrigin: "top center" }}>
                      {box.w} × {box.h}
                    </div>
                        }
                </div>);

                  })()}

            {/* Rubber-band preview while drawing a new layer */}
            {draft &&
                  <div className="draft-box" style={{
                    position: "absolute",
                    left: draft.x, top: draft.y, width: draft.w, height: draft.h,
                    border: `${chromeScale}px solid var(--accent)`,
                    background: tool === "shape" ? "#d9d9d9" : "rgba(124,92,255,.10)",
                    pointerEvents: "none", zIndex: 99998
                  }}>
                <div className="px-badge px-badge-size" style={{
                      position: "absolute", left: "50%", top: "100%",
                      transform: `translate(-50%, 6px) scale(${1 / zoom})`, transformOrigin: "top center"
                    }}>
                  {Math.round(draft.w)} × {Math.round(draft.h)}
                </div>
                <div className="handle nw" style={{ transform: `scale(${chromeScale})` }} />
                <div className="handle ne" style={{ transform: `scale(${chromeScale})` }} />
                <div className="handle sw" style={{ transform: `scale(${chromeScale})` }} />
                <div className="handle se" style={{ transform: `scale(${chromeScale})` }} />
              </div>
                  }

            {/* Rubber-band selection band (select tool) */}
            {marquee &&
                  <div style={{
                    position: "absolute",
                    left: marquee.x, top: marquee.y, width: marquee.w, height: marquee.h,
                    border: `${chromeScale}px solid var(--accent)`,
                    background: "rgba(78,76,219,.10)",
                    pointerEvents: "none", zIndex: 99997
                  }} />
                  }
          </div>
        </div>
      </div>
      </div>
      </div>
      </div>
    </div>);

}

// Per-corner radius helpers for shapes (radiusTL/TR/BR/BL override the uniform `radius`)
function shapeCorners(layer) {
  const base = layer.radius || 0;
  return {
    tl: layer.radiusTL ?? base,
    tr: layer.radiusTR ?? base,
    br: layer.radiusBR ?? base,
    bl: layer.radiusBL ?? base
  };
}
function shapeRadiusCss(layer) {
  const c = shapeCorners(layer);
  return `${c.tl}px ${c.tr}px ${c.br}px ${c.bl}px`;
}
function shapeRadiusMixed(layer) {
  const c = shapeCorners(layer);
  return !(c.tl === c.tr && c.tr === c.br && c.br === c.bl);
}

// ===== 클리핑 마스크 (Figma식) =====
// 마스크 레이어(M, isMask=true)는 같은 컨테이너에서 자기 "바로 아래"(낮은 z) 레이어 T를
// 자신의 도형 모양(모서리 반경 포함)대로 잘라낸다. 마스크 자체의 채움은 화면에 그리지 않는다.
function maskClipPathFor(M, T) {
  const it = Math.round(M.y - T.y); // top inset
  const il = Math.round(M.x - T.x); // left inset
  const ir = Math.round(T.x + T.w - (M.x + M.w)); // right inset
  const ib = Math.round(T.y + T.h - (M.y + M.h)); // bottom inset
  const c = shapeCorners(M);
  return `inset(${it}px ${ir}px ${ib}px ${il}px round ${c.tl}px ${c.tr}px ${c.br}px ${c.bl}px)`;
}
// 가이드 전체에서 마스크 관계를 계산.
// 반환: { targetClip: {대상레이어id: clipPath}, maskIds: Set(마스크레이어id), maskTargetId: {마스크id: 대상id} }
function computeMasks(layers, groups) {
  const targetClip = {},maskTargetId = {},maskIds = new Set();
  for (const M of layers) {
    if (!M.isMask || M.type === "bg") continue;
    const sibs = layers.
    filter((l) => (l.groupId || null) === (M.groupId || null) && l.type !== "bg").
    sort((a, b) => (a.z ?? 1) - (b.z ?? 1));
    const idx = sibs.findIndex((l) => l.id === M.id);
    const T = idx > 0 ? sibs[idx - 1] : null;
    if (T && !targetClip[T.id]) {
      targetClip[T.id] = maskClipPathFor(M, T);
      maskTargetId[M.id] = T.id;
      maskIds.add(M.id);
    } else {
      // 아래에 대상이 없으면 마스크는 일반 도형처럼 그대로 표시
    }
  }
  return { targetClip, maskTargetId, maskIds };
}

// Box-shadow string for a shape's outline (외곽선)
function shapeStroke(layer) {
  const parts = [];
  if (layer.strokeOn) {
    const w = layer.strokeWidth || 1;
    const c = layer.strokeColor === "" ? "transparent" : layer.strokeColor || "#000000";
    if (layer.strokePos === "inside") parts.push(`inset 0 0 0 ${w}px ${c}`);else
    if (layer.strokePos === "center") parts.push(`0 0 0 ${w / 2}px ${c}`, `inset 0 0 0 ${w / 2}px ${c}`);else
    parts.push(`0 0 0 ${w}px ${c}`);
  }
  if (layer.shadowOn) parts.push(`0 ${layer.shadowY ?? 4}px ${layer.shadowBlur ?? 12}px rgba(0,0,0,${(layer.shadowOpacity ?? 25) / 100})`);
  return parts.length ? parts.join(", ") : undefined;
}

// 텍스트 레이어 실측: 내용에 맞는 바운딩 박스 크기 계산
function measureTextSize(layer) {
  const el = document.createElement("div");
  el.style.cssText = "position:absolute;visibility:hidden;left:-99999px;top:0;white-space:pre;";
  el.style.fontFamily = layer.font || "Pretendard";
  el.style.fontWeight = String(layer.weight || 400);
  el.style.fontSize = (layer.size || 16) + "px";
  el.style.lineHeight = String(layer.lineHeight || 1.3);
  el.style.letterSpacing = (layer.letterSpacing || 0) + "px";
  el.textContent = layer.content || "";
  document.body.appendChild(el);
  let w = Math.ceil(el.offsetWidth) + 4;
  let h = Math.ceil(el.offsetHeight) + 2;
  document.body.removeChild(el);
  if (layer.bg) {const p = layer.padding || 8;w += p * 1.2 * 2;h += p * 2;}
  return { w: Math.max(8, Math.round(w)), h: Math.max(8, Math.round(h)) };
}

// 마스크/클립을 적용하는 래퍼. 실제 레이어 본체는 LayerBody가 그린다.
function LayerInner({ layer, editing, onCommit, onResize, clip, asMask }) {
  if (asMask) {
    // 마스크 레이어: 자체 채움은 그리지 않고, 도형 외곽선만 점선으로 표시(위치 식별용)
    return <div style={{
      position: "absolute", inset: 0,
      outline: "1.5px dashed rgba(78,76,219,.75)", outlineOffset: -1,
      borderRadius: shapeRadiusCss(layer), pointerEvents: "none"
    }} />;
  }
  if (clip) {
    return <div style={{ position: "absolute", inset: 0, clipPath: clip, WebkitClipPath: clip }}>
      <LayerBody layer={layer} editing={editing} onCommit={onCommit} onResize={onResize} />
    </div>;
  }
  return <LayerBody layer={layer} editing={editing} onCommit={onCommit} onResize={onResize} />;
}

function LayerBody({ layer, editing, onCommit, onResize }) {
  if (layer.type === "bg") return <div style={{ width: "100%", height: "100%", background: layer.color }} />;
  if (layer.type === "shape") return (
    <div style={{
      width: "100%", height: "100%", background: layer.color, borderRadius: shapeRadiusCss(layer),
      boxShadow: shapeStroke(layer),
      display: "flex", alignItems: "center",
      justifyContent: layer.align === "left" ? "flex-start" : layer.align === "right" ? "flex-end" : "center",
      overflow: "hidden"
    }}>
      {layer.content &&
      <span style={{
        fontFamily: layer.font || "Pretendard",
        fontWeight: layer.weight || 600,
        fontSize: layer.size || 16,
        color: layer.textColor === "" ? "transparent" : layer.textColor || "#FFFFFF",
        lineHeight: layer.lineHeight || 1.3,
        letterSpacing: layer.letterSpacing || 0,
        textAlign: layer.align || "center",
        whiteSpace: "pre", padding: "0 10px"
      }}>{layer.content}</span>
      }
    </div>);

  if (layer.type === "logo") return <ImagePlaceholder slot="logo" note={layer.note} />;
  if (layer.type === "image") {
    if (layer.src) return <img src={layer.src} alt="" draggable={false} style={{ width: "100%", height: "100%", objectFit: layer.fit || "cover", display: "block", pointerEvents: "none" }} />;
    return <ImagePlaceholder slot={layer.slot} note={layer.note} />;
  }
  if (layer.type === "text") {
    const hasBg = !!layer.bg;
    return (
      <div style={{
        width: "100%", height: "100%",
        background: hasBg ? layer.bg : "transparent",
        borderRadius: layer.radius || 0,
        padding: hasBg ? `${layer.padding || 8}px ${(layer.padding || 8) * 1.2}px` : 0,
        display: "flex",
        alignItems: hasBg ? "center" : "flex-start",
        justifyContent: layer.align === "center" ? "center" : layer.align === "right" ? "flex-end" : "flex-start",
        overflow: editing ? "visible" : "hidden"
      }}>
        <div className="bn-text" style={{
          fontFamily: layer.font || "Pretendard",
          fontWeight: layer.weight || 400,
          fontSize: layer.size || 16,
          color: layer.color === "" ? "transparent" : layer.color || "#000",
          lineHeight: layer.lineHeight || 1.3,
          letterSpacing: layer.letterSpacing || 0,
          textAlign: layer.align || "left",
          width: editing ? "max-content" : "100%",
          minWidth: editing ? "4px" : undefined,
          whiteSpace: editing ? "pre" : undefined,
          outline: editing ? "none" : undefined,
          cursor: editing ? "text" : undefined
        }}
        contentEditable={editing || undefined}
        suppressContentEditableWarning={true}
        ref={editing ? (el) => {if (el && document.activeElement !== el) {el.textContent = layer.content || "";el.focus();placeCaretEnd(el);}} : undefined}
        onMouseDown={editing ? (e) => e.stopPropagation() : undefined}
        onClick={editing ? (e) => e.stopPropagation() : undefined}
        onInput={editing ? (e) => onResize?.(e.currentTarget.textContent) : undefined}
        onKeyDown={editing ? (e) => {if (e.key === "Escape") {e.preventDefault();e.currentTarget.blur();}} : undefined}
        onBlur={editing ? (e) => onCommit?.(e.currentTarget.textContent) : undefined}>
          {editing ? null : layer.content}
        </div>
      </div>);

  }
  return null;
}

// Edge distance overlay (top/bottom/left/right) with dashed lines + pixel labels
function EdgeDistanceLabels({ layer, guide, zoom }) {
  const inv = 1 / zoom;
  const top = layer.y;
  const left = layer.x;
  const right = guide.width - (layer.x + layer.w);
  const bottom = guide.height - (layer.y + layer.h);
  // 세이프 영역 점선과 동일한 굵기·점선 패턴으로 통일 (strokeWidth 1.5/줌, dash 5/4)
  const thick = 1.5 * inv; // ≈ 세이프 영역과 동일한 1.5px
  const dash = 5 * inv,gap = 4 * inv; // ≈ 세이프 영역과 동일한 5px dash / 4px gap
  const vbg = `repeating-linear-gradient(to bottom, #E3714A 0 ${dash}px, transparent ${dash}px ${dash + gap}px)`;
  const hbg = `repeating-linear-gradient(to right, #E3714A 0 ${dash}px, transparent ${dash}px ${dash + gap}px)`;

  return (
    <>
      <div className="dist-line vert" style={{ top: -top, left: layer.w / 2, height: top, width: thick, marginLeft: -thick / 2, background: vbg }}>
        <span className="dist-label" style={{ transform: `translate(-50%, -50%) scale(${inv})` }}>
          {top}
        </span>
      </div>
      <div className="dist-line vert" style={{ top: layer.h, left: layer.w / 2, height: bottom, width: thick, marginLeft: -thick / 2, background: vbg }}>
        <span className="dist-label" style={{ transform: `translate(-50%, -50%) scale(${inv})` }}>
          {bottom}
        </span>
      </div>
      <div className="dist-line horiz" style={{ left: -left, top: layer.h / 2, width: left, height: thick, marginTop: -thick / 2, background: hbg }}>
        <span className="dist-label" style={{ transform: `translate(-50%, -50%) scale(${inv})` }}>
          {left}
        </span>
      </div>
      <div className="dist-line horiz" style={{ left: layer.w, top: layer.h / 2, width: right, height: thick, marginTop: -thick / 2, background: hbg }}>
        <span className="dist-label" style={{ transform: `translate(-50%, -50%) scale(${inv})` }}>
          {right}
        </span>
      </div>
    </>);

}

// Canvas split presets → line segments. Each: { o:"v"|"h", at:fraction, from?:fraction, to?:fraction }
// o="v" → vertical line at x=at (from/to along Y). o="h" → horizontal line at y=at (from/to along X).
// from/to default to the full 0..1 span. Compound presets (T/L/quad) use partial segments.
const SPLIT_SEGMENTS = {
  full: [],
  "2": [{ o: "v", at: 1 / 2 }],
  "2h": [{ o: "h", at: 1 / 2 }],
  "3": [{ o: "v", at: 1 / 3 }, { o: "v", at: 2 / 3 }],
  "4": [{ o: "v", at: 1 / 4 }, { o: "v", at: 1 / 2 }, { o: "v", at: 3 / 4 }],
  "1:2": [{ o: "v", at: 1 / 3 }],
  "1:2h": [{ o: "h", at: 1 / 3 }],
  "2:1": [{ o: "v", at: 2 / 3 }],
  "2:1h": [{ o: "h", at: 2 / 3 }],
  // 상1 · 하2 — 가로 전체 1/2 + 하단에만 세로 1/2
  T: [{ o: "h", at: 1 / 2 }, { o: "v", at: 1 / 2, from: 1 / 2, to: 1 }],
  // 상2 · 하1 — 가로 전체 1/2 + 상단에만 세로 1/2
  T2: [{ o: "h", at: 1 / 2 }, { o: "v", at: 1 / 2, from: 0, to: 1 / 2 }],
  // 좌2 · 우1 — 세로 전체 1/2 + 좌측에만 가로 1/2
  L: [{ o: "v", at: 1 / 2 }, { o: "h", at: 1 / 2, from: 0, to: 1 / 2 }],
  // 좌1 · 우2 — 세로 전체 1/2 + 우측에만 가로 1/2
  L2: [{ o: "v", at: 1 / 2 }, { o: "h", at: 1 / 2, from: 1 / 2, to: 1 }],
  // 2×2 균등 4분할 — 세로 1/2 + 가로 1/2
  quad: [{ o: "v", at: 1 / 2 }, { o: "h", at: 1 / 2 }]
};

// Small preview icon: a frame split at the given preset's segments
function splitIcon(key) {
  const segs = SPLIT_SEGMENTS[key] || [];
  const X = (f) => 2 + 12 * f;
  const Y = (f) => 3.5 + 9 * f;
  return (
    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.3" style={{ display: "block" }}>
      <rect x="2" y="3.5" width="12" height="9" rx="1.5" />
      {segs.map((s, i) => s.o === "v" ?
      <line key={i} x1={X(s.at)} y1={Y(s.from ?? 0)} x2={X(s.at)} y2={Y(s.to ?? 1)} /> :
      <line key={i} x1={X(s.from ?? 0)} y1={Y(s.at)} x2={X(s.to ?? 1)} y2={Y(s.at)} />
      )}
    </svg>);

}

function ToolBtn({ tip, keyHint, active, onClick, children, disabled }) {
  return (
    <button type="button" className={cls("tool-btn", active && "is-active")} disabled={disabled} onClick={(e) => {onClick?.(e);e.currentTarget.blur();}}>
      {children}
      {tip &&
      <span className="tb-tip">
          {tip}{keyHint && <span className="tb-tip-key">{keyHint}</span>}
        </span>
      }
    </button>);

}

function CanvasToolbar({ zoom, setZoom, showGrid, onToggleGrid, tool, setTool, onUndo, canUndo, onRedo, canRedo, gridDivision, setGridDivision, onFit }) {
  const [gridMenuOpen, setGridMenuOpen] = useState(false);
  const splitOpts = [{ v: "full", label: "전체" }, { v: "2", label: "2분할 (세로)" }, { v: "2h", label: "2분할 (가로)" }, { v: "3", label: "3분할 (세로)" }, { v: "T", label: "3분할 (상1·하2)" }, { v: "T2", label: "3분할 (상2·하1)" }, { v: "L", label: "3분할 (좌2·우1)" }, { v: "L2", label: "3분할 (좌1·우2)" }, { v: "4", label: "4분할 (세로)" }, { v: "quad", label: "4분할 (2x2)" }, { v: "1:2", label: "1:2 비율 (세로)" }, { v: "1:2h", label: "1:2 비율 (가로)" }, { v: "2:1", label: "2:1 비율 (세로)" }, { v: "2:1h", label: "2:1 비율 (가로)" }];
  // Close the split menu when clicking anywhere outside the grid group (the fixed backdrop
  // can't be used here — the toolbar's transform makes position:fixed relative to itself).
  useEffect(() => {
    if (!gridMenuOpen) return;
    const onDown = (e) => {if (!e.target.closest(".tb-grid-group")) setGridMenuOpen(false);};
    document.addEventListener("mousedown", onDown);
    return () => document.removeEventListener("mousedown", onDown);
  }, [gridMenuOpen]);
  return (
    <div className="canvas-bar">
      <div className="tb-group">
        <ToolBtn tip="선택" keyHint="V" active={tool === "select"} onClick={() => setTool?.("select")}><I.Cursor size={16} /></ToolBtn>
        <ToolBtn tip="손 도구" keyHint="Space" active={tool === "hand"} onClick={() => setTool?.("hand")}><I.Hand size={16} /></ToolBtn>
        <span className="divider" />
        <ToolBtn tip="이미지" keyHint="I" active={tool === "image"} onClick={() => setTool?.("image")}><I.Image size={16} /></ToolBtn>
        <ToolBtn tip="텍스트" keyHint="T" active={tool === "text"} onClick={() => setTool?.("text")}><I.Text size={16} /></ToolBtn>
        <ToolBtn tip="로고" keyHint="L" active={tool === "logo"} onClick={() => setTool?.("logo")}><I.Logo size={16} /></ToolBtn>
        <ToolBtn tip="도형" keyHint="R" active={tool === "shape"} onClick={() => setTool?.("shape")}><I.Square size={16} /></ToolBtn>
      </div>
      <span className="divider tb-sep" />
      <div className="tb-group tb-grid-group">
        <ToolBtn tip="그리드" keyHint="⇧G" active={showGrid} onClick={() => onToggleGrid?.()}><I.Grid size={15} /></ToolBtn>
        <button type="button" className={cls("tb-caret-btn", gridMenuOpen && "is-open")} title="화면 분할" onClick={() => setGridMenuOpen((o) => !o)}>
          <I.Chevron size={10} />
        </button>
        {gridMenuOpen &&
        <>
            <div className="tb-split-menu">
              {splitOpts.map((o) =>
            <button key={o.v} type="button" className={cls("tb-split-opt", gridDivision === o.v && "is-active")} onClick={() => {setGridDivision(o.v);setGridMenuOpen(false);}}>
                  <span className="tb-split-check">{gridDivision === o.v && <I.Check size={12} stroke="#fff" />}</span>
                  <span className="tb-split-ic">{splitIcon(o.v)}</span>
                  <span className="tb-split-label" style={{ fontSize: "10px" }}>{o.label}</span>
                </button>
            )}
            </div>
          </>
        }
      </div>
      <span className="divider tb-sep" />
      <div className="tb-group">
        <ToolBtn tip="되돌리기" keyHint="⌘Z" disabled={!canUndo} onClick={() => onUndo?.()}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><path d="M9 14L4 9l5-5" /><path d="M4 9h11a5 5 0 0 1 0 10h-1" /></svg>
        </ToolBtn>
        <ToolBtn tip="다시 실행" keyHint="⇧⌘Z" disabled={!canRedo} onClick={() => onRedo?.()}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><path d="M15 14l5-5-5-5" /><path d="M20 9H9a5 5 0 0 0 0 10h1" /></svg>
        </ToolBtn>
      </div>
      <span className="divider tb-sep" />
      <div className="tb-group">
        <ToolBtn tip="축소" keyHint="−" onClick={() => setZoom(zoomOut(zoom))}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="7" /><path d="M21 21l-4.3-4.3M8 11h6" /></svg>
        </ToolBtn>
        <ToolBtn tip="확대" keyHint="+" onClick={() => setZoom(zoomIn(zoom))}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="7" /><path d="M21 21l-4.3-4.3M11 8v6M8 11h6" /></svg>
        </ToolBtn>
        <ToolBtn tip="화면 맞춤" keyHint="⇧1" onClick={() => onFit?.()}>
          <span style={{ fontSize: "13px", fontWeight: "500" }}>Fit</span>
        </ToolBtn>
      </div>
    </div>);

}

// ---------- Left panel: Categories + Guide list + Layers ----------
// Category swatch color choices (shared with the add-category palette)
const CATEGORY_PALETTE = ["#7c5cff", "#4ea8ff", "#1ec997", "#ff9f43", "#ff6b8b", "#9b6bff", "#22b8cf"];

function LeftPanel({ categories, activeCategory, setActiveCategory, guides, activeGuideId, setActiveGuideId, selectedIds, selectedGroupIds = [], onSelectLayer, onAddCategory, onAddGuide, onRenameCategory, onRenameGuide, onRenameLayer, onRenameGroup, onToggleGroupLock, onToggleGroupVis, onToggleGroupCollapse, onUngroupGroup, onSelectGroup, onNestGroup, onMoveLayerToGroup, onMoveLayerBeforeGroup, onGroup, onDeleteGroup, onDeleteCategory, onDuplicateCategory, onDeleteGuide, onAddLayer, onToggleVis, onToggleLock, onReorder, onDeleteLayer, onDuplicateLayer, onCopyLayers, onPasteLayers, onToggleMask, onSetCategoryColor, onDuplicateGuide, onMoveGuideToCategory, demoStep, canAddCategory }) {
  const filtered = guides.filter((g) => g.category === activeCategory);
  const activeGuide = guides.find((g) => g.id === activeGuideId);
  // Show layers ordered by z desc (top-most first)
  const grps = activeGuide && activeGuide.groups || {};
  const allLayers = activeGuide ? activeGuide.layers : [];
  const orderedLayers = [...allLayers].sort((a, b) => (b.z ?? 1) - (a.z ?? 1)); // 레이어 개수 표시용
  const groupColor = (gid) => CATEGORY_PALETTE[[...String(gid)].reduce((a, c) => a + c.charCodeAt(0), 0) % CATEGORY_PALETTE.length];
  // 그룹 z = 하위 레이어 최대 z (형제 정렬용)
  const groupZ = {};
  Object.keys(grps).forEach((gid) => {
    const mem = layersInGroup(allLayers, grps, gid);
    groupZ[gid] = mem.length ? Math.max(...mem.map((l) => l.z ?? 0)) : 0;
  });
  // 컨테이너(gid|null) 직속 자식(하위 그룹 + 직속 레이어)을 z 내림차순으로
  const childrenOf = (gid) => {
    const subGroups = Object.keys(grps).filter((k) => (grps[k].parent || null) === gid && layersInGroup(allLayers, grps, k).length > 0).map((k) => ({ kind: "group", gid: k, z: groupZ[k] ?? 0 }));
    const directLayers = allLayers.filter((l) => (l.groupId || null) === gid).map((l) => ({ kind: "layer", layer: l, z: l.z ?? 1 }));
    return [...subGroups, ...directLayers].sort((a, b) => b.z - a.z);
  };
  // 중첩 트리를 평탄화한 렌더 행 (depth 포함, 접힌 그룹의 하위는 제외)
  const rows = (() => {
    const out = [];
    const walk = (gid, depth) => {
      childrenOf(gid).forEach((node) => {
        if (node.kind === "group") {
          out.push({ kind: "group", gid: node.gid, depth });
          if (!(grps[node.gid] || {}).collapsed) walk(node.gid, depth + 1);
        } else {
          out.push({ kind: "layer", layer: node.layer, depth });
        }
      });
    };
    if (activeGuide) walk(null, 0);
    return out;
  })();
  const [dragId, setDragId] = useState(null);
  // 레이어/그룹 드래그 시 브라우저 기본 고스트(반투명 DOM 스냅샷)가 캔버스 위로 떠다니며
  // 잔상처럼 보이는 현상을 막기 위해, 깔끔한 커스텀 드래그 칩으로 교체한다.
  const setLayerDragImage = (e, label) => {
    try {
      const chip = document.createElement("div");
      chip.textContent = label || "레이어";
      chip.style.cssText = "position:fixed;top:-1000px;left:-1000px;z-index:99999;padding:5px 10px;border-radius:6px;background:#4E4CDB;color:#fff;font-size:12px;font-weight:600;font-family:Pretendard,system-ui,sans-serif;line-height:1;white-space:nowrap;box-shadow:0 4px 14px rgba(0,0,0,.18);pointer-events:none;";
      document.body.appendChild(chip);
      e.dataTransfer.setDragImage(chip, 12, 14);
      setTimeout(() => chip.remove(), 0);
    } catch (_) {}
  };
  const [dragGroupId, setDragGroupId] = useState(null); // 드래그 중인 그룹 헤더 gid
  const [overGroupId, setOverGroupId] = useState(null); // 드롭 대상으로 하이라이트할 그룹 gid
  const [overGroupPos, setOverGroupPos] = useState(null); // 'before'(형제로 앞) | 'into'(그룹에 합류)
  const [colorPickId, setColorPickId] = useState(null); // category whose color popover is open
  const [guideMenu, setGuideMenu] = useState(null); // { id, x, y } context menu for a guide
  const [catMenu, setCatMenu] = useState(null); // { id, x, y } context menu for a category
  const [layerMenu, setLayerMenu] = useState(null); // { id, x, y } context menu for a layer
  const [groupMenu, setGroupMenu] = useState(null); // { gid, x, y } context menu for a group header
  const [dragGuide, setDragGuide] = useState(null); // guide id being dragged to a category
  const [dropCat, setDropCat] = useState(null); // category id currently hovered as drop target
  useEffect(() => {
    if (!guideMenu && !catMenu && !layerMenu && !groupMenu) return;
    const close = () => {setGuideMenu(null);setCatMenu(null);setLayerMenu(null);setGroupMenu(null);};
    window.addEventListener("mousedown", close);
    window.addEventListener("scroll", close, true);
    return () => {window.removeEventListener("mousedown", close);window.removeEventListener("scroll", close, true);};
  }, [guideMenu, catMenu, layerMenu, groupMenu]);
  const [overId, setOverId] = useState(null);
  const [overPos, setOverPos] = useState(null); // 'above' | 'below' — which edge of overId to show the insert line
  const [editing, setEditing] = useState(null); // { type: "cat"|"guide"|"layer", id }
  const [editVal, setEditVal] = useState("");
  const isCollapsed = (gid) => !!((activeGuide && activeGuide.groups || {})[gid] || {}).collapsed;
  const isEditing = (type, id) => editing && editing.type === type && editing.id === id;
  const startEdit = (type, id, name) => {setEditing({ type, id });setEditVal(name);};
  const commitEdit = () => {
    if (!editing) return;
    const v = editVal.trim();
    if (v) {
      if (editing.type === "cat") onRenameCategory?.(editing.id, v);else
      if (editing.type === "guide") onRenameGuide?.(editing.id, v);else
      if (editing.type === "group") onRenameGroup?.(editing.id, v);else
      if (editing.type === "layer") onRenameLayer?.(editing.id, v);
    }
    setEditing(null);
  };
  const editKey = (e) => {
    if (e.key === "Enter") {e.preventDefault();commitEdit();} else
    if (e.key === "Escape") setEditing(null);
  };

  return (
    <aside className="panel left">
      <div className="panel-section">
        <div className="panel-title" style={{ fontWeight: "700", fontSize: "11px", color: "rgb(30, 30, 30)" }}>
          카테고리
          {categories.length > 0 && <span style={{ marginLeft: 4, color: "rgb(17, 17, 17)", fontWeight: "500", fontSize: "11px" }}>({categories.length})</span>}
          <button
            className="btn ghost sm icon-only"
            style={{ marginLeft: "auto" }}
            title={canAddCategory ? "새 카테고리 추가" : "모든 카테고리가 추가됨"}
            onClick={onAddCategory}
            disabled={!canAddCategory}>
            
            <I.Plus size={12} />
          </button>
        </div>
        {categories.length === 0 ?
        <div className="panel-empty">
            <div className="panel-empty-body" style={{ fontSize: "12px", fontWeight: "600" }}><span className="inline-add-chip"><I.Plus size={11} /></span> 버튼을 눌러<br />첫 카테고리를 추가하세요.</div>
          </div> :

        <div className="cat-list">
            {categories.map((c) =>
          <div
            key={c.id}
            className={cls("cat-item", activeCategory === c.id && "is-active", dropCat === c.id && "is-drop-cat")}
            onClick={() => setActiveCategory(c.id)}
            onContextMenu={(e) => {e.preventDefault();e.stopPropagation();setCatMenu({ id: c.id, x: e.clientX, y: e.clientY });}}
            onDragOver={(e) => {if (dragGuide) {e.preventDefault();setDropCat(c.id);}}}
            onDragLeave={() => setDropCat((d) => d === c.id ? null : d)}
            onDrop={(e) => {if (dragGuide) {e.preventDefault();onMoveGuideToCategory?.(dragGuide, c.id);setDragGuide(null);setDropCat(null);}}}
            style={{ backgroundColor: activeCategory === c.id ? "rgb(243, 243, 243)" : undefined, height: "32px" }}>
            
                <span className="cat-swatch-wrap" style={{ position: "relative", display: "inline-flex" }}>
                  <button
                type="button"
                className="swatch"
                title="색상 변경"
                style={{ background: c.color, cursor: "pointer", border: 0, padding: 0 }}
                onClick={(e) => {e.stopPropagation();setColorPickId((id) => id === c.id ? null : c.id);}} />
                  {colorPickId === c.id &&
              <>
                      <div className="cat-color-backdrop" onClick={(e) => {e.stopPropagation();setColorPickId(null);}} onMouseDown={(e) => e.stopPropagation()} />
                      <div className="cat-color-pop" onClick={(e) => e.stopPropagation()}>
                        {CATEGORY_PALETTE.map((col) =>
                  <button
                    key={col}
                    type="button"
                    className={cls("cat-color-opt", (c.color || "").toLowerCase() === col.toLowerCase() && "is-active")}
                    style={{ background: col }}
                    onClick={() => {onSetCategoryColor?.(c.id, col);setColorPickId(null);}}>
                            {(c.color || "").toLowerCase() === col.toLowerCase() && <I.Check size={11} stroke={readableOn(col)} />}
                          </button>
                  )}
                        <label className="cat-color-custom" style={{ background: c.color }} title="사용자 지정 색상">
                          <input type="color" value={c.color} onChange={(e) => onSetCategoryColor?.(c.id, e.target.value)} />
                        </label>
                      </div>
                    </>
              }
                </span>
                {isEditing("cat", c.id) ?
            <input
              className="rename-input" autoFocus
              value={editVal}
              onChange={(e) => setEditVal(e.target.value)}
              onBlur={commitEdit} onKeyDown={editKey}
              onClick={(e) => e.stopPropagation()} /> :

            <span className="cat-name" style={{ color: "#1e1e1e" }} onDoubleClick={(e) => {e.stopPropagation();startEdit("cat", c.id, c.name);}}>{c.name}</span>
            }
                <span className="count">{guides.filter((g) => g.category === c.id).length}</span>
                <button
              className="cat-del"
              title="카테고리 삭제"
              onClick={(e) => {e.stopPropagation();onDeleteCategory(c);}}>
              
                  <I.Trash size={12} />
                </button>
              </div>
          )}
          </div>
        }
      </div>

      {activeCategory &&
      <div className="panel-section">
          <div className="panel-title" style={{ fontWeight: "700", fontSize: "11px", color: "rgb(30, 30, 30)" }}>
            가이드
            <span style={{ marginLeft: 4, color: "rgb(17, 17, 17)", fontWeight: "500", fontSize: "11px" }}>({filtered.length})</span>
            <button
            className="btn ghost sm icon-only"
            style={{ marginLeft: "auto" }}
            title="새로 만들기"
            onClick={onAddGuide}>
            
              <I.Plus size={12} />
            </button>
          </div>
          {filtered.length === 0 ?
        <div className="panel-empty">
              <div className="panel-empty-body" style={{ fontSize: "12px", fontWeight: "600" }}><span className="inline-add-chip"><I.Plus size={11} /></span> 버튼을 눌러<br />빈 캔버스에서 시작하세요.</div>
            </div> :

        <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
              {filtered.map((g) =>
          <div
            key={g.id}
            draggable
            onDragStart={(e) => {setDragGuide(g.id);e.dataTransfer.effectAllowed = "move";}}
            onDragEnd={() => {setDragGuide(null);setDropCat(null);}}
            onContextMenu={(e) => {e.preventDefault();e.stopPropagation();setGuideMenu({ id: g.id, x: e.clientX, y: e.clientY });}}
            onClick={() => setActiveGuideId(g.id)}
            className={cls("guide-item", activeGuideId === g.id && "is-active", dragGuide === g.id && "is-dragging")}
            style={{ ...{
                padding: "8px 8px",
                borderRadius: 5,
                background: activeGuideId === g.id ? "#F3F3F3" : undefined,
                border: activeGuideId === g.id ? "1px solid rgba(124,92,255,.3)" : "1px solid transparent",
                cursor: "pointer",
                display: "flex", alignItems: "center", gap: 6
              }, border: "1px solid rgba(124, 92, 255, 0)" }}>
            
                  <div style={{ flex: 1, minWidth: 0 }}>
                    {isEditing("guide", g.id) ?
              <input
                className="rename-input" autoFocus
                value={editVal}
                onChange={(e) => setEditVal(e.target.value)}
                onBlur={commitEdit} onKeyDown={editKey}
                onClick={(e) => e.stopPropagation()}
                style={{ marginBottom: 2 }} /> :

              <div style={{ fontSize: 12, fontWeight: 500, color: "var(--text-0)", marginBottom: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} onDoubleClick={(e) => {e.stopPropagation();startEdit("guide", g.id, g.name);}}>{g.name}</div>
              }
                    <div style={{ fontSize: 10.5, color: "var(--text-3)", display: "flex", gap: 6, alignItems: "center" }}>
                      <span>{g.width}×{g.height}</span>
                      <span>·</span>
                      <span>{g.used}회 사용</span>
                    </div>
                  </div>
                  <button
              className="guide-del"
              title="가이드 삭제"
              onClick={(e) => {e.stopPropagation();onDeleteGuide(g);}}>
              
                    <I.Trash size={12} />
                  </button>
                </div>
          )}
            </div>
        }
        </div>
      }

      {activeGuide &&
      <div className="panel-section scroll">
          <div className="panel-title">
            <span style={{ fontSize: "11px", color: "rgb(30, 30, 30)" }}>레이어 <span style={{ color: "rgb(17, 17, 17)", fontWeight: 500, fontSize: "11px" }}>({orderedLayers.filter((l) => l.type !== "bg").length})</span></span>
          </div>
          <div className="layer-list">
            {rows.map((row, idx) => {
            if (row.kind === "group") {
              const gid = row.gid;
              const gname = ((activeGuide.groups || {})[gid] || {}).name || "그룹";
              const coll = isCollapsed(gid);
              const gmembers = layersInGroup(allLayers, grps, gid);
              // 그룹 헤더 하이라이트: 멤버가 전부 선택됐고, (멤버 2개 이상이거나 그룹 자체를 직접 선택했을 때).
              // 멤버 1개짜리 그룹은 그 레이어만 선택하면 하이라이트 안 되고, 그룹 헤더를 직접 클릭해야 하이라이트됨.
              const groupSel = gmembers.length > 0 && gmembers.every((m) => selectedIds.includes(m.id)) && (gmembers.length > 1 || selectedGroupIds.includes(gid));
              const gLocked = gmembers.length > 0 && gmembers.every((m) => m.locked);
              const gHidden = gmembers.length > 0 && gmembers.every((m) => m.visible === false);
              const canDropHere = dragGroupId && dragGroupId !== gid && !isGroupInside(grps, gid, dragGroupId) || dragId && !dragGroupId;
              return (
                <div key={"g-" + gid} className={cls("layer-item", "layer-group-head", groupSel && "is-selected", overGroupId === gid && overGroupPos === "into" && "drop-into", overGroupId === gid && overGroupPos === "before" && "drop-above")}
                draggable
                style={{ display: "flex", alignItems: "center", gap: 6, position: "relative", paddingLeft: 8 + row.depth * 14 }}
                onDragStart={(e) => {e.stopPropagation();setDragGroupId(gid);setDragId(null);e.dataTransfer.effectAllowed = "move";setLayerDragImage(e, gname);}}
                onDragOver={(e) => {
                  if (!canDropHere) return;
                  e.preventDefault();
                  const r = e.currentTarget.getBoundingClientRect();
                  const pos = e.clientY < r.top + r.height * 0.4 ? "before" : "into";
                  setOverGroupId(gid);setOverGroupPos(pos);setOverId(null);setOverPos(null);
                }}
                onDragLeave={() => setOverGroupId((o) => o === gid ? null : o)}
                onDrop={(e) => {
                  e.preventDefault();e.stopPropagation();
                  const before = overGroupPos === "before";
                  if (dragGroupId && dragGroupId !== gid && !isGroupInside(grps, gid, dragGroupId)) {
                    onNestGroup?.(dragGroupId, before ? (grps[gid] || {}).parent || null : gid);
                  } else if (dragId && !dragGroupId) {
                    if (before) onMoveLayerBeforeGroup?.(dragId, gid);else onMoveLayerToGroup?.(dragId, gid);
                  }
                  setDragId(null);setDragGroupId(null);setOverGroupId(null);setOverGroupPos(null);setOverId(null);setOverPos(null);
                }}
                onDragEnd={() => {setDragId(null);setDragGroupId(null);setOverGroupId(null);setOverGroupPos(null);setOverId(null);setOverPos(null);}}
                onClick={(e) => onSelectGroup?.(gid, e.shiftKey || e.metaKey || e.ctrlKey)}
                onDoubleClick={(e) => {e.stopPropagation();startEdit("group", gid, gname);}}
                onContextMenu={(e) => {e.preventDefault();e.stopPropagation();setGroupMenu({ gid, x: e.clientX, y: e.clientY });}}>
                  <button className="grp-collapse" onClick={(e) => {e.stopPropagation();onToggleGroupCollapse?.(gid);}} style={{ position: "absolute", left: 8 + row.depth * 14 - 17, top: "50%", transform: "translateY(-50%)", background: "transparent", border: 0, padding: 0, cursor: "pointer", color: "var(--text-3)", display: "flex" }}>
                    <I.Chevron size={11} style={{ transform: coll ? "rotate(-90deg)" : "none", transition: "transform .1s" }} />
                  </button>
                  <span className="icon" style={{ padding: 0, width: 18, display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
                    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" strokeDasharray="3 3"><rect x="3" y="3" width="18" height="18" rx="2" /></svg>
                  </span>
                  {isEditing("group", gid) ?
                  <input className="rename-input" autoFocus value={editVal} onChange={(e) => setEditVal(e.target.value)} onBlur={commitEdit} onKeyDown={editKey} onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} style={{ flex: 1, minWidth: 0 }} /> :
                  <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1, color: "#1e1e1e", fontWeight: "400" }}>{gname}</span>}
                  <span style={{ display: "flex", gap: "6px", marginLeft: "auto" }}>
                    <button className="lock" onClick={(e) => {e.stopPropagation();onToggleGroupLock?.(gid);}} style={{ background: "transparent", border: 0, padding: 2, cursor: "pointer", color: "rgb(179, 179, 179)" }}>
                      {gLocked ? <I.Lock size={12} /> : <I.Unlock size={12} />}
                    </button>
                    <button className="vis" onClick={(e) => {e.stopPropagation();onToggleGroupVis?.(gid);}} style={{ background: "transparent", border: 0, padding: 2, cursor: "pointer", color: "var(--text-3)" }}>
                      {gHidden ? <I.EyeOff size={12} /> : <I.Eye size={12} />}
                    </button>
                  </span>
                </div>);
            }
            const l = row.layer;
            return (
              <div
                key={l.id}
                draggable={l.type !== "bg"}
                style={{ paddingLeft: 8 + row.depth * 14 }}
                onDragStart={(e) => {e.stopPropagation();setDragId(l.id);setDragGroupId(null);e.dataTransfer.effectAllowed = "move";setLayerDragImage(e, l.name);}}
                onDragOver={(e) => {
                  if (l.type === "bg") return;
                  if (dragGroupId) {
                    // 그룹을 이 레이어가 속한 컨테이너로 이동(중첩/이탈) — 자기/자기하위 금지
                    const into = l.groupId || null;
                    if (into && isGroupInside(grps, into, dragGroupId)) return;
                    e.preventDefault();
                    const r = e.currentTarget.getBoundingClientRect();
                    setOverId(l.id);setOverPos(e.clientY < r.top + r.height / 2 ? "above" : "below");setOverGroupId(null);
                  } else if (dragId && dragId !== l.id) {
                    e.preventDefault();
                    const r = e.currentTarget.getBoundingClientRect();
                    setOverId(l.id);setOverPos(e.clientY < r.top + r.height / 2 ? "above" : "below");
                  }
                }}
                onDragLeave={() => setOverId((o) => o === l.id ? null : o)}
                onDrop={(e) => {
                  e.preventDefault();
                  if (l.type !== "bg" && dragGroupId) {
                    const into = l.groupId || null;
                    if (!(into && isGroupInside(grps, into, dragGroupId))) onNestGroup?.(dragGroupId, into);
                  } else if (dragId && l.type !== "bg" && dragId !== l.id) {
                    onReorder?.(dragId, l.id, overPos);
                  }
                  setDragId(null);setDragGroupId(null);setOverId(null);setOverPos(null);setOverGroupId(null);
                }}
                onDragEnd={() => {setDragId(null);setDragGroupId(null);setOverId(null);setOverPos(null);setOverGroupId(null);}}
                className={cls("layer-item", selectedIds.includes(l.id) && "is-selected", dragId === l.id && "is-dragging", overId === l.id && overPos === "above" && "drop-above", overId === l.id && overPos === "below" && "drop-below")}
                onClick={(e) => onSelectLayer(l.id, e.shiftKey || e.metaKey || e.ctrlKey, !!(l.groupId && (activeGuide.groups || {})[l.groupId]))}
                onContextMenu={(e) => {if (l.type === "bg") return;e.preventDefault();e.stopPropagation();onSelectLayer(l.id, false, true);setLayerMenu({ id: l.id, x: e.clientX, y: e.clientY });}}
                onDoubleClick={(e) => {if (l.type === "bg") return;e.stopPropagation();startEdit("layer", l.id, l.name);}}>
            
                <span className="icon" style={{ padding: "0px" }}>
                  {l.isMask ? <I.Mask size={13} /> : <>
                  {l.type === "image" && <I.Image size={13} />}
                  {l.type === "text" && <I.Type size={13} />}
                  {l.type === "logo" && <I.Logo size={13} />}
                  {l.type === "shape" && <I.Square size={13} />}
                  {l.type === "bg" && <I.Frame size={13} />}
                  </>}
                </span>
                {isEditing("layer", l.id) ?
                <input
                  className="rename-input" autoFocus
                  value={editVal}
                  onChange={(e) => setEditVal(e.target.value)}
                  onBlur={commitEdit} onKeyDown={editKey}
                  onClick={(e) => e.stopPropagation()}
                  onMouseDown={(e) => e.stopPropagation()}
                  style={{ flex: 1, minWidth: 0 }} /> :

                <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "rgb(30, 30, 30)" }} onDoubleClick={(e) => {e.stopPropagation();startEdit("layer", l.id, l.name);}}><span style={{ color: "#1e1e1e" }}>{l.name}</span></span>
                }
                <span style={{ display: "flex", gap: "6px" }}>
                  <button
                    className={cls("lock", l.locked && "on")}
                    onClick={(e) => {e.stopPropagation();onToggleLock(l.id);}}
                    style={{ ...{ background: "transparent", border: 0, padding: 2, cursor: "pointer", color: l.locked ? "var(--accent-2)" : "var(--text-3)" }, color: "rgb(179, 179, 179)" }}>
                
                    {l.locked ? <I.Lock size={12} /> : <I.Unlock size={12} />}
                  </button>
                  <button
                    className={cls("vis", l.visible === false && "on")}
                    onClick={(e) => {e.stopPropagation();onToggleVis(l.id);}}
                    style={{ background: "transparent", border: 0, padding: 2, cursor: "pointer", color: "var(--text-3)" }}>
                
                    {l.visible === false ? <I.EyeOff size={12} /> : <I.Eye size={12} />}
                  </button>
                </span>
              </div>);
          })}
          </div>
        </div>
      }
      {guideMenu && ReactDOM.createPortal(
        <div className="guide-ctx" style={{ left: guideMenu.x, top: guideMenu.y }} onMouseDown={(e) => e.stopPropagation()}>
          <button type="button" onClick={() => {onDuplicateGuide?.(guideMenu.id);setGuideMenu(null);}}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="11" height="11" rx="2" /><path d="M5 15V5a2 2 0 0 1 2-2h10" /></svg>
            복제
          </button>
          <button type="button" className="danger" onClick={() => {const g = guides.find((x) => x.id === guideMenu.id);setGuideMenu(null);if (g) onDeleteGuide(g);}}>
            <I.Trash size={13} /> 삭제
          </button>
        </div>,
        document.body
      )}
      {catMenu && ReactDOM.createPortal(
        <div className="guide-ctx" style={{ left: catMenu.x, top: catMenu.y }} onMouseDown={(e) => e.stopPropagation()}>
          <button type="button" onClick={() => {onDuplicateCategory?.(catMenu.id);setCatMenu(null);}}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="11" height="11" rx="2" /><path d="M5 15V5a2 2 0 0 1 2-2h10" /></svg>
            복제
          </button>
          <button type="button" className="danger" onClick={() => {const c = categories.find((x) => x.id === catMenu.id);setCatMenu(null);if (c) onDeleteCategory(c);}}>
            <I.Trash size={13} /> 삭제
          </button>
        </div>,
        document.body
      )}
      {layerMenu && ReactDOM.createPortal((() => {
        const lm = activeGuide && activeGuide.layers.find((x) => x.id === layerMenu.id) || {};
        return (
          <div className="guide-ctx" style={{ left: layerMenu.x, top: layerMenu.y }} onMouseDown={(e) => e.stopPropagation()}>
            <button type="button" onClick={() => {onCopyLayers?.([layerMenu.id]);setLayerMenu(null);}}>복사</button>
            <button type="button" onClick={() => {onPasteLayers?.();setLayerMenu(null);}}>붙여넣기</button>
            <div className="ctx-sep" />
            <button type="button" onClick={() => {onToggleVis?.(layerMenu.id);setLayerMenu(null);}}>{lm.visible === false ? "보이기" : "감추기"}</button>
            <button type="button" onClick={() => {onToggleLock?.(layerMenu.id);setLayerMenu(null);}}>{lm.locked ? "잠금 해제" : "잠금"}</button>
            <div className="ctx-sep" />
            <button type="button" onClick={() => {onGroup?.();setLayerMenu(null);}}>그룹으로 묶기</button>
            {lm.groupId && <button type="button" onClick={() => {const p = ((activeGuide && activeGuide.groups || {})[lm.groupId] || {}).parent || null;onMoveLayerToGroup?.(layerMenu.id, p);setLayerMenu(null);}}>그룹에서 빼기</button>}
            {(() => {
              const layers = activeGuide && activeGuide.layers || [];
              const sibs = layers.
              filter((l) => (l.groupId || null) === (lm.groupId || null) && l.type !== "bg").
              sort((a, b) => (a.z ?? 1) - (b.z ?? 1));
              const idx = sibs.findIndex((l) => l.id === lm.id);
              const hasBelow = idx > 0;
              if (lm.isMask)
              return <>
                <div className="ctx-sep" />
                <button type="button" onClick={() => {onToggleMask?.(lm.id);setLayerMenu(null);}}>마스크 해제</button>
              </>;

              if (lm.type !== "bg" && hasBelow)
              return <>
                <div className="ctx-sep" />
                <button type="button" onClick={() => {onToggleMask?.(lm.id);setLayerMenu(null);}}>마스크로 사용</button>
              </>;

              return null;
            })()}
            <div className="ctx-sep" />
            <button type="button" className="danger" onClick={() => {const id = layerMenu.id;setLayerMenu(null);onDeleteLayer?.(id);}}>삭제</button>
          </div>);
      })(),
      document.body
      )}
      {groupMenu && ReactDOM.createPortal(
        <div className="guide-ctx" style={{ left: groupMenu.x, top: groupMenu.y }} onMouseDown={(e) => e.stopPropagation()}>
          <button type="button" onClick={() => {const ids = activeGuide ? layersInGroup(activeGuide.layers, activeGuide.groups || {}, groupMenu.gid).map((l) => l.id) : [];onCopyLayers?.(ids);setGroupMenu(null);}}>복사</button>
          <button type="button" onClick={() => {onPasteLayers?.();setGroupMenu(null);}}>붙여넣기</button>
          <div className="ctx-sep" />
          <button type="button" onClick={() => {onUngroupGroup?.(groupMenu.gid);setGroupMenu(null);}}>그룹 해제</button>
          <div className="ctx-sep" />
          <button type="button" onClick={() => {onToggleGroupVis?.(groupMenu.gid);setGroupMenu(null);}}>감추기</button>
          <button type="button" onClick={() => {onToggleGroupLock?.(groupMenu.gid);setGroupMenu(null);}}>잠금</button>
          <div className="ctx-sep" />
          <button type="button" className="danger" onClick={() => {const gid = groupMenu.gid;setGroupMenu(null);onDeleteGroup?.(gid);}}>삭제</button>
        </div>,
        document.body
      )}
    </aside>);

}

// Zoom control for the inspector tabs row: editable % input + level dropdown
function ZoomControl({ zoom, setZoom }) {
  const [open, setOpen] = useState(false);
  const [draft, setDraft] = useState(null);
  const pct = Math.round((zoom || 1) * 100);
  const commit = (str) => {
    const n = parseInt(String(str).replace(/[^0-9]/g, ""), 10);
    if (!isNaN(n) && n > 0) setZoom(Math.max(0.25, Math.min(6, n / 100)));
  };
  const levels = [...ZOOM_LEVELS].reverse();
  return (
    <div className="zoom-ctl">
      <input
        className="zoom-input" type="text" inputMode="numeric"
        value={draft !== null ? draft : String(pct)}
        onFocus={(e) => {setDraft(String(pct));e.target.select();}}
        onClick={(e) => e.currentTarget.select()}
        onChange={(e) => setDraft(e.target.value.replace(/[^0-9]/g, ""))}
        onBlur={() => {commit(draft);setDraft(null);}}
        onKeyDown={(e) => {if (e.key === "Enter") e.currentTarget.blur();}} />
      <span className="zoom-pct">%</span>
      <button type="button" className="zoom-caret" onClick={() => setOpen((o) => !o)}></button>
      {open &&
      <>
          <div className="zoom-backdrop" onMouseDown={() => setOpen(false)} />
          <div className="zoom-menu">
            {levels.map((l) =>
          <button key={l} type="button" className={cls("zoom-opt", Math.abs((zoom || 1) - l) < 1e-4 && "is-active")} onClick={() => {setZoom(l);setOpen(false);}} style={{ fontSize: "12px" }}>
                {Math.round(l * 100)}%
              </button>
          )}
          </div>
        </>
      }
    </div>);

}

// 그룹 "옵션 설정" — 편집기에서 그룹을 사용자 화면 컴트롤 명세로 태깅한다. (스키마 single source 의 편집 UI)
function OptionSettings({ gid, group, onUpdateGroup }) {
  const g = group || {};
  const up = (patch) => onUpdateGroup && onUpdateGroup(gid, patch);
  const Seg = ({ label, field, value, options }) => (
    <div style={{ marginBottom: 11 }}>
      <div className="panel-title" style={{ marginBottom: 6 }}>{label}</div>
      <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
        {options.map((o) => {
          const on = value === o.v;
          return (
            <button key={o.v} onClick={() => up({ [field]: o.v })}
              style={{ fontSize: 11, fontWeight: 600, padding: "6px 11px", borderRadius: 6, cursor: "pointer",
                border: "1px solid " + (on ? "#4E4CDB" : "#e6e6e6"),
                background: on ? "#edecfc" : "#fff", color: on ? "#3831a6" : "#757575" }}>
              {o.l}
            </button>);
        })}
      </div>
    </div>);
  const method = g.method || "one";
  const showCount = method === "count" || method === "multi";
  return (
    <div className="panel-section" style={{ background: "#fafaff", margin: "0 -14px", padding: "12px 14px", borderTop: "1px solid #ececf4", borderBottom: "1px solid #ececf4" }}>
      <div className="panel-title" style={{ fontWeight: 700, color: "#3831a6", display: "flex", alignItems: "center", gap: 6, marginBottom: 12 }}>
        <I.Settings size={13} /> 옵션 설정 <span style={{ marginLeft: "auto", fontSize: 10, fontWeight: 500, color: "#b3b3b3" }}>사용자 화면 연동</span>
      </div>
      <Seg label="역할" field="role" value={g.role || "optional"} options={[{ v: "required", l: "필수" }, { v: "optional", l: "선택" }]} />
      <Seg label="선택 방식" field="method" value={method} options={[{ v: "one", l: "택1" }, { v: "multi", l: "다중" }, { v: "count", l: "개수" }, { v: "toggle", l: "토글" }]} />
      <Seg label="입력 타입" field="input" value={g.input || "none"} options={[{ v: "none", l: "없음" }, { v: "image", l: "이미지" }, { v: "text", l: "텍스트" }, { v: "color", l: "색상" }]} />
      {showCount &&
        <div style={{ marginBottom: 11 }}>
          <div className="panel-title" style={{ marginBottom: 6 }}>개수 (최소 · 최대)</div>
          <div className="row grid-2">
            <NumField label="min" value={g.min ?? 0} onChange={(v) => up({ min: Math.max(0, Math.round(v)) })} />
            <NumField label="max" value={g.max ?? 1} onChange={(v) => up({ max: Math.max(1, Math.round(v)) })} />
          </div>
        </div>}
      <div>
        <div className="panel-title" style={{ marginBottom: 6 }}>적용 유형 <span style={{ fontWeight: 400, color: "#b3b3b3" }}>(제약 · 비우면 항상)</span></div>
        <input value={g.appliesTo || ""} placeholder="예: 누끼형, 풀이미지형"
          onChange={(e) => up({ appliesTo: e.target.value })}
          style={{ width: "100%", fontSize: 12, padding: "7px 9px", border: "1px solid #e6e6e6", borderRadius: 6, fontFamily: "inherit", color: "#1e1e1e", boxSizing: "border-box" }} />
      </div>
    </div>);
}

// ---------- Right inspector ----------
function RightInspector({ guide, layer, selectedLayers, onDeleteSelected, onUpdateGuide, onUpdateLayer, onDeleteLayer, onChangeZ, onUploadSample, demoStep, brandColors, sizePresets, onAlign, onSetGap, onGroup, onUngroup, onUpdateGroup, zoom, setZoom }) {
  if (!guide) {
    return (
      <aside className="panel right"></aside>);

  }
  // Multi-selection — show a group panel
  if (selectedLayers && selectedLayers.length > 1) {
    const deletable = selectedLayers.filter((l) => l.type !== "bg" && !l.locked).length;
    const anyGrouped = selectedLayers.some((l) => l.groupId);
    // 레이어간 간격 계산 — 그룹은 하나의 단위(강체)로 취급해 주축/평균간격 판별
    const gParts = selectedLayers.filter((l) => l.type !== "bg");
    let gapInfo = null,gapOverlap = false;
    if (gParts.length >= 2) {
      const gUnits = selectionUnits(guide.layers, guide.groups || {}, new Set(gParts.map((l) => l.id)));
      if (gUnits.length >= 2) {
        // X축으로 겹치면 세로 배치, Y축으로 겹치면 가로 배치. 두 축 모두 겹치면(단위가 겹침) 간격 개념 없음 → 숨김
        const xOverlap = Math.min(...gUnits.map((u) => u.maxX)) - Math.max(...gUnits.map((u) => u.minX));
        const yOverlap = Math.min(...gUnits.map((u) => u.maxY)) - Math.max(...gUnits.map((u) => u.minY));
        const overlapping = xOverlap > 0 && yOverlap > 0;
        gapOverlap = overlapping;
        if (!overlapping) {
          const horizontal = yOverlap > xOverlap;
          const sorted = [...gUnits].sort((a, b) => horizontal ? a.minX - b.minX : a.minY - b.minY);
          const gaps = [];
          for (let i = 1; i < sorted.length; i++) {
            const pv = sorted[i - 1],cu = sorted[i];
            gaps.push(Math.round(horizontal ? cu.minX - pv.maxX : cu.minY - pv.maxY));
          }
          const avg = Math.round(gaps.reduce((a, b) => a + b, 0) / gaps.length);
          const uniform = gaps.every((g) => Math.abs(g - gaps[0]) <= 1);
          gapInfo = { horizontal, avg, uniform };
        }
      }
    }
    return (
      <aside className="panel right">
        <div className="ri-head">
        <div className="tabs">
          <button className="is-active">가이드</button>
          <button>샘플</button>
          <button>버전</button>
          <ZoomControl zoom={zoom} setZoom={setZoom} />
        </div>
        </div>
        <div className="ri-body">
        <div className="panel-section">
          <div className="row" style={{ justifyContent: "space-between" }}>
            <span style={{ display: "flex", alignItems: "center", gap: 6, fontWeight: 500 }}>
              <I.Layers size={13} /> {selectedLayers.length}개 레이어 선택됨
            </span>
            {deletable > 0 &&
              <button className="btn ghost sm danger" title="선택 레이어 삭제" onClick={onDeleteSelected}><I.Trash size={12} /></button>
              }
          </div>
        </div>
        <div className="panel-section">
          <div className="row" style={{ gap: 8 }}>
            <button className="btn sm" style={{ flex: 1, justifyContent: "center" }} onClick={() => onGroup?.()}><I.Layers size={12} /> 그룹 만들기</button>
            {anyGrouped &&
              <button className="btn sm" style={{ flex: 1, justifyContent: "center" }} onClick={() => onUngroup?.()}>그룹 해제</button>}
          </div>
        </div>
        {(() => {
          const optGid = representedGroup(guide, selectedLayers);
          if (!optGid) return null;
          return <OptionSettings gid={optGid} group={(guide.groups || {})[optGid]} onUpdateGroup={onUpdateGroup} />;
        })()}
        <div className="panel-section">
          <div className="panel-title">정렬</div>
          <div className="row grid-2">
            <div className="seg align-seg" style={{ backgroundColor: "rgb(245, 245, 245)" }}>
              <button title="왼쪽 정렬" onClick={() => onAlign?.("left")}>{alignIcon("left")}</button>
              <span className="align-seg-div" />
              <button title="가로 가운데" onClick={() => onAlign?.("centerH")}>{alignIcon("centerH")}</button>
              <span className="align-seg-div" />
              <button title="오른쪽 정렬" onClick={() => onAlign?.("right")}>{alignIcon("right")}</button>
            </div>
            <div className="seg align-seg" style={{ backgroundColor: "rgb(245, 245, 245)" }}>
              <button title="위쪽 정렬" onClick={() => onAlign?.("top")}>{alignIcon("top")}</button>
              <span className="align-seg-div" />
              <button title="세로 가운데" onClick={() => onAlign?.("middleV")}>{alignIcon("middleV")}</button>
              <span className="align-seg-div" />
              <button title="아래쪽 정렬" onClick={() => onAlign?.("bottom")}>{alignIcon("bottom")}</button>
            </div>
          </div>
        </div>
        <div className="panel-section">
          <div className="panel-title">간격</div>
          {gapInfo ?
            <>
              <div className="row" style={{ alignItems: "center", gap: 8 }}>
                <NumField label={gapIcon(gapInfo.horizontal)} value={gapInfo.avg} suffix="px" onChange={(v) => onSetGap?.(Math.round(v))} />
              </div>
              {!gapInfo.uniform &&
              <div className="brand-hint" style={{ marginTop: 8 }}>간격이 균일하지 않아요. 값을 입력하면 첫 레이어 기준으로 균등 배치됩니다.</div>}
            </> :
            gapOverlap ?
            <div className="brand-hint">오브젝트가 겹쳐 있어 간격을 조정할 수 없습니다.</div> :
            <div className="brand-hint">간격을 조정하려면 2개 이상의 레이어를 선택하세요.</div>}
        </div>
        <div className="panel-section">
          <div className="panel-title">선택 항목</div>
          <div className="multi-list">
            {selectedLayers.map((l) =>
              <div key={l.id} className="multi-list-item">
                <span className="icon">
                  {l.type === "image" && <I.Image size={12} />}
                  {l.type === "text" && <I.Type size={12} />}
                  {l.type === "logo" && <I.Logo size={12} />}
                  {l.type === "shape" && <I.Square size={12} />}
                  {l.type === "bg" && <I.Frame size={12} />}
                </span>
                <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{l.name}</span>
                <span className="multi-pos">{l.x}, {l.y}</span>
              </div>
              )}
          </div>
        </div>
        </div>
      </aside>);

  }
  if (!layer) {
    return (
      <aside className="panel right">
        <div className="ri-head">
          <div className="tabs">
            <button className="is-active">가이드</button>
            <button>샘플</button>
            <button>버전</button>
            <ZoomControl zoom={zoom} setZoom={setZoom} />
          </div>
        </div>
        <div className="ri-body">
        <div className="panel-section">
          <div className="panel-title" style={{ fontWeight: "700", color: "#1e1e1e" }}>크기</div>
          <div className="row grid-2">
            <NumField label="W" value={guide.width} onChange={(v) => onUpdateGuide({ width: v })} />
            <NumField label="H" value={guide.height} onChange={(v) => onUpdateGuide({ height: v })} />
          </div>
          <div>
            {(() => {
                const presets = sizePresets && sizePresets.length ? sizePresets : [{ w: 1200, h: 480 }, { w: 600, h: 600 }, { w: 750, h: 280 }];
                const opts = presets.map((p) => ({ value: `${p.w}x${p.h}`, label: `${p.w} × ${p.h}` }));
                const curVal = `${guide.width}x${guide.height}`;
                if (!opts.some((o) => o.value === curVal)) opts.unshift({ value: curVal, label: `${guide.width} × ${guide.height} · 사용자 지정` });
                return (
                  <DSelect
                    value={curVal}
                    options={opts}
                    onChange={(v) => {const [w, h] = v.split("x").map(Number);onUpdateGuide({ width: w, height: h });}} />);

              })()}
          </div>
        </div>
        <div className="panel-section">
          <div className="panel-title" style={{ fontWeight: "700", color: "#1e1e1e", display: "flex", alignItems: "center" }}>
            세이프 영역
            <span style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 6 }}>
              <span className="label" style={{ fontSize: "10px" }}>경계선 표시</span>
              <Toggle checked={guide.safeAreaShow ?? true} onChange={(v) => onUpdateGuide({ safeAreaShow: v })} />
            </span>
          </div>
          {(() => {
              const sa = guide.safeArea || { top: 50, right: 50, bottom: 50, left: 50 };
              const upd = (patch) => onUpdateGuide({ safeArea: { ...sa, ...patch } });
              return (
                <>
                <div className="row grid-2">
                  <NumField label="상" value={sa.top} onChange={(v) => upd({ top: Math.max(0, Math.round(v)) })} suffix="px" />
                  <NumField label="하" value={sa.bottom} onChange={(v) => upd({ bottom: Math.max(0, Math.round(v)) })} suffix="px" />
                </div>
                <div className="row grid-2" style={{ marginTop: 6 }}>
                  <NumField label="좌" value={sa.left} onChange={(v) => upd({ left: Math.max(0, Math.round(v)) })} suffix="px" />
                  <NumField label="우" value={sa.right} onChange={(v) => upd({ right: Math.max(0, Math.round(v)) })} suffix="px" />
                </div>
                <div className="brand-hint" style={{ marginTop: 8, background: "none", border: 0, borderRadius: 0, padding: 0, marginBottom: 0 }}>주요 이미지(상품·모델)가 이 경계 안에 들어오도록 자동 배치됩니다. 경계선은 배치 기준일 뿐, 이미지를 자르지 않습니다.

                </div>
                <div style={{ borderTop: "1px solid var(--border)", margin: "12px 0 0" }} />
                <div className="row" style={{ justifyContent: "space-between", alignItems: "center", gap: 10, marginTop: 10 }}>
                  <span className="label" style={{ fontWeight: 700, color: "#1e1e1e" }}>배경 AI 자동 생성</span>
                  <Toggle checked={guide.aiBgFill ?? true} onChange={(v) => onUpdateGuide({ aiBgFill: v })} />
                </div>
                <div className="brand-hint" style={{ marginTop: 3, background: "none", border: 0, borderRadius: 0, padding: 0, marginBottom: 0 }}>업로드한 이미지의 배경을 분석해, 세이프 영역 바깥을 어울리는 배경으로 자동 확장합니다.</div>
              </>);

            })()}
        </div>
        <div className="panel-section">
          <div className="panel-title" style={{ fontWeight: "700", color: "#1e1e1e" }}>색상</div>
          {(() => {
              const bg = guide.layers.find((l) => l.type === "bg");
              return (
                <ColorField
                  value={bg?.color ?? "#FFFFFF"}
                  onChange={(v) => onUpdateGuide({ layers: guide.layers.map((l) => l.type === "bg" ? { ...l, color: v } : l) })}
                  brandColors={brandColors} />);

            })()}
        </div>
        <div className="panel-section">
          <div className="panel-title" style={{ color: "#1e1e1e", fontWeight: "700" }}>메타데이터</div>
          <div className="row" style={{ justifyContent: "space-between" }}>
            <span className="label">담당자</span>
            <span style={{ fontSize: 12, color: "var(--text-1)" }}>{guide.author}</span>
          </div>
          <div className="row" style={{ justifyContent: "space-between" }}>
            <span className="label">사용 횟수</span>
            <span style={{ fontSize: 12, color: "var(--text-1)" }}>{guide.used}회</span>
          </div>
          <div className="row" style={{ justifyContent: "space-between" }}>
            <span className="label">최근 수정</span>
            <span style={{ fontSize: 12, color: "var(--text-1)" }}>{guide.updatedAt}</span>
          </div>
        </div>
        <div className="panel-section">
          <div className="panel-title" style={{ fontWeight: "700", color: "#1e1e1e" }}>가이드 샘플 이미지</div>
          <SamplePreview guide={guide} />
          <button
              className="btn"
              style={{ width: "100%", justifyContent: "center" }}
              onClick={onUploadSample}
              disabled={guide.sampleUploaded}>
            
            <I.Upload size={12} /> {guide.sampleUploaded ? "샘플 이미지 업로드됨" : "샘플 이미지 업로드"}
          </button>
        </div>
        <div className="panel-section">
          <div className="panel-title" style={{ fontWeight: "700", color: "#1e1e1e" }}>자동 처리 규칙</div>
          <div className="row" style={{ justifyContent: "space-between" }}>
            <span className="label">텍스트 자동 줄바꿈</span>
            <Toggle checked />
          </div>
          <div className="row" style={{ justifyContent: "space-between" }}>
            <span className="label">대비 자동 조정</span>
            <Toggle />
          </div>
        </div>
        </div>
      </aside>);

  }

  // Layer is selected
  return (
    <aside className="panel right">
      <div className="ri-head">
        <div className="tabs">
          <button className="is-active">가이드</button>
          <button>샘플</button>
          <button>버전</button>
            <ZoomControl zoom={zoom} setZoom={setZoom} />
        </div>
        </div>
        <div className="ri-body">
        <div className="panel-section">
          <div className="row" style={{ justifyContent: "space-between" }}>
            <span style={{ display: "flex", alignItems: "center", gap: 6, fontWeight: "700" }}>
              {layer.type === "image" && <I.Image size={13} />}
              {layer.type === "text" && <I.Type size={13} />}
              {layer.type === "logo" && <I.Logo size={13} />}
              {layer.type === "shape" && <I.Square size={13} />}
              {layer.type === "bg" && <I.Frame size={13} />}
              {layer.name}
            </span>
            {!layer.locked && layer.type !== "bg" &&
            <button className="btn ghost sm danger" onClick={() => onDeleteLayer(layer.id)}><I.Trash size={12} /></button>
            }
            {layer.type === "bg" &&
            <span style={{ marginLeft: "auto", fontSize: 11, color: "var(--text-3)" }}>삭제 불가</span>
            }
          </div>
        </div>

        {/* Position (fixed) */}
        {layer.type !== "bg" &&
        <div className="panel-section">
          <div className="panel-title" style={{ fontSize: "11px", fontWeight: "700", color: "rgb(30, 30, 30)" }}>위치</div>
          <div className="row grid-2">
            <NumField label="X" value={layer.x} onChange={(v) => onUpdateLayer({ x: v })} />
            <NumField label="Y" value={layer.y} onChange={(v) => onUpdateLayer({ y: v })} />
          </div>
          <div className="row grid-2">
            <NumField label="∠" value={Math.round(layer.rotation || 0)} suffix="°" onChange={(v) => onUpdateLayer({ rotation: (Math.round(v) % 360 + 360) % 360 })} />
            <div />
          </div>
        </div>
        }

        {/* Size (scroll starts here) */}
        <div className="panel-section">
          <div className="panel-title" style={{ fontSize: "11px", fontWeight: "700", color: "rgb(30, 30, 30)" }}>크기</div>
          <div className="row grid-2">
            <NumField label="W" value={layer.w} onChange={(v) => onUpdateLayer({ w: v })} />
            <NumField label="H" value={layer.h} onChange={(v) => onUpdateLayer({ h: v })} />
          </div>
        </div>

        {/* Type-specific */}
        {layer.type === "bg" &&
        <div className="panel-section">
            <div className="panel-title" style={{ fontSize: "11px", fontWeight: "700", color: "rgb(30, 30, 30)" }}>색상</div>
            <ColorField value={layer.color ?? "#FFFFFF"} onChange={(v) => onUpdateLayer({ color: v })} brandColors={brandColors} />
          </div>
        }
        {layer.type === "text" && <TextInspector layer={layer} onUpdate={onUpdateLayer} brandColors={brandColors} />}
        {layer.type === "image" && <ImageInspector layer={layer} onUpdate={onUpdateLayer} />}
        {layer.type === "logo" && <LogoInspector layer={layer} onUpdate={onUpdateLayer} />}
        {layer.type === "shape" && <ShapeInspector layer={layer} onUpdate={onUpdateLayer} brandColors={brandColors} />}
      </div>
    </aside>);

}

function TextInspector({ layer, onUpdate, brandColors }) {
  return (
    <>
      <div className="panel-section">
        <div className="panel-title">타이포그래피</div>
        <DSelect value={layer.font || "Pretendard"} options={FONT_OPTIONS} onChange={(v) => onUpdate({ font: v })} />
        <div className="row grid-2">
          <DSelect value={layer.weight || 400} options={WEIGHT_OPTIONS} onChange={(v) => onUpdate({ weight: v })} />
          <SizeField value={layer.size || 16} onChange={(v) => onUpdate({ size: v })} />
        </div>
        <div className="row grid-2">
          <NumField label={lineHeightIcon()} value={layer.lineHeight || 1.3} step={0.05} onChange={(v) => onUpdate({ lineHeight: v })} />
          <NumField label={letterSpacingIcon()} value={layer.letterSpacing || 0} step={0.1} onChange={(v) => onUpdate({ letterSpacing: v })} />
        </div>
        <div className="seg" style={{ backgroundColor: "rgb(245, 245, 245)" }}>
          <button className={layer.align === "left" || !layer.align ? "is-active" : ""} onClick={() => onUpdate({ align: "left" })}><I.Align.Left size={13} /></button>
          <button className={layer.align === "center" ? "is-active" : ""} onClick={() => onUpdate({ align: "center" })}><I.Align.Center size={13} /></button>
          <button className={layer.align === "right" ? "is-active" : ""} onClick={() => onUpdate({ align: "right" })}><I.Align.Right size={13} /></button>
        </div>
      </div>
      <div className="panel-section">
        <div className="panel-title">색상</div>
        <ColorField label="텍스트" value={layer.color ?? ""} placeholder="없음" onChange={(v) => onUpdate({ color: v })} clearable brandColors={brandColors} />
        <ColorField label="배경" value={layer.bg || ""} placeholder="없음" onChange={(v) => onUpdate({ bg: v })} clearable brandColors={brandColors} />
        {layer.bg &&
        <div className="row grid-2">
            <NumField label="R" value={layer.radius || 0} onChange={(v) => onUpdate({ radius: v })} />
            <NumField label="P" value={layer.padding || 0} onChange={(v) => onUpdate({ padding: v })} />
          </div>
        }
      </div>
    </>);

}

function ImageInspector({ layer, onUpdate }) {
  return (
    <>
      <div className="panel-section">
        <div className="panel-title" style={{ fontWeight: "700", color: "rgb(30, 30, 30)" }}>타입</div>
        <div className="seg" style={{ backgroundColor: "rgb(245, 245, 245)" }}>
          <button className={layer.slot === "product" || !layer.slot ? "is-active" : ""} onClick={() => onUpdate({ slot: "product" })}>상품</button>
          <button className={layer.slot === "deco" ? "is-active" : ""} onClick={() => onUpdate({ slot: "deco" })}>데코</button>
          <button className={layer.slot === "bg" ? "is-active" : ""} onClick={() => onUpdate({ slot: "bg" })}>배경</button>
        </div>
        <div className="label" style={{ marginTop: 4 }}>설명 (사용자에게 노출)</div>
        <textarea className="textarea input" rows={2} value={layer.note || ""} placeholder="예: 상품 정면 컷, 누끼 처리"
        onChange={(e) => onUpdate({ note: e.target.value })} style={{ height: "48px", padding: "6px 8px 0px" }} />
      </div>
      <div className="panel-section">
        <div className="panel-title" style={{ fontWeight: "700", color: "rgb(30, 30, 30)" }}>자동 처리</div>
        <div className="row" style={{ justifyContent: "space-between" }}>
          <span className="label">누끼 자동</span>
          <Toggle checked={layer.autoCutout ?? true} onChange={(v) => onUpdate({ autoCutout: v })} />
        </div>
        <div className="row" style={{ justifyContent: "space-between" }}>
          <span className="label">자동 리사이즈</span>
          <Toggle checked={layer.autoResize ?? true} onChange={(v) => onUpdate({ autoResize: v })} />
        </div>
        <div className="row" style={{ justifyContent: "space-between" }}>
          <span className="label">그림자 추가</span>
          <Toggle checked={layer.shadow ?? false} onChange={(v) => onUpdate({ shadow: v })} />
        </div>
        <div style={{ borderTop: "1px solid var(--border)", margin: "10px 0 2px" }} />
        <div className="label" style={{ marginTop: 4, fontWeight: "700", color: "rgb(30, 30, 30)" }}>피팅 방식</div>
        <div className="seg" style={{ backgroundColor: "rgb(245, 245, 245)" }}>
          <button className="is-active">Contain</button>
          <button>Cover</button>
          <button>Fill</button>
        </div>
      </div>
    </>);

}

function LogoInspector({ layer, onUpdate }) {
  const lt = layer.logoType || "horizontal";
  return (
    <React.Fragment>
      <div className="panel-section">
        <div className="panel-title" style={{ color: "#1e1e1e", fontWeight: "700" }}>로고 타입</div>
        <div className="seg" style={{ backgroundColor: "rgb(245, 245, 245)" }}>
          <button className={lt === "horizontal" ? "is-active" : ""} onClick={() => onUpdate({ logoType: "horizontal" })}>가로형</button>
          <button className={lt === "vertical" ? "is-active" : ""} onClick={() => onUpdate({ logoType: "vertical" })}>세로형</button>
          <button className={lt === "brand" ? "is-active" : ""} onClick={() => onUpdate({ logoType: "brand" })}>브랜드 로고</button>
        </div>
      </div>
      <div className="panel-section">
        <div className="panel-title" style={{ color: "#1e1e1e", fontWeight: "700" }}>로고 이미지</div>
        {lt === "brand" ?
        <div className="micro-note">브랜드 설정에 등록된 로고가 자동으로 사용됩니다.</div> :
        <React.Fragment>
            <button className="btn" style={{ width: "100%", justifyContent: "center" }}>
              <I.Upload size={12} /> 로고 SVG / PNG 업로드
            </button>
            <div className="micro-note">{lt === "vertical" ? "세로형" : "가로형"} 로고 슬롯은 이미지 업로드만 가능합니다.</div>
          </React.Fragment>}
      </div>
    </React.Fragment>);

}

// Font-size field: typeable input + preset-size dropdown (combo)
function SizeField({ value, onChange, options = [10, 11, 12, 13, 14, 15, 16, 20, 24, 32] }) {
  const [str, setStr] = useState(String(value));
  const focused = useRef(false);
  useEffect(() => {if (!focused.current) setStr(String(value));}, [value]);
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState(null);
  const wrapRef = useRef(null);
  const handle = (e) => {
    const raw = e.target.value.replace(/[^0-9.]/g, "");
    setStr(raw);
    const n = parseFloat(raw);
    if (!isNaN(n)) onChange(n);
  };
  const blur = () => {
    focused.current = false;
    const n = parseFloat(str);
    const f = isNaN(n) ? value : Math.round(n);
    setStr(String(f));
    onChange(f);
  };
  const openMenu = () => {
    const r = wrapRef.current.getBoundingClientRect();
    const H = Math.min(options.length * 30 + 60, 300);
    const top = r.bottom + 4 + H > window.innerHeight ? Math.max(8, r.top - 4 - H) : r.bottom + 4;
    setPos({ left: r.left, top, width: r.width });
    setOpen(true);
  };
  const inList = options.includes(Number(value));
  return (
    <div className="input-wrap size-field" ref={wrapRef}>
      <input
        type="text" inputMode="decimal"
        className="input size-input"
        value={str}
        onFocus={() => {focused.current = true;}}
        onBlur={blur}
        onKeyDown={(e) => {if (e.key === "Enter") e.currentTarget.blur();}}
        onChange={handle} />
      <button type="button" className="size-caret" onClick={openMenu} tabIndex={-1}><I.Chevron size={11} /></button>
      {open && pos && ReactDOM.createPortal(
        <>
          <div className="dsel-backdrop" onMouseDown={() => setOpen(false)} />
          <div className="dsel-menu size-menu" style={{ left: pos.left, top: pos.top, minWidth: pos.width }}>
            {!inList &&
            <>
                <button type="button" className="dsel-opt is-active" onClick={() => setOpen(false)}>
                  <span className="dsel-check"><I.Check size={13} stroke="#fff" /></span>{value}
                </button>
                <div className="dsel-sep" />
              </>
            }
            {options.map((o) => {
              const active = Number(value) === o;
              return (
                <button key={o} type="button" className={cls("dsel-opt", active && "is-active")} onClick={() => {onChange(o);setOpen(false);}} style={{ fontSize: "12px" }}>
                  <span className="dsel-check">{active && <I.Check size={13} stroke="#fff" />}</span>{o}
                </button>);

            })}
          </div>
        </>,
        document.body
      )}
    </div>);

}

// Object-alignment glyphs (relative to the selection bounds), centered in a 16×16 box
function alignIcon(mode) {
  const L = (x1, y1, x2, y2) => <line x1={x1} y1={y1} x2={x2} y2={y2} stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />;
  const R = (x, y, w, h) => <rect x={x} y={y} width={w} height={h} rx="1" fill="currentColor" />;
  const content = {
    left: <>{L(2, 3, 2, 13)}{R(4, 4.8, 9, 2.4)}{R(4, 8.8, 5.5, 2.4)}</>,
    centerH: <>{L(8, 3, 8, 13)}{R(3.5, 4.8, 9, 2.4)}{R(5.25, 8.8, 5.5, 2.4)}</>,
    right: <>{L(14, 3, 14, 13)}{R(3.5, 4.8, 9, 2.4)}{R(7, 8.8, 5.5, 2.4)}</>,
    top: <>{L(3, 2, 13, 2)}{R(4.8, 4, 2.4, 9)}{R(8.8, 4, 2.4, 5.5)}</>,
    middleV: <>{L(3, 8, 13, 8)}{R(4.8, 3.5, 2.4, 9)}{R(8.8, 5.25, 2.4, 5.5)}</>,
    bottom: <>{L(3, 14, 13, 14)}{R(4.8, 3, 2.4, 9)}{R(8.8, 6.5, 2.4, 5.5)}</>
  };
  return (
    <svg width="15" height="15" viewBox="0 0 16 16" style={{ display: "block" }}>
      {content[mode]}
    </svg>);

}

// 행간 (line-height) glyph: overlined "A"
function lineHeightIcon() {return (
    <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" style={{ display: "block" }}>
      <path d="M3 3 H13" />
      <path d="M5.4 13 L8 5.4 L10.6 13" />
      <path d="M6.4 10.4 H9.6" />
    </svg>);

}
// 자간 (letter-spacing) glyph: "|A|"
function letterSpacingIcon() {
  return (
    <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" style={{ display: "block" }}>
      <path d="M2.6 3 V13" />
      <path d="M13.4 3 V13" />
      <path d="M5.6 12.6 L8 5.8 L10.4 12.6" />
      <path d="M6.5 10 H9.5" />
    </svg>);

}

// 레이어 간격 glyph: 가로/세로 배치에 맞춘 스페이싱 아이콘 (다른 인풋 아이콘과 동일한 13px/1.3 스트로크/currentColor)
function gapIcon(horizontal) {
  return horizontal ?
  <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" style={{ display: "block" }}>
      <rect x="3.5" y="3" width="3" height="10" rx="1.2" />
      <rect x="9.5" y="3" width="3" height="10" rx="1.2" />
    </svg> :

  <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" style={{ display: "block" }}>
      <rect x="3" y="3.5" width="10" height="3" rx="1.2" />
      <rect x="3" y="9.5" width="10" height="3" rx="1.2" />
    </svg>;
}

// Corner-bracket glyphs for the per-corner radius fields
function cornerIcon(corner) {const paths = {
    tl: "M4 10 V6 Q4 4 6 4 H10",
    tr: "M4 4 H8 Q10 4 10 6 V10",
    bl: "M4 4 V8 Q4 10 6 10 H10",
    br: "M4 10 H8 Q10 10 10 8 V4"
  };
  return (
    <svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: "block" }}>
      <path d={paths[corner]} />
    </svg>);

}

function ShapeInspector({ layer, onUpdate, brandColors }) {
  return (
    <>
      <div className="panel-section">
        <div className="panel-title" style={{ color: "#1e1e1e", fontSize: "11px", fontWeight: "700" }}>모서리 반경</div>
        <NumField
          label={<svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: "block" }}>
              <path d="M3 5.2 V4 Q3 3 4 3 H5.2" />
              <path d="M8.8 3 H10 Q11 3 11 4 V5.2" />
              <path d="M11 8.8 V10 Q11 11 10 11 H8.8" />
              <path d="M5.2 11 H4 Q3 11 3 10 V8.8" />
            </svg>}
          value={shapeCorners(layer).tl}
          displayText={shapeRadiusMixed(layer) ? "혼합됨" : undefined}
          onChange={(v) => onUpdate({ radius: v, radiusTL: null, radiusTR: null, radiusBR: null, radiusBL: null })}
          suffix="px" />
        <div className="row grid-2">
          <NumField label={cornerIcon("tl")} value={shapeCorners(layer).tl} onChange={(v) => onUpdate({ radiusTL: v })} suffix="px" />
          <NumField label={cornerIcon("tr")} value={shapeCorners(layer).tr} onChange={(v) => onUpdate({ radiusTR: v })} suffix="px" />
        </div>
        <div className="row grid-2">
          <NumField label={cornerIcon("bl")} value={shapeCorners(layer).bl} onChange={(v) => onUpdate({ radiusBL: v })} suffix="px" />
          <NumField label={cornerIcon("br")} value={shapeCorners(layer).br} onChange={(v) => onUpdate({ radiusBR: v })} suffix="px" />
        </div>
      </div>
      <div className="panel-section">
        <div className="panel-title" style={{ color: "#1e1e1e", fontSize: "12px", fontWeight: "700" }}>텍스트</div>
        <input
          className="input"
          value={layer.content || ""}
          placeholder="예) 지금 구매 →"
          onKeyDown={(e) => {if (e.key === "Enter") e.currentTarget.blur();}}
          onChange={(e) => onUpdate({ content: e.target.value })} />
      </div>
      <div className="panel-section">
        <div className="panel-title" style={{ color: "#1e1e1e", fontWeight: "700", fontSize: "11px" }}>타이포그래피</div>
        <DSelect value={layer.font || "Pretendard"} options={FONT_OPTIONS} onChange={(v) => onUpdate({ font: v })} />
        <div className="row grid-2">
          <DSelect value={layer.weight || 600} options={WEIGHT_OPTIONS} onChange={(v) => onUpdate({ weight: v })} />
          <SizeField value={layer.size || 16} onChange={(v) => onUpdate({ size: v })} />
        </div>
        <div className="row grid-2">
          <NumField label={lineHeightIcon()} value={layer.lineHeight || 1.3} step={0.05} onChange={(v) => onUpdate({ lineHeight: v })} />
          <NumField label={letterSpacingIcon()} value={layer.letterSpacing || 0} step={0.1} onChange={(v) => onUpdate({ letterSpacing: v })} />
        </div>
        <div className="seg" style={{ backgroundColor: "rgb(245, 245, 245)" }}>
          <button className={layer.align === "left" || !layer.align ? "is-active" : ""} onClick={() => onUpdate({ align: "left" })}><I.Align.Left size={13} /></button>
          <button className={layer.align === "center" ? "is-active" : ""} onClick={() => onUpdate({ align: "center" })}><I.Align.Center size={13} /></button>
          <button className={layer.align === "right" ? "is-active" : ""} onClick={() => onUpdate({ align: "right" })}><I.Align.Right size={13} /></button>
        </div>
      </div>
      <div className="panel-section">
        <div className="panel-title" style={{ color: "#1e1e1e", fontWeight: "700", fontSize: "11px" }}>색상</div>
        <ColorField label="텍스트" value={layer.textColor ?? ""} placeholder="없음" onChange={(v) => onUpdate({ textColor: v })} clearable brandColors={brandColors} />
        <ColorField label="배경" value={layer.color ?? ""} onChange={(v) => onUpdate({ color: v })} clearable brandColors={brandColors} />
        <div className="micro-note">텍스트를 입력하면 도형이 CTA 버튼으로 표시됩니다.</div>
      </div>
      <div className="panel-section">
        <div className="row" style={{ justifyContent: "space-between" }}>
          <span className="panel-title" style={{ margin: 0, fontWeight: "700", color: "#1e1e1e" }}>외곽선</span>
          <Toggle checked={layer.strokeOn ?? false} onChange={(v) => onUpdate({ strokeOn: v })} />
        </div>
        {layer.strokeOn &&
        <>
            <ColorField label="색상" value={layer.strokeColor ?? ""} placeholder="없음" onChange={(v) => onUpdate({ strokeColor: v })} clearable brandColors={brandColors} />
            <div className="row grid-2">
              <div>
                <div className="label" style={{ padding: "0px 0px 4px" }}>위치</div>
                <DSelect value={layer.strokePos || "outside"} onChange={(v) => onUpdate({ strokePos: v })} style={{ height: "27px" }} options={[{ value: "outside", label: "외부" }, { value: "center", label: "중앙" }, { value: "inside", label: "내부" }]} />
              </div>
              <div>
                <div className="label" style={{ margin: "0px", padding: "0px 0px 4px" }}>굵기</div>
                <NumField label="W" value={layer.strokeWidth || 1} onChange={(v) => onUpdate({ strokeWidth: v })} suffix="px" />
              </div>
            </div>
          </>
        }
      </div>
      <div className="panel-section">
        <div className="row" style={{ justifyContent: "space-between" }}>
          <span className="panel-title" style={{ margin: 0, fontWeight: "700", color: "#1e1e1e" }}>효과</span>
        </div>
        <div className="row" style={{ justifyContent: "space-between" }}>
          <span className="label">그림자</span>
          <Toggle checked={layer.shadowOn ?? false} onChange={(v) => onUpdate({ shadowOn: v })} />
        </div>
        {layer.shadowOn &&
        <div className="row grid-2" style={{ marginTop: 6 }}>
            <div>
              <div className="label" style={{ padding: "0px 0px 4px" }}>흐림</div>
              <NumField label="B" value={layer.shadowBlur ?? 12} onChange={(v) => onUpdate({ shadowBlur: Math.max(0, Math.round(v)) })} suffix="px" />
            </div>
            <div>
              <div className="label" style={{ padding: "0px 0px 4px" }}>거리</div>
              <NumField label="Y" value={layer.shadowY ?? 4} onChange={(v) => onUpdate({ shadowY: Math.round(v) })} suffix="px" />
            </div>
            <div>
              <div className="label" style={{ padding: "0px 0px 4px" }}>투명도</div>
              <NumField label="%" value={layer.shadowOpacity ?? 25} onChange={(v) => onUpdate({ shadowOpacity: Math.max(0, Math.min(100, Math.round(v))) })} suffix="%" />
            </div>
          </div>
        }
      </div>
    </>);

}

function NumField({ label, value, onChange, suffix, step = 1, displayText }) {
  const [str, setStr] = useState(String(value));
  const [isFocused, setIsFocused] = useState(false);
  const focused = useRef(false);
  useEffect(() => {if (!focused.current) setStr(String(value));}, [value]);
  const handle = (e) => {
    let raw = e.target.value.replace(/[^0-9.\-]/g, "");
    raw = raw.replace(/^(-?)0+(?=\d)/, "$1"); // 0 다음 숫자 입력 시 앞의 0 제거 (01 → 1)
    setStr(raw);
    const n = parseFloat(raw);
    if (!isNaN(n)) onChange(n);
  };
  const onFocus = () => {focused.current = true;setIsFocused(true);if (displayText != null) setStr("");};
  const blur = () => {
    focused.current = false;
    setIsFocused(false);
    const n = parseFloat(str);
    const final = isNaN(n) ? 0 : n;
    setStr(String(final));
    onChange(final);
  };
  return (
    <div className="input-wrap seg">
      <span className="input-prefix">{label}</span>
      <input
        type="text"
        inputMode="decimal"
        className="input with-prefix with-suffix"
        value={!isFocused && displayText != null ? displayText : str}
        onFocus={onFocus}
        onBlur={blur}
        onKeyDown={(e) => {if (e.key === "Enter") {e.preventDefault();e.currentTarget.blur();}}}
        onChange={handle} />
      
      {suffix && !(!isFocused && displayText != null) && <span className="input-suffix">{suffix}</span>}
    </div>);

}

// ---- color + alpha helpers (hex8 #RRGGBBAA) ----
function splitColor(v) {
  if (!v || typeof v !== "string" || v[0] !== "#") return { hex: v || "", alpha: 100 };
  const h = v.slice(1);
  if (h.length === 8) return { hex: "#" + h.slice(0, 6), alpha: Math.round(parseInt(h.slice(6, 8), 16) / 255 * 100) };
  return { hex: "#" + h.slice(0, 6), alpha: 100 };
}
function joinColor(hex, alpha) {
  if (!hex || hex[0] !== "#") return hex;
  const h6 = hex.slice(1).slice(0, 6);
  const a = Math.max(0, Math.min(100, Math.round(alpha)));
  if (a >= 100) return "#" + h6;
  return "#" + h6 + Math.round(a / 100 * 255).toString(16).padStart(2, "0");
}

// ---- color-space conversions (display formats only — storage stays hex) ----
function hexToRgb(hex) {
  const h = (hex || "").replace("#", "").slice(0, 6).padEnd(6, "0");
  return { r: parseInt(h.slice(0, 2), 16) || 0, g: parseInt(h.slice(2, 4), 16) || 0, b: parseInt(h.slice(4, 6), 16) || 0 };
}
function rgbToHex(r, g, b) {
  const c = (n) => Math.max(0, Math.min(255, Math.round(n || 0))).toString(16).padStart(2, "0");
  return "#" + c(r) + c(g) + c(b);
}
function rgbToHsl(r, g, b) {
  r /= 255;g /= 255;b /= 255;
  const max = Math.max(r, g, b),min = Math.min(r, g, b);
  let h = 0,s = 0;const l = (max + min) / 2;
  if (max !== min) {
    const d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    if (max === r) h = (g - b) / d + (g < b ? 6 : 0);else
    if (max === g) h = (b - r) / d + 2;else
    h = (r - g) / d + 4;
    h /= 6;
  }
  return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
}
function hslToRgb(h, s, l) {
  h /= 360;s /= 100;l /= 100;
  let r, g, b;
  if (s === 0) {r = g = b = l;} else {
    const hue2rgb = (p, q, t) => {
      if (t < 0) t += 1;if (t > 1) t -= 1;
      if (t < 1 / 6) return p + (q - p) * 6 * t;
      if (t < 1 / 2) return q;
      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
      return p;
    };
    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;const p = 2 * l - q;
    r = hue2rgb(p, q, h + 1 / 3);g = hue2rgb(p, q, h);b = hue2rgb(p, q, h - 1 / 3);
  }
  return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
}
function rgbToHsb(r, g, b) {
  r /= 255;g /= 255;b /= 255;
  const max = Math.max(r, g, b),min = Math.min(r, g, b),d = max - min;
  let h = 0;const s = max === 0 ? 0 : d / max;const v = max;
  if (d !== 0) {
    if (max === r) h = (g - b) / d + (g < b ? 6 : 0);else
    if (max === g) h = (b - r) / d + 2;else
    h = (r - g) / d + 4;
    h /= 6;
  }
  return { h: Math.round(h * 360), s: Math.round(s * 100), b: Math.round(v * 100) };
}
function hsbToRgb(h, s, v) {
  h /= 360;s /= 100;v /= 100;
  const i = Math.floor(h * 6);const f = h * 6 - i;
  const p = v * (1 - s),q = v * (1 - f * s),t = v * (1 - (1 - f) * s);
  let r, g, b;
  switch (i % 6) {
    case 0:r = v;g = t;b = p;break;
    case 1:r = q;g = v;b = p;break;
    case 2:r = p;g = v;b = t;break;
    case 3:r = p;g = q;b = v;break;
    case 4:r = t;g = p;b = v;break;
    default:r = v;g = p;b = q;break;}

  return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
}
function parseCss(str) {
  str = (str || "").trim();
  const m = str.match(/rgba?\(([^)]+)\)/i);
  if (m) {const p = m[1].split(",").map((s) => parseFloat(s));return rgbToHex(p[0], p[1], p[2]);}
  if (str[0] === "#") return str;
  if (/^[0-9a-fA-F]{6}$/.test(str)) return "#" + str;
  return null;
}

const COLOR_FORMATS = ["Hex", "RGB", "CSS", "HSL", "HSB"];

// "배경 없음" (no-fill) glyph — white chip with a red diagonal slash
function NoFillGlyph({ size = 16 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" style={{ display: "block" }}>
      <rect x="1.25" y="1.25" width="13.5" height="13.5" rx="3" fill="#ffffff" stroke="var(--border-strong)" strokeWidth="1" />
      <line x1="3" y1="13" x2="13" y2="3" stroke="#e5484d" strokeWidth="1.6" strokeLinecap="round" />
    </svg>);

}

// Value editor whose fields depend on the chosen color format
function CfValue({ format, isNone, hex, alpha, setHex, placeholder }) {
  const rgb = hexToRgb(hex || "#000000");
  if (format === "Hex") {
    return (
      <input
        className="cf-hex"
        value={isNone ? "" : hex.replace(/^#/, "").toUpperCase()}
        placeholder={placeholder || "없음"}
        onKeyDown={(e) => {if (e.key === "Enter") e.currentTarget.blur();}}
        onChange={(e) => {const v = e.target.value.replace(/^#/, "").trim();setHex(v ? "#" + v : "#000000");}} />);

  }
  if (format === "CSS") {
    const css = isNone ? "" : alpha >= 100 ? hex : `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${(alpha / 100).toFixed(2)})`;
    return (
      <input
        className="cf-hex"
        value={css}
        placeholder={placeholder || "없음"}
        onKeyDown={(e) => {if (e.key === "Enter") e.currentTarget.blur();}}
        onChange={(e) => {const h = parseCss(e.target.value);if (h) setHex(h);}} />);

  }
  const cells =
  format === "RGB" ? [["R", rgb.r], ["G", rgb.g], ["B", rgb.b]] :
  format === "HSL" ? (() => {const { h, s, l } = rgbToHsl(rgb.r, rgb.g, rgb.b);return [["H", h], ["S", s], ["L", l]];})() :
  (() => {const { h, s, b } = rgbToHsb(rgb.r, rgb.g, rgb.b);return [["H", h], ["S", s], ["B", b]];})();
  const onCell = (idx, val) => {
    const nums = cells.map((c) => c[1]);
    nums[idx] = parseFloat(val) || 0;
    let h;
    if (format === "RGB") h = rgbToHex(nums[0], nums[1], nums[2]);else
    if (format === "HSL") {const c = hslToRgb(nums[0], nums[1], nums[2]);h = rgbToHex(c.r, c.g, c.b);} else
    {const c = hsbToRgb(nums[0], nums[1], nums[2]);h = rgbToHex(c.r, c.g, c.b);}
    setHex(h);
  };
  return (
    <div className="cf-triple">
      {cells.map(([lab, v], i) =>
      <label key={lab} className="cf-triple-cell">
          <input type="number" value={isNone ? "" : Math.round(v)} onKeyDown={(e) => {if (e.key === "Enter") e.currentTarget.blur();}} onChange={(e) => onCell(i, e.target.value)} />
        </label>
      )}
    </div>);

}

// Figma-style color picker popup: SV square + hue/alpha sliders + eyedropper + format dropdown
function hsvToHex(h, s, v) {const c = hsbToRgb(h, s, v);return rgbToHex(c.r, c.g, c.b);}
const clamp01 = (n) => Math.max(0, Math.min(1, n));

// ---- Gradient value helpers (value can be a hex color OR a CSS gradient string) ----
function splitTopComma(s) {
  const out = [];let depth = 0,cur = "";
  for (const ch of s) {
    if (ch === "(") depth++;
    if (ch === ")") depth--;
    if (ch === "," && depth === 0) {out.push(cur);cur = "";} else cur += ch;
  }
  if (cur.trim()) out.push(cur);
  return out;
}
function parseGradient(v) {
  if (typeof v !== "string" || !/^(linear|radial)-gradient/.test(v.trim())) return null;
  const type = v.trim().startsWith("radial") ? "radial" : "linear";
  const inner = v.slice(v.indexOf("(") + 1, v.lastIndexOf(")"));
  let parts = splitTopComma(inner);
  let angle = 90;
  if (type === "linear" && /deg\s*$/.test(parts[0])) {angle = parseFloat(parts[0]);parts = parts.slice(1);} else
  if (type === "radial" && !/(#|rgb)/.test(parts[0])) {parts = parts.slice(1);}
  const stops = parts.map((p) => {
    const m = p.trim().match(/(#[0-9a-fA-F]{3,8}|rgba?\([^)]*\))\s*([\d.]+)?%?/);
    if (!m) return null;
    return { color: m[1], pos: m[2] != null ? parseFloat(m[2]) : 0 };
  }).filter(Boolean);
  return stops.length ? { type, angle, stops } : null;
}
function gradToCss(g) {
  const stops = g.stops.slice().sort((a, b) => a.pos - b.pos).map((s) => `${s.color} ${Math.round(s.pos)}%`).join(", ");
  return g.type === "radial" ? `radial-gradient(circle, ${stops})` : `linear-gradient(${Math.round(g.angle)}deg, ${stops})`;
}
// Horizontal preview gradient (angle-independent) for the editor bar
function gradBarCss(g) {
  const stops = g.stops.slice().sort((a, b) => a.pos - b.pos).map((s) => `${s.color} ${Math.round(s.pos)}%`).join(", ");
  return `linear-gradient(90deg, ${stops})`;
}

function ColorPicker({ value, hex, alpha, onChangeHex, onChangeAlpha, onChangeValue, pos, onClose, placeholder }) {
  const parsed = parseGradient(value);
  const [mode, setMode] = useState(parsed ? "gradient" : "solid");
  const [grad, setGrad] = useState(parsed || { type: "linear", angle: 90, stops: [{ pos: 0, color: "#D9D9D9" }, { pos: 100, color: "#737373" }] });
  const [selStop, setSelStop] = useState(0);
  const [format, setFormat] = useState("Hex");
  const [menuOpen, setMenuOpen] = useState(false);
  const svRef = useRef(null);
  const hueRef = useRef(null);
  const alphaRef = useRef(null);
  const barRef = useRef(null);

  // The color currently being edited: the solid value, or the selected gradient stop
  const activeVal = mode === "gradient" ? grad.stops[selStop]?.color || "#000000" : hex || "#000000";
  const aSplit = splitColor(activeVal[0] === "#" ? activeVal : "#000000");
  const aHex = aSplit.hex,aAlpha = mode === "gradient" ? aSplit.alpha : alpha;

  const [hsv, setHsv] = useState(() => {const { r, g, b } = hexToRgb(aHex);return rgbToHsb(r, g, b);});
  useEffect(() => {const { r, g, b } = hexToRgb(aHex);const n = rgbToHsb(r, g, b);setHsv({ h: n.h, s: n.s, b: n.b });}, [selStop, mode]); // eslint-disable-line

  const updateGrad = (ng) => {setGrad(ng);onChangeValue?.(gradToCss(ng));};
  const updateStop = (i, patch) => updateGrad({ ...grad, stops: grad.stops.map((s, idx) => idx === i ? { ...s, ...patch } : s) });

  const applyHex = (newHex) => {
    if (mode === "gradient") updateStop(selStop, { color: joinColor(newHex, aAlpha) });else
    onChangeHex(newHex);
  };
  const applyAlpha = (a) => {
    if (mode === "gradient") updateStop(selStop, { color: joinColor(aHex, a) });else
    onChangeAlpha(a);
  };
  const pushHsv = (n) => {setHsv(n);applyHex(hsvToHex(n.h, n.s, n.b));};
  const applyHexField = (newHex) => {
    const { r, g, b } = hexToRgb(newHex);const n = rgbToHsb(r, g, b);
    setHsv((p) => ({ h: n.s === 0 ? p.h : n.h, s: n.s, b: n.b }));
    applyHex(newHex);
  };

  const switchMode = (m) => {
    if (m === mode) return;
    setMode(m);
    if (m === "gradient") onChangeValue?.(gradToCss(grad));else
    onChangeValue?.(grad.stops[selStop]?.color || joinColor(hex || "#000000", alpha));
  };

  const addStop = () => {
    const sorted = grad.stops.slice().sort((a, b) => a.pos - b.pos);
    let gap = -1,at = 50;
    for (let i = 0; i < sorted.length - 1; i++) {const d = sorted[i + 1].pos - sorted[i].pos;if (d > gap) {gap = d;at = (sorted[i].pos + sorted[i + 1].pos) / 2;}}
    const ng = { ...grad, stops: [...grad.stops, { pos: Math.round(at), color: aHex }] };
    setGrad(ng);onChangeValue?.(gradToCss(ng));setSelStop(ng.stops.length - 1);
  };
  const removeStop = (i) => {
    if (grad.stops.length <= 2) return;
    const stops = grad.stops.filter((_, idx) => idx !== i);
    updateGrad({ ...grad, stops });
    setSelStop((s) => Math.max(0, Math.min(s, stops.length - 1)));
  };

  const dragOn = (ref, fn) => (e) => {
    e.preventDefault();
    const handle = (ev) => {const r = ref.current.getBoundingClientRect();fn(r, ev);};
    handle(e);
    const up = () => {window.removeEventListener("mousemove", handle);window.removeEventListener("mouseup", up);};
    window.addEventListener("mousemove", handle);
    window.addEventListener("mouseup", up);
  };
  const onSV = dragOn(svRef, (r, ev) => {
    const s = Math.round(clamp01((ev.clientX - r.left) / r.width) * 100);
    const v = Math.round((1 - clamp01((ev.clientY - r.top) / r.height)) * 100);
    pushHsv({ h: hsv.h, s, b: v });
  });
  const onHue = dragOn(hueRef, (r, ev) => {
    const h = Math.round(clamp01((ev.clientX - r.left) / r.width) * 360);
    pushHsv({ h, s: hsv.s, b: hsv.b });
  });
  const onAlphaDrag = dragOn(alphaRef, (r, ev) => {
    applyAlpha(Math.round(clamp01((ev.clientX - r.left) / r.width) * 100));
  });
  const onStopDrag = (i) => dragOn(barRef, (r, ev) => {
    updateStop(i, { pos: Math.round(clamp01((ev.clientX - r.left) / r.width) * 100) });
  });

  useEffect(() => {
    const onKey = (e) => {if (e.key === "Escape") onClose();};
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  const hueColor = `hsl(${hsv.h} 100% 50%)`;
  const cur = hsvToHex(hsv.h, hsv.s, hsv.b);
  const hasEyedropper = typeof window !== "undefined" && "EyeDropper" in window;
  const eyedrop = async () => {
    try {const ed = new window.EyeDropper();const res = await ed.open();applyHexField(res.sRGBHex);} catch (e) {}
  };

  return (
    <>
      <div className="cp-backdrop" onMouseDown={onClose} />
      <div className="cp-popup" style={{ left: pos.left, top: pos.top, maxHeight: `calc(100vh - ${pos.top + 12}px)` }} onMouseDown={(e) => e.stopPropagation()}>
        <div className="cp-filltype">
          <button type="button" className={cls(mode === "solid" && "is-active")} onClick={() => switchMode("solid")}>단색</button>
          <button type="button" className={cls(mode === "gradient" && "is-active")} onClick={() => switchMode("gradient")}>그라데이션</button>
        </div>
        <div
          className="cp-sv" ref={svRef} onMouseDown={onSV}
          style={{ background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, transparent), ${hueColor}`, borderRadius: "5px" }}>
          <span className="cp-sv-thumb" style={{ left: `${hsv.s}%`, top: `${100 - hsv.b}%`, background: cur }} />
        </div>

        {mode === "solid" &&
        <div className="cp-controls">
            {hasEyedropper &&
          <button type="button" className="cp-eyedrop" onClick={eyedrop} title="화면에서 색상 추출">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M15 11.25l1.5 1.5.75-.75V8.758l2.276-.61a3 3 0 1 0-3.675-3.675l-.61 2.277H12l-.75.75 1.5 1.5" />
                  <path d="M15 11.25l-8.47 8.47c-.34.34-.8.53-1.28.53s-.94.19-1.28.53l-.97.97-.75-.75.97-.97c.34-.34.53-.8.53-1.28s.19-.94.53-1.28L12.75 9" />
                </svg>
              </button>
          }
            <div className="cp-tracks">
              <div className="cp-hue" ref={hueRef} onMouseDown={onHue} style={{ width: "180px" }}>
                <span className="cp-track-thumb" style={{ left: `calc(9px + ${hsv.h / 360} * (100% - 18px))`, background: hueColor }} />
              </div>
              <div className="cp-alpha checker" ref={alphaRef} onMouseDown={onAlphaDrag} style={{ width: "180px" }}>
                <span className="cp-alpha-fill" style={{ background: `linear-gradient(to right, transparent, ${cur})` }} />
                <span className="cp-track-thumb" style={{ left: `calc(9px + ${aAlpha / 100} * (100% - 18px))`, background: cur }} />
              </div>
            </div>
          </div>
        }

        {mode === "gradient" &&
        <div className="cp-grad">
            <div className="cp-grad-typerow">
              <DSelect value={grad.type} options={[{ value: "linear", label: "선형" }, { value: "radial", label: "방사형" }]} onChange={(v) => updateGrad({ ...grad, type: v })} />
            </div>
            <div className="cp-grad-bar checker" ref={barRef}>
              <span className="cp-grad-bar-fill" style={{ background: gradBarCss(grad) }} />
              {grad.stops.map((s, i) =>
            <span
              key={i}
              className={cls("cp-grad-stopdot", i === selStop && "is-active")}
              style={{ left: `${s.pos}%`, background: splitColor(s.color).hex }}
              onMouseDown={(e) => {e.stopPropagation();setSelStop(i);onStopDrag(i)(e);}} />

            )}
            </div>
            <div className="cp-grad-stops">
              <div className="cp-grad-stops-head">
                <span>중지점</span>
                <button type="button" title="중지점 추가" onClick={addStop}><I.Plus size={12} /></button>
              </div>
              {grad.stops.map((s, i) => {
              const sp = splitColor(s.color);
              return (
                <div key={i} className={cls("cp-grad-stoprow", i === selStop && "is-active")} onMouseDown={() => setSelStop(i)}>
                    <input
                    className="cp-grad-pos" type="text" inputMode="numeric"
                    value={Math.round(s.pos)}
                    onChange={(e) => updateStop(i, { pos: Math.max(0, Math.min(100, parseInt(e.target.value.replace(/[^0-9]/g, "")) || 0)) })} />
                    <span className="cp-grad-pct">%</span>
                    <span className={cls("cp-grad-sw checker", chipNeedsBorder(sp.hex) && "bordered")}><span style={{ background: sp.hex }} /></span>
                    <span className="cp-grad-hex">{sp.hex.replace("#", "").toUpperCase()}</span>
                    <input
                    className="cp-grad-aval" type="text" inputMode="numeric"
                    value={sp.alpha}
                    onChange={(e) => updateStop(i, { color: joinColor(sp.hex, Math.max(0, Math.min(100, parseInt(e.target.value.replace(/[^0-9]/g, "")) || 0))) })} />
                    <span className="cp-grad-pct">%</span>
                    <button type="button" className="cp-grad-del" disabled={grad.stops.length <= 2} onClick={(e) => {e.stopPropagation();removeStop(i);}}><I.Minus size={12} /></button>
                  </div>);

            })}
            </div>
          </div>
        }

        {mode === "solid" &&
        <div className="cp-fields">
            <div className="cf-fmt cp-fmt-pill">
              <button type="button" className="cf-fmt-btn" onClick={() => setMenuOpen((o) => !o)}>
                {format}<I.Chevron size={10} />
              </button>
              {menuOpen &&
            <>
                  <div className="cf-fmt-backdrop" onMouseDown={() => setMenuOpen(false)} />
                  <div className="cf-fmt-menu">
                    {COLOR_FORMATS.map((f) =>
                <button key={f} type="button" className={cls("cf-fmt-opt", f === format && "is-active")} onClick={() => {setFormat(f);setMenuOpen(false);}}>
                        <span className="cf-fmt-check">{f === format && <I.Check size={12} stroke="#fff" />}</span>{f}
                      </button>
                )}
                  </div>
                </>
            }
            </div>
            <div className="cp-valuebox">
              <CfValue format={format} isNone={false} hex={aHex} alpha={aAlpha} setHex={applyHexField} placeholder={placeholder} />
              <div className="cf-alpha">
                <input type="number" min="0" max="100" value={aAlpha} onKeyDown={(e) => {if (e.key === "Enter") e.currentTarget.blur();}} onChange={(e) => applyAlpha(parseFloat(e.target.value) || 0)} />
                <span>%</span>
              </div>
            </div>
          </div>
        }
      </div>
    </>);

}

function ColorField({ label, value, onChange, placeholder, clearable, brandColors }) {
  const { hex, alpha } = splitColor(value);
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState(null);
  const [menuOpen, setMenuOpen] = useState(false);
  const [menuPos, setMenuPos] = useState(null);
  const swatchRef = useRef(null);
  const hexRef = useRef(null);
  const lastColorRef = useRef("#000000");
  const [hexDraft, setHexDraft] = useState(null); // local text while typing a hex code
  if (value && /^#[0-9a-fA-F]{6,8}$/.test(hex)) lastColorRef.current = hex;
  const isNone = !value;
  const setHex = (h) => onChange(joinColor(h, alpha));

  const openPopup = () => {
    const r = swatchRef.current.getBoundingClientRect();
    const W = 246,H = 480;
    let left = r.left - W - 12; // open to the left of the inspector
    if (left < 8) left = Math.min(r.right + 12, window.innerWidth - W - 8);
    const top = Math.min(Math.max(8, r.top - 40), window.innerHeight - H - 8);
    setPos({ left, top });
    setOpen(true);
  };
  const openMenu = () => {
    const r = hexRef.current.getBoundingClientRect();
    const H = 300;
    const top = r.bottom + 4 + H > window.innerHeight ? Math.max(8, r.top - 4 - H) : r.bottom + 4;
    setMenuPos({ left: r.left, top, width: r.width });
    setMenuOpen(true);
  };
  const toggleNone = () => {if (isNone) onChange(lastColorRef.current || "#000000");else onChange("");};

  return (
    <div>
      {label && <div className="label" style={{ padding: "0px 0px 4px" }}>{label}</div>}
      <div className="cf-line">
        <div className="cf-row" style={{ height: "26px" }}>
          <button
            ref={swatchRef} type="button" className={cls("cf-swatch checker", chipNeedsBorder(value) && "bordered")}
            onClick={openPopup} style={{ position: "relative", padding: 0 }}>
            <span style={{ display: "block", width: "100%", height: "100%", background: value || "transparent", borderRadius: 3, borderStyle: "none" }} />
            {isNone &&
            <svg viewBox="0 0 22 22" style={{ position: "absolute", inset: 0, width: "100%", height: "100%", pointerEvents: "none" }}>
                <line x1="3" y1="19" x2="19" y2="3" stroke="#e5484d" strokeWidth="1.6" strokeLinecap="round" />
              </svg>
            }
          </button>
          <div ref={hexRef} className="cf-hex cf-hex-btn" style={{ display: "flex", alignItems: "center" }}>
            <input
              type="text"
              className={cls("cf-hex-val cf-hex-input", isNone && "is-placeholder")}
              style={{ fontSize: "12px" }}
              spellCheck={false}
              value={hexDraft !== null ? hexDraft : value ? hex.replace(/^#/, "").toUpperCase() : ""}
              placeholder={placeholder || "없음"}
              onFocus={(e) => {openMenu();e.target.select();}}
              onChange={(e) => {
                const raw = e.target.value.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
                setHexDraft(raw.toUpperCase());
                if (raw.length === 3 || raw.length === 6) onChange(joinColor("#" + raw, alpha));
              }}
              onKeyDown={(e) => {if (e.key === "Enter") e.currentTarget.blur();}}
              onBlur={() => {
                const raw = (hexDraft || "").replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
                if (raw.length) {
                  // Snap an incomplete code to a similar, valid color by filling to 6 digits.
                  let full;
                  if (raw.length === 6) full = raw;else
                  if (raw.length === 3) full = raw.split("").map((c) => c + c).join("");else
                  {full = raw;let i = 0;while (full.length < 6) {full += raw[i % raw.length];i++;}}
                  onChange(joinColor("#" + full, alpha));
                }
                setHexDraft(null);
              }} />
          </div>
          <div className="cf-alpha" style={{ flexDirection: "row", justifyContent: "flex-start", padding: "0px 8px 0px 8px" }}>
            <input
              type="number" min="0" max="100"
              value={alpha}
              onKeyDown={(e) => {if (e.key === "Enter") e.currentTarget.blur();}}
              onChange={(e) => onChange(joinColor(hex || "#000000", parseFloat(e.target.value) || 0))} />
            <span>%</span>
          </div>
        </div>
        {clearable &&
        <button
          type="button"
          className={cls("cf-eye", isNone && "is-off")}
          title={isNone ? "배경 표시" : "배경 없음"}
          onClick={toggleNone}>
          
            {isNone ? <I.EyeOff size={15} /> : <I.Eye size={15} />}
          </button>
        }
      </div>

      {menuOpen && menuPos && ReactDOM.createPortal(
        <BrandColorMenu
          colors={brandColors}
          value={value}
          currentHex={value ? hex : null}
          onPick={(c) => onChange(joinColor(c, alpha))}
          pos={menuPos} onClose={() => setMenuOpen(false)} />,

        document.body
      )}

      {open && pos && ReactDOM.createPortal(
        <ColorPicker
          value={value}
          hex={hex || "#000000"} alpha={alpha}
          onChangeHex={setHex}
          onChangeAlpha={(a) => onChange(joinColor(hex || "#000000", a))}
          onChangeValue={(v) => onChange(v)}
          pos={pos} onClose={() => setOpen(false)} placeholder={placeholder} />,

        document.body
      )}
    </div>);

}

// Readable foreground (black/white) for a hex background
function readableOn(hex) {
  const c = (hex || "#000000").replace("#", "");
  if (c.length < 6) return "#fff";
  const r = parseInt(c.slice(0, 2), 16),g = parseInt(c.slice(2, 4), 16),b = parseInt(c.slice(4, 6), 16);
  return 0.299 * r + 0.587 * g + 0.114 * b > 150 ? "#1a1a1a" : "#ffffff";
}

// Brand-guide color chips — shared between admin & user color pickers
function BrandChips({ colors, value, onChange, compact }) {
  const cur = (value || "").toLowerCase();
  return (
    <div className={cls("brand-chips", compact && "compact")}>
      <span className="brand-chips-label"><I.Palette size={10} /> 브랜드</span>
      <div className="brand-chips-row" style={{ width: "166px" }}>
        {colors.map((c) => {
          const active = cur === c.value.toLowerCase();
          return (
            <button
              key={c.id}
              type="button"
              title={`${c.name} · ${c.value}`}
              className={cls("brand-chip", active && "is-active")}
              style={{ background: c.value, width: "16px", height: "16px", borderRadius: "3px" }}
              onClick={() => onChange(c.value)}>
              
              {active && <I.Check size={9} stroke={readableOn(c.value)} />}
            </button>);

        })}
      </div>
    </div>);

}

// Brand color dropdown — opened by clicking the hex value; lists current value + brand colors (swatch + name).
function BrandColorMenu({ colors, value, currentHex, onPick, pos, onClose }) {
  useEffect(() => {
    const onKey = (e) => {if (e.key === "Escape") onClose();};
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);
  const cur = (value || "").toLowerCase();
  return (
    <>
      <div className="bcm-backdrop" onMouseDown={onClose} />
      <div className="bcm-menu" style={{ left: pos.left, top: pos.top, minWidth: pos.width }}>
        {currentHex &&
        <button type="button" className="bcm-opt is-active" onClick={() => {onPick(currentHex);onClose();}}>
            <span className={cls("bcm-sw", chipNeedsBorder(currentHex) && "bordered")} style={{ background: currentHex }} />
            <span className="bcm-name mono" style={{ fontSize: "12px" }}>{currentHex}</span>
          </button>
        }
        {(colors || []).map((c) => {
          const active = cur === c.value.toLowerCase();
          return (
            <button
              key={c.id || c.value}
              type="button"
              className={cls("bcm-opt", active && "is-active")}
              onClick={() => {onPick(c.value);onClose();}}>
              
              <span className={cls("bcm-sw", chipNeedsBorder(c.value) && "bordered")} style={{ background: c.value }} />
              <span className="bcm-name" style={{ fontSize: "12px" }}>{c.name}</span>
              <span className="bcm-val mono">{c.value}</span>
            </button>);

        })}
      </div>
    </>);

}

function Toggle({ checked, onChange }) {
  return (
    <button
      onClick={() => onChange && onChange(!checked)}
      style={{
        width: 30, height: 16, padding: 0,
        borderRadius: 99,
        background: checked ? "var(--accent)" : "var(--surface-3)",
        border: "1px solid " + (checked ? "var(--accent)" : "var(--border)"),
        position: "relative",
        cursor: "pointer"
      }}>
      
      <span style={{
        position: "absolute",
        top: 1, left: checked ? 15 : 1,
        width: 12, height: 12,
        background: "white",
        borderRadius: 99,
        transition: "left .15s"
      }} />
    </button>);

}

function SamplePreview({ guide }) {
  // Empty state when guide has no real layers yet
  const realLayers = guide.layers.filter((l) => l.type !== "bg");
  const boxRef = useRef(null);
  const [scale, setScale] = useState(0.2);
  useEffect(() => {
    const el = boxRef.current;
    if (!el) return;
    const fit = () => {
      const r = el.getBoundingClientRect();
      if (r.width && r.height) setScale(Math.min(r.width / guide.width, r.height / guide.height));
    };
    fit();
    let ro;
    if (typeof ResizeObserver !== "undefined") {ro = new ResizeObserver(fit);ro.observe(el);}
    return () => ro && ro.disconnect();
  }, [guide.width, guide.height, guide.sampleUploaded]);

  if (guide.sampleImageSrc) {
    return (
      <div className="sample-preview" style={{ background: guide.sampleImageBg || "#F5F5F7" }}>
        <img src={guide.sampleImageSrc} alt="" style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "contain" }} />
        <div className="sample-badge"><I.Check size={9} /> 등록됨</div>
      </div>);

  }

  if (!guide.sampleUploaded) {
    return (
      <div className="sample-preview sample-empty" style={{ backgroundColor: "rgb(243, 243, 243)" }}>
        <div className="sample-empty-inner" style={{ color: "rgb(153, 153, 153)" }}>
          <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
            <rect x="3" y="3" width="18" height="18" rx="2" />
            <circle cx="9" cy="9" r="1.5" />
            <path d="M21 16l-5-5L5 21" />
          </svg>
          <div className="sample-empty-text" style={{ color: "rgb(153, 153, 153)" }}>
            {realLayers.length === 0 ? "레이어를 추가하여 시작" : "샘플 이미지 미등록"}
          </div>
        </div>
      </div>);

  }

  // Render a tiny mock of the guide, auto-scaled to fit (contain) the fixed area
  return (
    <div className="sample-preview" ref={boxRef} style={{
      background: guide.sampleImageBg || "#F5F5F7"
    }}>
      <div style={{
        position: "absolute", top: "50%", left: "50%",
        width: guide.width, height: guide.height,
        transform: `translate(-50%, -50%) scale(${scale})`,
        transformOrigin: "center center"
      }}>
        {(() => {
          const md = computeMasks(guide.layers, guide.groups || {});
          return [...guide.layers].sort((a, b) => (a.z ?? 1) - (b.z ?? 1)).map((l) => {
            if (md.maskIds.has(l.id)) return null; // 마스크 도형 자체는 샘플에서 숨김
            const clip = md.targetClip[l.id];
            return (
              <div key={l.id} style={{
                position: "absolute",
                left: l.x, top: l.y, width: l.w, height: l.h,
                clipPath: clip || undefined, WebkitClipPath: clip || undefined
              }}>
                <SampleLayer layer={l} />
              </div>);

          });
        })()}
      </div>
      <div className="sample-badge"><I.Check size={9} /> 등록됨</div>
    </div>);

}

// A filled-in version of a layer for the sample mockup
function SampleLayer({ layer }) {
  if (layer.type === "bg") return <div style={{ width: "100%", height: "100%", background: layer.color }} />;
  if (layer.type === "shape") return <div style={{ width: "100%", height: "100%", background: layer.color, borderRadius: shapeRadiusCss(layer) }} />;
  if (layer.type === "logo") return (
    <div style={{
      width: "100%", height: "100%",
      background: layer.color || "#1a1a1a", color: "#fff",
      display: "grid", placeItems: "center",
      fontSize: Math.min(layer.h * 0.5, 22), fontWeight: 700,
      borderRadius: 4
    }}>{layer.content || "LOGO"}</div>);

  if (layer.type === "image") {
    if (layer.src) return <img src={layer.src} alt="" draggable={false} style={{ width: "100%", height: "100%", objectFit: layer.fit || "cover", display: "block" }} />;
    // Render a stylized "real" product image instead of the wireframe placeholder
    const palette = layer.slot === "product" ?
    ["#3a4a5e", "#5e7090", "#8b9bb0"] :
    layer.slot === "deco" ?
    ["#ffd4d4", "#ffa3a3", "#ff8a8a"] :
    ["#e6dcc8", "#c4a373", "#b08a5a"];
    return (
      <div style={{
        width: "100%", height: "100%",
        background: `linear-gradient(135deg, ${palette[0]}, ${palette[1]})`,
        position: "relative",
        overflow: "hidden"
      }}>
        <div style={{
          position: "absolute", inset: "15% 20%",
          background: `radial-gradient(circle at 35% 30%, ${palette[2]}, ${palette[0]})`,
          borderRadius: "50% 45% 50% 40% / 55% 50% 45% 50%"
        }} />
      </div>);

  }
  if (layer.type === "text") {
    const hasBg = !!layer.bg;
    return (
      <div style={{
        width: "100%", height: "100%",
        background: hasBg ? layer.bg : "transparent",
        borderRadius: layer.radius || 0,
        padding: hasBg ? `${layer.padding || 8}px ${(layer.padding || 8) * 1.2}px` : 0,
        display: "flex",
        alignItems: hasBg ? "center" : "flex-start",
        justifyContent: layer.align === "center" ? "center" : layer.align === "right" ? "flex-end" : "flex-start",
        overflow: "hidden"
      }}>
        <div style={{
          fontFamily: layer.font || "Pretendard",
          fontWeight: layer.weight || 400,
          fontSize: layer.size || 16,
          color: layer.color === "" ? "transparent" : layer.color || "#000",
          lineHeight: layer.lineHeight || 1.3,
          letterSpacing: layer.letterSpacing || 0,
          textAlign: layer.align || "left",
          width: "100%",
          whiteSpace: "pre-wrap"
        }}>
          {layer.content}
        </div>
      </div>);

  }
  return null;
}

// ---------- Main Admin Editor ----------
// Demo flow: start completely empty.
// Step 1 → click [+] in 카테고리 → adds the next category from CATEGORIES template, in order.
// Step 2 → click [+ 새로 만들기] in 가이드 → creates a blank guide (canvas only, no layers besides bg).
// Step 3 → click layer-add buttons to add layers.
// Step 4 → click [샘플 이미지 업로드] in right inspector → fills the sample preview with a mockup.
function AdminEditor({ tweaks, brand, onToggleGrid, initialCategories, initialGuides, initialActiveCategory, initialActiveGuideId, persistKey, editorId }) {
  // 영구 저장(관리자 상품배너550 프리셋 전용): persistKey가 있을 때만 로컬 저장소에서 복원/자동 저장.
  const persistedRef = useRef(undefined);
  if (persistedRef.current === undefined) {
    persistedRef.current = (() => {
      if (!persistKey) return null;
      try {const raw = window.localStorage.getItem(persistKey);return raw ? JSON.parse(raw) : null;} catch (e) {return null;}
    })();
  }
  const persisted = persistedRef.current;

  const [categories, setCategories] = useState(persisted && persisted.categories || initialCategories || []); // start empty unless seeded
  const [guides, setGuides] = useState(persisted && persisted.guides || initialGuides || []); // start empty unless seeded
  const [activeCategory, setActiveCategory] = useState((persisted && persisted.activeCategory) ?? initialActiveCategory ?? null);
  const [activeGuideId, setActiveGuideId] = useState((persisted && persisted.activeGuideId) ?? initialActiveGuideId ?? null);
  const [selectedIds, setSelectedIds] = useState([]); // multi-selection
  const [selectedGroupIds, setSelectedGroupIds] = useState([]); // 사용자가 "그룹 자체"를 클릭해 선택한 gid들 (멤버 1개짜리 그룹 하이라이트 판단용)
  const [zoom, setZoom] = useState(0.6);
  const [showGrid, setShowGrid] = useState(tweaks?.showGrid || brand?.grid?.show || false); // 그리드 오버레이 표시(로컬 토글)

  // ---- Undo/redo history (snapshots of categories + guides) ----
  const undoStack = useRef([]);
  const redoStack = useRef([]);
  const prevSnap = useRef({ categories: [], guides: [] });
  const isRestoring = useRef(false);
  const mountedHist = useRef(false);
  const suspendHistory = useRef(false); // true while a continuous gesture (drag) is in progress
  const gestureBase = useRef(null); // snapshot captured at the start of a gesture
  const [, setHistTick] = useState(0);

  useEffect(() => {
    if (!mountedHist.current) {mountedHist.current = true;prevSnap.current = { categories, guides };return;}
    if (isRestoring.current) {isRestoring.current = false;prevSnap.current = { categories, guides };return;}
    // During a drag, swallow every intermediate state — only the latest is kept as prevSnap,
    // and a single history entry is committed when the gesture ends (mouseup).
    if (suspendHistory.current) {prevSnap.current = { categories, guides };return;}
    undoStack.current.push(prevSnap.current);
    if (undoStack.current.length > 80) undoStack.current.shift();
    redoStack.current = []; // a fresh edit clears the redo branch
    prevSnap.current = { categories, guides };
    setHistTick((t) => t + 1);
  }, [categories, guides]);

  // 영구 저장: 작업 상태가 바뀔 때마다(디바운스) 로컬 저장소에 자동 저장 — persistKey가 있을 때만.
  const persistTimer = useRef(null);
  useEffect(() => {
    if (!persistKey) return;
    if (persistTimer.current) clearTimeout(persistTimer.current);
    persistTimer.current = setTimeout(() => {
      try {
        window.localStorage.setItem(persistKey, JSON.stringify({ categories, guides, activeCategory, activeGuideId }));
      } catch (e) {/* quota/serialize 실패 시 무시 */}
    }, 400);
    return () => {if (persistTimer.current) clearTimeout(persistTimer.current);};
  }, [persistKey, categories, guides, activeCategory, activeGuideId]);

  // ---- 백업 내보내기 / 불러오기 (환경 독립적 보존) ----
  // 전역 상단바(shell.jsx)에서 호출하므로, 이 편집기의 API를 editorId로 전역 레지스트리에 등록.
  const liveStateRef = useRef();
  liveStateRef.current = { categories, guides, activeCategory, activeGuideId };
  const exportBackup = () => {
    const s = liveStateRef.current;
    const payload = {
      app: "bannerly", kind: "editor-backup", version: 1,
      exportedAt: new Date().toISOString(),
      categories: s.categories, guides: s.guides,
      activeCategory: s.activeCategory, activeGuideId: s.activeGuideId
    };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    const stamp = new Date().toISOString().slice(0, 16).replace(/[:T]/g, "-");
    a.href = url;a.download = `bannerly-backup-${stamp}.json`;
    document.body.appendChild(a);a.click();a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
    return { ok: true, counts: { categories: s.categories.length, guides: s.guides.length } };
  };
  const importBackup = (data) => {
    if (!data || !Array.isArray(data.categories) || !Array.isArray(data.guides)) {
      return { ok: false, error: "백업 파일 형식이 올바르지 않습니다." };
    }
    setCategories(data.categories);
    setGuides(data.guides);
    setActiveCategory(data.activeCategory ?? data.categories[0]?.id ?? null);
    const gExists = data.guides.some((g) => g.id === data.activeGuideId);
    setActiveGuideId(gExists ? data.activeGuideId : data.guides[0]?.id ?? null);
    setSelectedIds([]);
    return { ok: true, counts: { categories: data.categories.length, guides: data.guides.length } };
  };
  // 테스트용 이미지 레이어 주입 — 마스크 동작을 실제 이미지로 확인하기 위함
  const addImageLayer = (src, opts = {}) => {
    const s = liveStateRef.current;
    const g = s.guides.find((x) => x.id === s.activeGuideId);
    if (!g) return { ok: false, error: "활성 가이드 없음" };
    const ratio = opts.ratio || 0.5368;
    const w = Math.round(opts.w || g.width * 0.78);
    const h = Math.round(opts.h || w * ratio);
    const z = Math.max(1, ...g.layers.map((l) => l.z ?? 1)) + 1;
    const layer = {
      id: uid(), type: "image", name: opts.name || "테스트 이미지", slot: "product", src,
      x: Math.round((g.width - w) / 2), y: Math.round((g.height - h) / 2), w, h, z,
      autoCutout: false, autoResize: false, fit: opts.fit || "cover"
    };
    setGuides((gs) => gs.map((gg) => gg.id !== s.activeGuideId ? gg : { ...gg, layers: [...gg.layers, layer] }));
    setSelectedIds([layer.id]);
    return { ok: true, id: layer.id };
  };
  useEffect(() => {
    if (!editorId) return;
    const reg = window.__bannerlyEditors = window.__bannerlyEditors || {};
    reg[editorId] = { exportBackup, importBackup, addImageLayer, updateLayer, getState: () => liveStateRef.current };
    return () => {if (reg[editorId]) delete reg[editorId];};
  }, [editorId]);

  // Begin a coalesced gesture: remember the pre-drag state, suspend per-change recording.
  const beginGesture = () => {
    if (suspendHistory.current) return;
    suspendHistory.current = true;
    gestureBase.current = prevSnap.current;
  };
  // End a gesture: commit exactly one history entry covering the whole drag (the mouseup state).
  const endGesture = () => {
    if (!suspendHistory.current) return;
    suspendHistory.current = false;
    const base = gestureBase.current;
    gestureBase.current = null;
    if (base && (base.guides !== prevSnap.current.guides || base.categories !== prevSnap.current.categories)) {
      undoStack.current.push(base);
      if (undoStack.current.length > 80) undoStack.current.shift();
      redoStack.current = [];
      setHistTick((t) => t + 1);
    }
  };

  // Restore a snapshot and reconcile active/selection references against it.
  const applySnap = (snap) => {
    isRestoring.current = true;
    setCategories(snap.categories);
    setGuides(snap.guides);
    const ag = snap.guides.find((g) => g.id === activeGuideId);
    if (!ag) {setActiveGuideId(null);setSelectedIds([]);} else
    {const valid = new Set(ag.layers.map((l) => l.id));setSelectedIds((sel) => sel.filter((id) => valid.has(id)));}
    if (!snap.categories.some((c) => c.id === activeCategory)) setActiveCategory(snap.categories[0]?.id ?? null);
    setHistTick((t) => t + 1);
  };

  const undo = () => {
    if (!undoStack.current.length) return;
    const snap = undoStack.current.pop();
    redoStack.current.push(prevSnap.current);
    prevSnap.current = snap;
    applySnap(snap);
  };
  const redo = () => {
    if (!redoStack.current.length) return;
    const snap = redoStack.current.pop();
    undoStack.current.push(prevSnap.current);
    prevSnap.current = snap;
    applySnap(snap);
  };
  const canUndo = undoStack.current.length > 0;
  const canRedo = redoStack.current.length > 0;

  const undoRef = useRef(undo);
  undoRef.current = undo;
  const redoRef = useRef(redo);
  redoRef.current = redo;
  useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && (e.key === "z" || e.key === "Z")) {
        const t = (e.target.tagName || "").toLowerCase();
        if (t === "input" || t === "textarea" || e.target.isContentEditable) return;
        e.preventDefault();
        if (e.shiftKey) redoRef.current();else undoRef.current();
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  const activeGuide = guides.find((g) => g.id === activeGuideId);
  // Primary selection = last one added (drives the single-layer inspector)
  const selectedLayerId = selectedIds.length ? selectedIds[selectedIds.length - 1] : null;
  const selectedLayer = activeGuide?.layers.find((l) => l.id === selectedLayerId);
  const selectedLayers = activeGuide ? activeGuide.layers.filter((l) => selectedIds.includes(l.id)) : [];

  // Select a layer. additive (shift/⌘-click) toggles it in/out of the current set.
  const selectLayer = (id, additive = false, single = false) => {
    setSelectedGroupIds([]); // 레이어를 직접 선택하면 "그룹 선택" 상태 해제
    if (id == null) {setSelectedIds([]);return;}
    const lyr = activeGuide && activeGuide.layers.find((l) => l.id === id);
    const groupIds = !single && lyr && lyr.groupId ? activeGuide.layers.filter((l) => l.groupId === lyr.groupId).map((l) => l.id) : null;
    setSelectedIds((prev) => {
      if (additive) {
        if (groupIds) {
          const allIn = groupIds.every((g) => prev.includes(g));
          return allIn ? prev.filter((x) => !groupIds.includes(x)) : [...new Set([...prev, ...groupIds])];
        }
        return prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id];
      }
      if (groupIds) {
        const same = groupIds.length === prev.length && groupIds.every((g) => prev.includes(g));
        return same ? prev : groupIds;
      }
      if (prev.length === 1 && prev[0] === id) return prev;
      return [id];
    });
  };

  // ---- 레이어 그룹 / 그룹 해제 ----
  const groupSelected = () => {
    if (!activeGuide) return;
    const grps0 = activeGuide.groups || {};
    const selLayers = activeGuide.layers.filter((l) => selectedIds.includes(l.id) && l.type !== "bg");
    if (selLayers.length < 1) return;
    // 공통 컨테이너 C = 선택된 모든 레이어를 포함하는 가장 깊은 그룹 (없으면 null=최상위)
    const chains = selLayers.map((l) => groupAncestors(grps0, l.groupId)); // [immediate..top]
    let C = null;
    if (chains.every((c) => c.length)) {
      for (const cand of chains[0]) {if (chains.every((c) => c.includes(cand))) {C = cand;break;}}
    }
    const gid = "grp-" + uid();
    const selIdSet = new Set(selLayers.map((l) => l.id));
    setGuides((gs) => gs.map((g) => {
      if (g.id !== activeGuideId) return g;
      const oldGroups = g.groups || {};
      const groups = { ...oldGroups };
      groups[gid] = { name: "그룹 " + (Object.keys(oldGroups).length + 1), parent: C || null };
      // C 직속 자식 중 “전체가 선택된” 서브그룹은 통째로 새 그룹으로 재배치(중첩 유지)
      const childRoots = new Set();
      selLayers.forEach((l) => {const cr = childOfContainer(oldGroups, l.groupId, C);if (cr) childRoots.add(cr);});
      const reparent = new Set();
      childRoots.forEach((cr) => {
        const all = layersInGroup(g.layers, oldGroups, cr);
        if (all.length && all.every((l) => selIdSet.has(l.id))) reparent.add(cr);
      });
      reparent.forEach((cr) => {groups[cr] = { ...(groups[cr] || {}), parent: gid };});
      const layers = g.layers.map((l) => {
        if (!selIdSet.has(l.id)) return l;
        const cr = childOfContainer(oldGroups, l.groupId, C);
        if (cr && reparent.has(cr)) return l; // 서브그룹 통째 유지(부모만 변경됨)
        return { ...l, groupId: gid }; // C 직속(또는 부분 선택) → 새 그룹 직속
      });
      return { ...g, groups, layers };
    }));
  };
  const ungroupSelected = () => {
    if (!activeGuide) return;
    const gids = [...new Set(activeGuide.layers.filter((l) => selectedIds.includes(l.id) && l.groupId).map((l) => l.groupId))];
    if (!gids.length) return;
    setGuides((gs) => gs.map((g) => {
      if (g.id !== activeGuideId) return g;
      const groups = { ...(g.groups || {}) };
      let layers = g.layers;
      gids.forEach((gid) => {
        const parent = (groups[gid] || {}).parent || null;
        layers = layers.map((l) => l.groupId === gid ? { ...l, groupId: parent } : l);
        Object.keys(groups).forEach((k) => {if ((groups[k] || {}).parent === gid) groups[k] = { ...groups[k], parent };});
        delete groups[gid];
      });
      return { ...g, groups, layers };
    }));
  };
  const renameGroup = (gid, name) => setGuides((gs) => gs.map((g) => g.id !== activeGuideId ? g : { ...g, groups: { ...(g.groups || {}), [gid]: { ...((g.groups || {})[gid] || {}), name } } }));
  // 그룹에 옵션 메타데이터(역할/선택방식/입력/개수/적용유형) 비파괴 병합 — renameGroup과 동일 패턴
  const updateGroup = (gid, patch) => setGuides((gs) => gs.map((g) => g.id !== activeGuideId ? g : { ...g, groups: { ...(g.groups || {}), [gid]: { ...((g.groups || {})[gid] || {}), ...patch } } }));
  const toggleGroupLock = (gid) => {
    if (!activeGuide) return;
    const members = layersInGroup(activeGuide.layers, activeGuide.groups || {}, gid);
    const allLocked = members.every((l) => l.locked);
    const pm = {};members.forEach((l) => {pm[l.id] = { locked: !allLocked };});
    updateManyLayers(pm);
  };
  const toggleGroupVis = (gid) => {
    if (!activeGuide) return;
    const members = layersInGroup(activeGuide.layers, activeGuide.groups || {}, gid);
    const allHidden = members.every((l) => l.visible === false);
    const pm = {};members.forEach((l) => {pm[l.id] = { visible: allHidden };});
    updateManyLayers(pm);
  };
  const toggleGroupCollapse = (gid) => setGuides((gs) => gs.map((g) => g.id !== activeGuideId ? g : { ...g, groups: { ...(g.groups || {}), [gid]: { ...((g.groups || {})[gid] || {}), collapsed: !((g.groups || {})[gid] || {}).collapsed } } }));
  // 주어진 그룹들을 패널에서 펼친다(collapsed=false). 캔버스 드릴다운 시 선택 그룹/레이어가 패널에 노출되도록.
  const expandGroups = (gids) => {
    if (!gids || !gids.length) return;
    setGuides((gs) => gs.map((g) => {
      if (g.id !== activeGuideId) return g;
      const groups = { ...(g.groups || {}) };
      let changed = false;
      gids.forEach((gid) => {
        if (groups[gid] && groups[gid].collapsed) {groups[gid] = { ...groups[gid], collapsed: false };changed = true;}
      });
      return changed ? { ...g, groups } : g;
    }));
  };
  const ungroupGroup = (gid) => setGuides((gs) => gs.map((g) => {
    if (g.id !== activeGuideId) return g;
    const groups = { ...(g.groups || {}) };
    const parent = (groups[gid] || {}).parent || null;
    const layers = g.layers.map((l) => l.groupId === gid ? { ...l, groupId: parent } : l);
    Object.keys(groups).forEach((k) => {if ((groups[k] || {}).parent === gid) groups[k] = { ...groups[k], parent };});
    delete groups[gid];
    return { ...g, groups, layers };
  }));
  const deleteGroup = (gid) => setGuides((gs) => gs.map((g) => {
    if (g.id !== activeGuideId) return g;
    const groups = { ...(g.groups || {}) };
    const layers = g.layers.filter((l) => !(l.groupId && groupAncestors(groups, l.groupId).includes(gid)));
    descendantGroupIds(groups, gid).forEach((k) => delete groups[k]);
    return { ...g, groups, layers };
  }));
  // gid 그룹 전체(중첩 하위 포함) 선택
  const selectGroup = (gid, additive = false) => {
    if (!activeGuide) return;
    const grps = activeGuide.groups || {};
    const ids = layersInGroup(activeGuide.layers, grps, gid).map((l) => l.id);
    if (!ids.length) return;
    const gids = descendantGroupIds(grps, gid); // 선택한 그룹 + 그 하위 그룹 전체
    setSelectedGroupIds((prev) => additive ? [...new Set([...prev, ...gids])] : gids);
    setSelectedIds((prev) => additive ? [...new Set([...prev, ...ids])] : ids);
  };
  // gid 그룹을 intoGid 그룹 안으로 이동(부모 지정). intoGid=null 이면 최상위로 빼냄. 순환 방지.
  const nestGroup = (gid, intoGid) => {
    if (gid === intoGid) return;
    setGuides((gs) => gs.map((g) => {
      if (g.id !== activeGuideId) return g;
      const groups = { ...(g.groups || {}) };
      if (!groups[gid]) return g;
      if (intoGid && (intoGid === gid || groupAncestors(groups, intoGid).includes(gid))) return g; // 자기/자기하위로 금지
      groups[gid] = { ...groups[gid], parent: intoGid || null };
      return { ...g, groups };
    }));
  };
  // 레이어를 gid 그룹 직속으로 이동 (gid=null 이면 그룹에서 빼냄)
  // 레이어를 gid 그룹 직속 '맨 위'로 이동 (gid=null 이면 최상위 맨 앞). 위치 정밀 배치.
  const moveLayerToGroup = (layerId, gid) => {
    if (!activeGuide) return;
    const lyr = activeGuide.layers.find((l) => l.id === layerId);
    if (!lyr || lyr.type === "bg") return;
    const groups = activeGuide.groups || {};
    const inG = gid ? new Set(layersInGroup(activeGuide.layers, groups, gid).map((l) => l.id)) : null;
    let order = flattenLayerOrder(activeGuide.layers, groups).filter((id) => id !== layerId);
    let at;
    if (gid) {const fi = order.findIndex((id) => inG.has(id));at = fi < 0 ? order.length : fi;} else
    at = 0;
    order.splice(at, 0, layerId);
    applyLayerOrder(order, { [layerId]: { groupId: gid || null } });
  };
  // 레이어를 gid 그룹의 '형제'(같은 부모)로, 그 그룹 바로 앞에 배치
  const moveLayerBeforeGroup = (layerId, gid) => {
    if (!activeGuide) return;
    const lyr = activeGuide.layers.find((l) => l.id === layerId);
    if (!lyr || lyr.type === "bg") return;
    const groups = activeGuide.groups || {};
    const container = (groups[gid] || {}).parent || null;
    const inG = new Set(layersInGroup(activeGuide.layers, groups, gid).map((l) => l.id));
    let order = flattenLayerOrder(activeGuide.layers, groups).filter((id) => id !== layerId);
    const fi = order.findIndex((id) => inG.has(id));
    const at = fi < 0 ? order.length : fi;
    order.splice(at, 0, layerId);
    applyLayerOrder(order, { [layerId]: { groupId: container } });
  };
  const duplicateLayer = (id) => {
    if (!activeGuide) return;
    const l = activeGuide.layers.find((x) => x.id === id);
    if (!l || l.type === "bg") return;
    const z = Math.max(0, ...activeGuide.layers.map((x) => x.z ?? 0));
    const copy = { ...l, id: uid(), x: l.x + 12, y: l.y + 12, z: z + 1, name: l.name + " 복사본", groupId: null };
    setGuides((gs) => gs.map((g) => g.id !== activeGuideId ? g : { ...g, layers: [...g.layers, copy] }));
    setSelectedIds([copy.id]);
  };
  const panelClip = useRef([]);
  // 마스크 토글 — 레이어를 클리핑 마스크로 사용/해제 (자기 바로 아래 레이어를 자기 도형대로 잘라냄)
  const toggleMask = (id) => {
    setGuides((gs) => gs.map((g) => {
      if (g.id !== activeGuideId) return g;
      return { ...g, layers: g.layers.map((l) => l.id === id ? { ...l, isMask: !l.isMask } : l) };
    }));
  };
  const copyLayers = (ids) => {
    if (!activeGuide) return;
    const ls = activeGuide.layers.filter((l) => ids.includes(l.id) && l.type !== "bg");
    if (ls.length) panelClip.current = ls.map((l) => ({ ...l }));
  };
  // 붙여넣기 공통: 복사본을 '원본 바로 위'(같은 그룹 내부)에 끼워 넣고, 다른 레이어/그룹 순서는 그대로 유지.
  const pasteClip = (clip) => {
    if (!activeGuide || !clip || !clip.length) return;
    const groups = activeGuide.groups || {};
    const order = flattenLayerOrder(activeGuide.layers, groups); // 현재 트리 순서(위→아래)
    const validGroup = (gid) => gid && groups[gid] ? gid : null;
    const pasted = clip.map((l) => ({ ...l, id: uid(), x: l.x + 16, y: l.y + 16, groupId: validGroup(l.groupId) }));
    const newOrder = [...order];
    pasted.forEach((p, i) => {
      let at = newOrder.indexOf(clip[i].id); // 원본 위치
      if (at < 0) at = 0;
      newOrder.splice(at, 0, p.id); // 원본 바로 앞(=위)에 삽입
    });
    const n = newOrder.length;
    const zById = {};
    newOrder.forEach((id, i) => {zById[id] = n - i;});
    setGuides((gs) => gs.map((g) => {
      if (g.id !== activeGuideId) return g;
      const withPasted = [...g.layers, ...pasted];
      return { ...g, layers: withPasted.map((l) => l.type === "bg" ? { ...l, z: 0 } : { ...l, z: zById[l.id] ?? l.z }) };
    }));
    setSelectedIds(pasted.map((p) => p.id));
  };
  const pasteLayers = () => {pasteClip(panelClip.current);};
  const duplicateCategory = (id) => {
    const cat = categories.find((c) => c.id === id);
    if (!cat) return;
    const newId = "cat-" + uid();
    const newGuides = guides.filter((g) => g.category === id).map((g) => ({ ...g, id: "g-" + uid(), category: newId }));
    setCategories((cs) => {const idx = cs.findIndex((c) => c.id === id);const arr = [...cs];arr.splice(idx + 1, 0, { ...cat, id: newId, name: cat.name + " 복사본" });return arr;});
    setGuides((gs) => [...gs, ...newGuides]);
  };

  // Compute the current demo step — used to render a pulse hint on the next action
  const demoStep = useMemo(() => {
    if (categories.length === 0) return "addCategory";
    if (guides.filter((g) => g.category === activeCategory).length === 0) return "addGuide";
    if (activeGuide && activeGuide.layers.filter((l) => l.type !== "bg").length === 0) return "addLayer";
    if (activeGuide && !activeGuide.sampleUploaded) return "uploadSample";
    return null;
  }, [categories, guides, activeCategory, activeGuide]);

  // Auto-fit zoom when guide changes
  // Initial zoom when opening a guide is handled inside Canvas (fitToScreen) — it measures
  // the real available area between the panels and toolbar.

  // Switch active guide when category changes
  useEffect(() => {
    if (!activeCategory) return;
    const first = guides.find((g) => g.category === activeCategory);
    if (first && first.id !== activeGuideId) setActiveGuideId(first.id);
    if (!first) setActiveGuideId(null);
  }, [activeCategory]);

  // ---- Step 1: Add a new category named "새 카테고리" (rename via double-click) ----
  const CAT_COLORS = ["#7c5cff", "#4ea8ff", "#1ec997", "#ff9f43", "#ff6b8b", "#9b6bff", "#22b8cf"];
  const addCategory = () => {
    const added = { id: uid(), name: `새 카테고리 ${categories.length + 1}`, color: CAT_COLORS[categories.length % CAT_COLORS.length], count: 0 };
    setCategories((cs) => [...cs, added]);
    setActiveCategory(added.id);
    setActiveGuideId(null);
    setSelectedIds([]);
  };

  // ---- Step 2: Create a new empty guide in the active category ----
  const addGuide = () => {
    if (!activeCategory) return;
    const id = uid();
    const sameCat = guides.filter((g) => g.category === activeCategory);
    // Default canvas size from the brand-guide registered presets (by creation order)
    const presets = brand?.sizePresets && brand.sizePresets.length ? brand.sizePresets : [{ w: 720, h: 360 }];
    const existingCount = guides.filter((g) => g.category === activeCategory).length;
    const dims = presets[existingCount] || presets[presets.length - 1];
    const newGuide = {
      id,
      name: `새 가이드 ${sameCat.length + 1}`,
      category: activeCategory,
      platform: tweaks.platform,
      width: dims.w,
      height: dims.h,
      updatedAt: "방금",
      author: "현재 사용자",
      used: 0,
      sampleUploaded: false,
      layers: [
      { id: uid(), type: "bg", name: "캔버스", x: 0, y: 0, w: dims.w, h: dims.h, locked: true, visible: true, color: "#FFFFFF", z: 0 }]

    };
    setGuides((gs) => [...gs, newGuide]);
    setActiveGuideId(id);
    setSelectedIds([]);
  };

  const updateGuide = (patch) => {
    setGuides((gs) => gs.map((g) => {
      if (g.id !== activeGuideId) return g;
      const ng = { ...g, ...patch };
      // Keep the locked background layer matched to the canvas size
      if (patch.width != null || patch.height != null) {
        ng.layers = g.layers.map((l) => l.type === "bg" ? { ...l, x: 0, y: 0, w: ng.width, h: ng.height } : l);
      }
      return ng;
    }));
  };

  const updateLayer = (id, patch) => {
    // 텍스트: 폰트 크기·글꼴 등 타이포 속성이 바뀌면 바운딩 박스를 내용에 맞게 재측정 (w/h를 직접 지정한 경우는 제외)
    const TYPO = ["content", "size", "font", "weight", "lineHeight", "letterSpacing", "bg", "padding"];
    const remeasure = patch.w === undefined && patch.h === undefined && TYPO.some((k) => k in patch);
    setGuides((gs) => gs.map((g) => g.id !== activeGuideId ? g : {
      ...g,
      layers: g.layers.map((l) => {
        if (l.id !== id) return l;
        const next = { ...l, ...patch };
        if (remeasure && next.type === "text") {const s = measureTextSize(next);next.w = s.w;next.h = s.h;}
        return next;
      })
    }));
  };

  // Apply a patch (or per-id patch map) to many layers at once — used for group move
  const updateManyLayers = (patchMap) => {
    setGuides((gs) => gs.map((g) => g.id !== activeGuideId ? g : {
      ...g,
      layers: g.layers.map((l) => patchMap[l.id] ? { ...l, ...patchMap[l.id] } : l)
    }));
  };

  const addLayer = (type, rect, contentOverride) => {
    const id = uid();
    const maxZ = Math.max(...activeGuide.layers.map((l) => l.z ?? 0));
    const dW = type === "logo" ? 80 : type === "text" ? 240 : 160;
    const dH = type === "logo" ? 80 : type === "text" ? 40 : 160;
    const base = {
      id,
      type,
      visible: true,
      z: maxZ + 1,
      x: rect?.x ?? 60, y: rect?.y ?? 60,
      w: rect?.w ?? dW, h: rect?.h ?? dH
    };
    let layer;
    if (type === "text") layer = { ...base, name: "텍스트", content: contentOverride !== undefined ? contentOverride : "여기에 입력", font: "Pretendard", weight: 600, size: 24, color: "#1a1a1a" };
    if (type === "image") layer = { ...base, name: "이미지", slot: "product", autoCutout: true, autoResize: true };
    if (type === "logo") layer = { ...base, name: "로고", content: "LOGO", color: "#1a1a1a" };
    if (type === "shape") layer = { ...base, name: "도형", color: "#D9D9D9", radius: 0 };
    setGuides((gs) => gs.map((g) => g.id !== activeGuideId ? g : { ...g, layers: [...g.layers, layer] }));
    setSelectedIds([id]);
    return id;
  };

  const deleteLayer = (id) => {
    setGuides((gs) => gs.map((g) => g.id !== activeGuideId ? g : {
      ...g,
      layers: g.layers.filter((l) => l.id !== id)
    }));
    setSelectedIds((prev) => prev.filter((x) => x !== id));
  };

  // Delete every selected (non-locked) layer at once
  const deleteSelected = () => {
    if (!activeGuide) return;
    const ids = activeGuide.layers.filter((l) => selectedIds.includes(l.id) && l.type !== "bg" && !l.locked).map((l) => l.id);
    if (!ids.length) return;
    setGuides((gs) => gs.map((g) => g.id !== activeGuideId ? g : { ...g, layers: g.layers.filter((l) => !ids.includes(l.id)) }));
    setSelectedIds([]);
  };

  // Align selected (movable) layers relative to their combined bounding box
  const alignLayers = (mode) => {
    if (!activeGuide) return;
    const grps = activeGuide.groups || {};
    const movable = activeGuide.layers.filter((l) => selectedIds.includes(l.id) && l.type !== "bg" && !l.locked);
    if (movable.length < 2) return;
    // 그룹은 하나의 강체 단위로 묶어 정렬 (그룹 내부 레이어끼리는 움직이지 않음)
    const units = selectionUnits(activeGuide.layers, grps, new Set(movable.map((l) => l.id)));
    if (units.length < 2) return;
    const minX = Math.min(...units.map((u) => u.minX));
    const maxX = Math.max(...units.map((u) => u.maxX));
    const minY = Math.min(...units.map((u) => u.minY));
    const maxY = Math.max(...units.map((u) => u.maxY));
    const cx = (minX + maxX) / 2,cy = (minY + maxY) / 2;
    const patchMap = {};
    units.forEach((u) => {
      let dx = 0,dy = 0;
      if (mode === "left") dx = minX - u.minX;else
      if (mode === "centerH") dx = cx - u.cx;else
      if (mode === "right") dx = maxX - u.maxX;else
      if (mode === "top") dy = minY - u.minY;else
      if (mode === "middleV") dy = cy - u.cy;else
      if (mode === "bottom") dy = maxY - u.maxY;
      if (!dx && !dy) return;
      u.layers.forEach((l) => {
        const p = {};
        if (dx) p.x = Math.round(l.x + dx);
        if (dy) p.y = Math.round(l.y + dy);
        patchMap[l.id] = p;
      });
    });
    updateManyLayers(patchMap);
  };

  // 선택 단위(그룹=강체)들을 주축 기준으로 지정 간격으로 균등 배치 (첫 단위 고정)
  const setLayerGap = (gap) => {
    if (!activeGuide) return;
    const grps = activeGuide.groups || {};
    const parts = activeGuide.layers.filter((l) => selectedIds.includes(l.id) && l.type !== "bg");
    if (parts.length < 2) return;
    const units = selectionUnits(activeGuide.layers, grps, new Set(parts.map((l) => l.id)));
    if (units.length < 2) return;
    // X축으로 겹치면(같은 열) 세로 배치, Y축으로 겹치면(같은 행) 가로 배치 — 단위 바운딩 박스 기준
    const xOverlap = Math.min(...units.map((u) => u.maxX)) - Math.max(...units.map((u) => u.minX));
    const yOverlap = Math.min(...units.map((u) => u.maxY)) - Math.max(...units.map((u) => u.minY));
    if (xOverlap > 0 && yOverlap > 0) return; // 겹친 경우 간격 개념 없음
    const horizontal = yOverlap > xOverlap;
    const sorted = [...units].sort((a, b) => horizontal ? a.minX - b.minX : a.minY - b.minY);
    const patchMap = {};
    let cursor = horizontal ? sorted[0].maxX + gap : sorted[0].maxY + gap;
    for (let i = 1; i < sorted.length; i++) {
      const u = sorted[i];
      if (horizontal) {
        const dx = Math.round(cursor) - u.minX;
        u.layers.forEach((l) => {patchMap[l.id] = { x: Math.round(l.x + dx) };});
        cursor = Math.round(cursor) + (u.maxX - u.minX) + gap;
      } else {
        const dy = Math.round(cursor) - u.minY;
        u.layers.forEach((l) => {patchMap[l.id] = { y: Math.round(l.y + dy) };});
        cursor = Math.round(cursor) + (u.maxY - u.minY) + gap;
      }
    }
    updateManyLayers(patchMap);
  };

  const changeZ = (where) => {
    if (!selectedLayer) return;
    const sorted = [...activeGuide.layers].sort((a, b) => (a.z ?? 0) - (b.z ?? 0));
    const idx = sorted.findIndex((l) => l.id === selectedLayerId);
    let newSorted = [...sorted];
    const [moved] = newSorted.splice(idx, 1);
    if (where === "front") newSorted.push(moved);else
    if (where === "back") newSorted.unshift(moved);else
    if (where === "forward") newSorted.splice(Math.min(idx + 1, newSorted.length), 0, moved);else
    if (where === "backward") newSorted.splice(Math.max(idx - 1, 0), 0, moved);
    const patched = newSorted.map((l, i) => ({ ...l, z: i }));
    setGuides((gs) => gs.map((g) => g.id !== activeGuideId ? g : { ...g, layers: patched }));
  };

  // Drag-to-reorder in the layer list (top of list = front-most). Background stays at the back.
  // 트리(패널) 순서 배열대로 z 재할당 + 패치 적용 (위=높은 z). bg는 0 고정.
  const applyLayerOrder = (orderIds, patches = {}) => {
    const n = orderIds.length;
    const zById = {};
    orderIds.forEach((id, i) => {zById[id] = n - i;});
    setGuides((gs) => gs.map((g) => {
      if (g.id !== activeGuideId) return g;
      const layers = g.layers.map((l) => l.type === "bg" ? { ...l, z: 0 } : { ...l, z: zById[l.id] ?? l.z, ...(patches[l.id] || {}) });
      return { ...g, layers };
    }));
  };
  // 레이어 행 사이로 드롭 → 대상 레이어의 컨테이너에 합류하고, 패널에서 본 위치 그대로 배치
  const reorderLayer = (fromId, toId, pos) => {
    if (!activeGuide || fromId === toId) return;
    const groups = activeGuide.groups || {};
    const target = activeGuide.layers.find((l) => l.id === toId);
    if (!target || target.type === "bg") return;
    const newGroupId = target.groupId || null;
    let order = flattenLayerOrder(activeGuide.layers, groups).filter((id) => id !== fromId);
    let idx = order.indexOf(toId);
    if (idx < 0) return;
    if (pos === "below") idx += 1;
    order.splice(idx, 0, fromId);
    applyLayerOrder(order, { [fromId]: { groupId: newGroupId } });
  };

  // Keyboard nav — operates on the whole selection
  useEffect(() => {
    const onKey = (e) => {
      if (!activeGuide || selectedIds.length === 0) return;
      // Ignore when typing in inputs
      const tag = (e.target.tagName || "").toLowerCase();
      if (tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable) return;

      // Movable = selected, not background, not locked
      const movable = activeGuide.layers.filter((l) => selectedIds.includes(l.id) && l.type !== "bg" && !l.locked);
      const step = e.shiftKey ? 10 : 1;
      let axis = null,delta = 0;
      if (e.key === "ArrowLeft") {axis = "x";delta = -step;} else
      if (e.key === "ArrowRight") {axis = "x";delta = step;} else
      if (e.key === "ArrowUp") {axis = "y";delta = -step;} else
      if (e.key === "ArrowDown") {axis = "y";delta = step;}

      if (axis) {
        if (!movable.length) return;
        e.preventDefault();
        const patchMap = {};
        movable.forEach((l) => {patchMap[l.id] = { [axis]: l[axis] + delta };});
        updateManyLayers(patchMap);
        return;
      }
      if (e.key === "Delete" || e.key === "Backspace") {
        if (movable.length) {e.preventDefault();deleteSelected();}
        return;
      }
      if ((e.key === "d" || e.key === "D") && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        if (!movable.length) return;
        let z = Math.max(...activeGuide.layers.map((l) => l.z ?? 0));
        const dupes = movable.map((l) => ({ ...l, id: uid(), x: l.x + 10, y: l.y + 10, z: ++z, name: l.name + " 복사본" }));
        setGuides((gs) => gs.map((g) => g.id !== activeGuideId ? g : { ...g, layers: [...g.layers, ...dupes] }));
        setSelectedIds(dupes.map((d) => d.id));
        return;
      }
      if ((e.key === "g" || e.key === "G") && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        if (e.shiftKey) ungroupSelected();else groupSelected();
        return;
      }
      if (e.key === "Escape") {setSelectedIds([]);}
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [selectedIds, activeGuide, activeGuideId]);
  const clipboard = useRef([]);
  useEffect(() => {
    const onCopyPaste = (e) => {
      if (!activeGuide) return;
      const tag = (e.target.tagName || "").toLowerCase();
      if (tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable) return;
      if (!(e.metaKey || e.ctrlKey)) return;
      const k = (e.key || "").toLowerCase();
      if (k === "c") {
        const sel = activeGuide.layers.filter((l) => selectedIds.includes(l.id) && l.type !== "bg");
        if (sel.length) {clipboard.current = sel.map((l) => ({ ...l }));e.preventDefault();}
      } else if (k === "v") {
        if (!clipboard.current.length) return;
        e.preventDefault();
        pasteClip(clipboard.current);
      }
    };
    window.addEventListener("keydown", onCopyPaste);
    return () => window.removeEventListener("keydown", onCopyPaste);
  }, [selectedIds, activeGuide, activeGuideId]);

  // ---- Delete a guide ----
  const deleteGuide = (id) => {
    setGuides((gs) => gs.filter((g) => g.id !== id));
    if (activeGuideId === id) {
      setActiveGuideId(null);
      setSelectedIds([]);
    }
  };

  // ---- Duplicate a guide (deep-copy layers with fresh ids), insert right after ----
  const duplicateGuide = (id) => {
    setGuides((gs) => {
      const idx = gs.findIndex((g) => g.id === id);
      if (idx < 0) return gs;
      const src = gs[idx];
      const copy = {
        ...src,
        id: uid(),
        name: src.name + " 복사본",
        used: 0,
        updatedAt: "방금",
        layers: src.layers.map((l) => ({ ...l, id: uid() }))
      };
      const next = [...gs];
      next.splice(idx + 1, 0, copy);
      setActiveGuideId(copy.id);
      return next;
    });
  };

  // ---- Move a guide into another category (drag & drop) ----
  const moveGuideToCategory = (id, catId) => {
    setGuides((gs) => gs.map((g) => g.id === id ? { ...g, category: catId } : g));
    setActiveCategory(catId);
    setActiveGuideId(id);
  };

  // ---- Delete a category (cascades — removes all guides in it) ----
  const deleteCategory = (id) => {
    setCategories((cs) => cs.filter((c) => c.id !== id));
    setGuides((gs) => gs.filter((g) => g.category !== id));
    if (activeCategory === id) {
      setActiveCategory(null);
      setActiveGuideId(null);
      setSelectedIds([]);
    }
  };

  // ---- Confirm dialog state ----
  const [confirm, setConfirm] = useState(null); // { title, message, onConfirm } | null
  const askConfirm = (cfg) => setConfirm(cfg);
  const closeConfirm = () => setConfirm(null);

  const markSampleUploaded = () => {
    // Pick a tasteful background per category for the uploaded mockup
    const bgs = {
      main: "linear-gradient(135deg, #FFE9DA 0%, #FFD4B8 100%)",
      category: "linear-gradient(135deg, #E8F4FF 0%, #CDE6FF 100%)",
      product: "linear-gradient(135deg, #F7F7F5 0%, #ECECEA 100%)",
      timedeal: "linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%)",
      brand: "linear-gradient(135deg, #FFF3D6 0%, #FFE49E 100%)",
      popup: "linear-gradient(135deg, #F0E8FF 0%, #DDC9FF 100%)",
      live: "linear-gradient(135deg, #FFE0E8 0%, #FFC4D2 100%)"
    };
    setGuides((gs) => gs.map((g) => g.id !== activeGuideId ? g : {
      ...g,
      sampleUploaded: true,
      sampleImageBg: g.sampleImageBg || bgs[g.category] || "linear-gradient(135deg, #F5F5F7, #E5E5EA)"
    }));
  };

  return (
    <div className="workspace">
      <LeftPanel
        categories={categories}
        activeCategory={activeCategory}
        setActiveCategory={setActiveCategory}
        guides={guides}
        activeGuideId={activeGuideId}
        setActiveGuideId={setActiveGuideId}
        selectedIds={selectedIds}
        selectedGroupIds={selectedGroupIds}
        onSelectLayer={selectLayer}
        onAddCategory={addCategory}
        onAddGuide={addGuide}
        onRenameCategory={(id, name) => setCategories((cs) => cs.map((c) => c.id === id ? { ...c, name } : c))}
        onSetCategoryColor={(id, color) => setCategories((cs) => cs.map((c) => c.id === id ? { ...c, color } : c))}
        onRenameGuide={(id, name) => setGuides((gs) => gs.map((g) => g.id === id ? { ...g, name } : g))}
        onDuplicateGuide={duplicateGuide}
        onMoveGuideToCategory={moveGuideToCategory}
        onRenameLayer={(id, name) => updateLayer(id, { name })}
        onRenameGroup={renameGroup}
        onToggleGroupLock={toggleGroupLock}
        onToggleGroupVis={toggleGroupVis}
        onToggleGroupCollapse={toggleGroupCollapse}
        onUngroupGroup={ungroupGroup}
        onSelectGroup={selectGroup}
        onNestGroup={nestGroup}
        onMoveLayerToGroup={moveLayerToGroup}
        onMoveLayerBeforeGroup={moveLayerBeforeGroup}
        onGroup={groupSelected}
        onDeleteGroup={deleteGroup}
        onDuplicateLayer={duplicateLayer}
        onCopyLayers={copyLayers}
        onPasteLayers={pasteLayers}
        onToggleMask={toggleMask}
        onDuplicateCategory={duplicateCategory}
        onDeleteCategory={(c) => askConfirm({
          title: "카테고리를 삭제하시겠습니까?",
          message: `"${c.name}" 카테고리와 포함된 가이드 ${guides.filter((g) => g.category === c.id).length}개가 모두 삭제됩니다. 이 작업은 되돌릴 수 없습니다.`,
          confirmLabel: "카테고리 삭제",
          onConfirm: () => {deleteCategory(c.id);closeConfirm();}
        })}
        onDeleteGuide={(g) => askConfirm({
          title: "가이드를 삭제하시겠습니까?",
          message: `"${g.name}" 가이드가 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다.`,
          confirmLabel: "가이드 삭제",
          onConfirm: () => {deleteGuide(g.id);closeConfirm();}
        })}
        onAddLayer={addLayer}
        onToggleVis={(id) => updateLayer(id, { visible: !activeGuide.layers.find((l) => l.id === id).visible })}
        onToggleLock={(id) => updateLayer(id, { locked: !activeGuide.layers.find((l) => l.id === id).locked })}
        onReorder={reorderLayer}
        onDeleteLayer={deleteLayer}
        demoStep={demoStep}
        canAddCategory={true} />
      

      {activeGuide ?
      <Canvas
        guide={activeGuide}
        selectedIds={selectedIds}
        onSelect={selectLayer}
        onSelectMany={(ids) => {setSelectedGroupIds([]);setSelectedIds(ids);}}
        onUpdateLayer={updateLayer}
        onUpdateManyLayers={updateManyLayers}
        onAddLayer={addLayer}
        onDeleteLayer={deleteLayer}
        zoom={zoom}
        setZoom={setZoom}
        showGrid={showGrid}
        onToggleGrid={() => setShowGrid((s) => !s)}
        onUndo={undo}
        canUndo={canUndo}
        onRedo={redo}
        canRedo={canRedo}
        onGestureStart={beginGesture}
        onGestureEnd={endGesture}
        onRevealGroups={expandGroups}
        onRenameGuide={(name) => updateGuide({ name })} /> :


      <CanvasEmpty demoStep={demoStep} categories={categories} onAddCategory={addCategory} onAddGuide={addGuide} />
      }

      <RightInspector
        guide={activeGuide}
        layer={selectedLayer}
        selectedLayers={selectedLayers}
        onDeleteSelected={deleteSelected}
        onUpdateGuide={updateGuide}
        onUpdateLayer={(patch) => updateLayer(selectedLayerId, patch)}
        onDeleteLayer={deleteLayer}
        onChangeZ={changeZ}
        onUploadSample={markSampleUploaded}
        demoStep={demoStep}
        brandColors={brand?.colors}
        sizePresets={brand?.sizePresets}
        onAlign={alignLayers}
        onSetGap={setLayerGap}
        onGroup={groupSelected}
        onUngroup={ungroupSelected}
        onUpdateGroup={updateGroup}
        zoom={zoom}
        setZoom={setZoom} />
      

      <ConfirmDialog
        open={!!confirm}
        title={confirm?.title}
        message={confirm?.message}
        confirmLabel={confirm?.confirmLabel || "삭제"}
        onConfirm={confirm?.onConfirm}
        onCancel={closeConfirm} />
      
    </div>);

}

// ---------- Confirm dialog ----------
function ConfirmDialog({ open, title, message, confirmLabel, onConfirm, onCancel }) {
  useEffect(() => {
    if (!open) return;
    const onKey = (e) => {
      if (e.key === "Escape") onCancel?.();
      if (e.key === "Enter") onConfirm?.();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onCancel, onConfirm]);

  if (!open) return null;
  return (
    <div className="confirm-overlay" onMouseDown={onCancel}>
      <div className="confirm-dialog" onMouseDown={(e) => e.stopPropagation()}>
        <header className="confirm-head">
          <div className="confirm-title">{title}</div>
          <button className="btn ghost sm icon-only confirm-close" onClick={onCancel} title="닫기" style={{ borderRadius: "6px" }}><I.Close size={16} /></button>
        </header>
        <div className="confirm-main">
          <div className="confirm-message">{message}</div>
        </div>
        <div className="confirm-actions">
          <button className="btn" onClick={onCancel} style={{ borderRadius: "6px", fontWeight: "600" }}>취소</button>
          <button className="btn confirm-danger" autoFocus onClick={onConfirm} style={{ borderRadius: "6px", fontWeight: "600" }}>{confirmLabel}</button>
        </div>
      </div>
    </div>);

}

// ---------- Canvas empty state (shown when no guide is active) ----------
function CanvasEmpty({ demoStep, categories, onAddCategory, onAddGuide }) {
  const noCat = categories.length === 0;
  const message = noCat ?
  { title: "관리자 영역에 오신 것을 환영합니다", body: "카테고리를 추가하여 시작하세요.", cta: "새 카테고리 만들기", action: onAddCategory } :
  { title: "가이드를 만들어 보세요", body: "선택한 카테고리에 사용할 새 가이드를 만들어보세요.", cta: "새 가이드 만들기", action: onAddGuide };
  return (
    <div className="canvas-wrap canvas-wrap-empty">
      <div className="canvas-empty">
        <div className="canvas-empty-art">
          <svg width="120" height="80" viewBox="0 0 120 80" fill="none">
            <rect x="2" y="2" width="116" height="76" rx="6" stroke="var(--border-strong)" strokeWidth="1.5" strokeDasharray="5 4" fill="var(--surface-0)" />
            <rect x="18" y="22" width="40" height="8" rx="2" fill="var(--surface-2)" />
            <rect x="18" y="36" width="60" height="8" rx="2" fill="var(--surface-2)" />
            <rect x="18" y="50" width="28" height="10" rx="5" fill="var(--accent-soft)" />
            <circle cx="92" cy="40" r="14" fill="var(--surface-2)" />
          </svg>
        </div>
        <div className="canvas-empty-title">{message.title}</div>
        <div className="canvas-empty-body">{message.body}</div>
        <button className="btn primary" style={{ fontWeight: 700, marginTop: 16, borderRadius: "6px", height: "32px", paddingLeft: "12px", paddingRight: "12px" }} onClick={message.action}>
          <I.Plus size={12} /> {message.cta}
        </button>
      </div>
    </div>);

}

Object.assign(window, { AdminEditor, LayerInner, ImagePlaceholder, useToasts, ColorField, BrandChips, BrandColorMenu, NumField, Toggle, readableOn, ConfirmDialog });

// ---------- Brand Guide Manager (modal) ----------
// Manages a platform's brand colors / logo / grid. Color chips here surface in every color picker.
function BrandGuideManager({ platform, brand, onChange, onClose }) {
  const [tab, setTab] = useState("colors");
  const colors = brand.colors || [];

  useEffect(() => {
    const onKey = (e) => {if (e.key === "Escape") onClose?.();};
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  const setColors = (next) => onChange({ colors: next });
  const addColor = () => setColors([...colors, { id: uid(), name: "새 색상", value: "#888888" }]);
  const updColor = (id, patch) => setColors(colors.map((c) => c.id === id ? { ...c, ...patch } : c));
  const delColor = (id) => setColors(colors.filter((c) => c.id !== id));

  const grid = brand.grid || { columns: 12, gutter: 20, margin: 60, show: false };
  const setGrid = (patch) => onChange({ grid: { ...grid, ...patch } });

  const sizePresets = brand.sizePresets || [];
  const setPresets = (next) => onChange({ sizePresets: next });
  const addPreset = () => setPresets([...sizePresets, { w: 1080, h: 1080 }]);
  const updPreset = (i, patch) => setPresets(sizePresets.map((p, idx) => idx === i ? { ...p, ...patch } : p));
  const delPreset = (i) => setPresets(sizePresets.filter((_, idx) => idx !== i));

  return (
    <div className="brand-overlay" onMouseDown={onClose}>
      <div className="brand-modal" onMouseDown={(e) => e.stopPropagation()}>
        <header className="brand-modal-head">
          <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
            <span className="brand-modal-ic" style={{ background: platform.color }}>
              <I.Palette size={16} stroke="#fff" />
            </span>
            <div>
              <div className="brand-modal-title">브랜드 가이드</div>
            </div>
          </div>
          <button className="btn ghost sm icon-only" onClick={onClose} title="닫기" style={{ borderRadius: "6px" }}><I.Close size={14} /></button>
        </header>

        <div className="brand-tabs">
          <button className={cls(tab === "colors" && "is-active")} onClick={() => setTab("colors")}>
            <I.Palette size={13} /> 색상 <span className="count">{colors.length}</span>
          </button>
          <button className={cls(tab === "logo" && "is-active")} onClick={() => setTab("logo")}>
            <I.Logo size={13} /> 로고
          </button>
          <button className={cls(tab === "grid" && "is-active")} onClick={() => setTab("grid")}>
            <I.Grid size={13} /> 그리드
          </button>
          <button className={cls(tab === "size" && "is-active")} onClick={() => setTab("size")}>
            <I.Square size={13} /> 사이즈 <span className="count">{sizePresets.length}</span>
          </button>
        </div>

        <div className="brand-modal-body">
          {tab === "colors" &&
          <>
              <div className="brand-color-grid">
                {colors.map((c) =>
              <div key={c.id} className="brand-color-card">
                    <label className="brand-color-swatch" style={{ background: c.value }}>
                      <input type="color" value={c.value} onChange={(e) => updColor(c.id, { value: e.target.value })} />
                    </label>
                    <div className="brand-color-fields">
                      <input className="input sm" value={c.name} onKeyDown={(e) => {if (e.key === "Enter") e.currentTarget.blur();}} onChange={(e) => updColor(c.id, { name: e.target.value })} />
                      <input
                    className="input sm mono"
                    value={c.value}
                    onKeyDown={(e) => {if (e.key === "Enter") e.currentTarget.blur();}}
                    onChange={(e) => updColor(c.id, { value: e.target.value })} />
                  
                    </div>
                    <button className="btn ghost sm danger icon-only" title="삭제" onClick={() => delColor(c.id)}>
                      <I.Trash size={12} />
                    </button>
                  </div>
              )}
                <button className="brand-color-add" onClick={addColor}>
                  <I.Plus size={16} /> 색상 추가
                </button>
              </div>
            </>
          }

          {tab === "logo" &&
          <div className="brand-logo-tab">
              <div className="brand-logo-preview">
                <div className="brand-logo-chip" style={{ background: brand.logoBg, color: brand.logoFg }}>
                  {brand.logoText || "LOGO"}
                </div>
                <div className="brand-logo-preview-dim">미리보기 · 실제 배너에 이 로고가 자동 삽입됩니다</div>
              </div>
              <div className="brand-logo-controls">
                <button className="btn" style={{ width: "100%", justifyContent: "center" }}>
                  <I.Upload size={13} /> 로고 SVG / PNG 업로드
                </button>
                <div className="label" style={{ marginTop: 10 }}>로고 텍스트 (이미지 미등록 시)</div>
                <input className="input" value={brand.logoText || ""} onKeyDown={(e) => {if (e.key === "Enter") e.currentTarget.blur();}} onChange={(e) => onChange({ logoText: e.target.value })} />
                <div className="row grid-2" style={{ marginTop: 8 }}>
                  <ColorField label="배경" value={brand.logoBg} onChange={(v) => onChange({ logoBg: v })} clearable brandColors={colors} />
                  <ColorField label="글자" value={brand.logoFg ?? ""} placeholder="없음" onChange={(v) => onChange({ logoFg: v })} clearable brandColors={colors} />
                </div>
              </div>
            </div>
          }

          {tab === "grid" &&
          <div className="brand-grid-tab">
              <div className="brand-grid-preview">
                <div className="brand-grid-art" style={{ padding: Math.min(grid.margin / 4, 24) }}>
                  {Array.from({ length: Math.min(grid.columns, 16) }).map((_, i) =>
                <div key={i} className="brand-grid-col" style={{ marginRight: i < grid.columns - 1 ? Math.min(grid.gutter / 3, 10) : 0 }} />
                )}
                </div>
              </div>
              <div className="brand-grid-controls">
                <div className="row grid-2">
                  <NumField label="컬럼" value={grid.columns} onChange={(v) => setGrid({ columns: Math.max(1, Math.min(24, Math.round(v))) })} />
                  <NumField label="거터" value={grid.gutter} onChange={(v) => setGrid({ gutter: v })} suffix="px" />
                </div>
                <div className="row grid-2" style={{ marginTop: 8 }}>
                  <NumField label="여백" value={grid.margin} onChange={(v) => setGrid({ margin: v })} suffix="px" />
                  <div className="row" style={{ justifyContent: "space-between", alignItems: "center", background: "var(--surface-1)", borderRadius: 6, padding: "0 10px" }}>
                    <span className="label">캔버스 오버레이</span>
                    <Toggle checked={grid.show} onChange={(v) => setGrid({ show: v })} />
                  </div>
                </div>
                <div className="brand-hint" style={{ marginTop: 12 }}>
                  그리드는 가이드 제작 시 정렬 기준으로 사용됩니다. 오버레이를 켜면 캔버스 위에 표시됩니다.
                </div>
              </div>
            </div>
          }

          {tab === "size" &&
          <div className="brand-size-tab">
              <div className="brand-size-list">
                {sizePresets.map((p, i) =>
              <div key={i} className="brand-size-card">
                    <NumField label="W" value={p.w} onChange={(v) => updPreset(i, { w: Math.max(1, Math.round(v)) })} />
                    <span className="brand-size-x">×</span>
                    <NumField label="H" value={p.h} onChange={(v) => updPreset(i, { h: Math.max(1, Math.round(v)) })} />
                    <button className="btn ghost sm danger icon-only" title="삭제" onClick={() => delPreset(i)}>
                      <I.Trash size={12} />
                    </button>
                  </div>
              )}
                <button className="brand-color-add" onClick={addPreset}>
                  <I.Plus size={16} /> 사이즈 추가
                </button>
              </div>
              <div className="brand-hint" style={{ marginTop: 12 }}>
                여기서 등록한 배너 사이즈는 가이드 편집 화면의 <b>사이즈 드롭다운</b>에 표시됩니다.
              </div>
            </div>
          }
        </div>

        <footer className="brand-modal-foot">
          <span style={{ fontSize: 11.5, color: "var(--text-3)" }}>
            <I.Check size={11} stroke="var(--success)" /> 변경 사항은 자동 저장됩니다
          </span>
          <button className="btn primary" onClick={onClose} style={{ borderRadius: "6px" }}>완료</button>
        </footer>
      </div>
    </div>);

}

Object.assign(window, { BrandGuideManager });