/* ============================================================
   提案書デザイン画面 — 生成した提案書をそのまま画面に表示し、その場で直してPPTXに書き戻す。
   これまでは「生成 → ダウンロード → PowerPoint/Canvaで確認」しないと仕上がりが見えなかった。
   サーバーがPPTXを図形の座標つきJSON（/api/proposal/deck）で返すので、それを実寸1920×1080の
   絶対配置で再現している。文字をクリックするとその場で編集でき、保存すると同じPPTXの
   テキストだけ差し替えた新版が案件に追加される（レイアウト・色・画像には触れない）。
   ============================================================ */
/* リサイズのつまみ（PowerPointと同じ8方向） */
const DECK_HANDLES = [
  { k: 'nw', l: -5, t: -5, c: 'nwse-resize' }, { k: 'n', l: '50%', t: -5, tf: 'translateX(-50%)', c: 'ns-resize' },
  { k: 'ne', r: -5, t: -5, c: 'nesw-resize' }, { k: 'w', l: -5, t: '50%', tf: 'translateY(-50%)', c: 'ew-resize' },
  { k: 'e', r: -5, t: '50%', tf: 'translateY(-50%)', c: 'ew-resize' }, { k: 'sw', l: -5, b: -5, c: 'nesw-resize' },
  { k: 's', l: '50%', b: -5, tf: 'translateX(-50%)', c: 'ns-resize' }, { k: 'se', r: -5, b: -5, c: 'nwse-resize' },
];
function DeckEditor({ caseData, onClose, inline, onLoadError, controlRef, locked = false, initialPage = 0 }) {   // inline=true で作成スタジオ内に埋め込み（モーダルにしない）。onLoadError=読込失敗を親に通知（旧形式HTML等）
  const { showToast, syncProposalResult } = useStore();
  const [deck, setDeck] = React.useState(null);
  const [err, setErr] = React.useState('');
  const [page, setPage] = React.useState(initialPage);
  const [sel, setSel] = React.useState(null);       // 選択中の図形 index
  const [edits, setEdits] = React.useState({});     // "p:i" -> 文字列
  const [geo, setGeo] = React.useState({});         // "p:i" -> {x,y,w,h}（ドラッグ移動・リサイズ）
  const [ops, setOps] = React.useState({});         // "p:i" -> {del:true} / {dup:true}
  const [styles, setStyles] = React.useState({});   // "p:i" -> {size,color,bold,align}（見た目の調整）
  const [guides, setGuides] = React.useState(null); // 位置合わせのガイド線
  const drag = React.useRef(null);
  const [busy, setBusy] = React.useState(false);
  const saving = React.useRef(false);
  const blocked = React.useRef(false); blocked.current = locked || busy;
  const [zoom, setZoom] = React.useState(0.46);
  const centerRef = React.useRef(null);            // inline時：中央エリアの幅にスライドを自動フィット

  React.useEffect(() => {
    if (!inline || !deck || !centerRef.current) return;
    const fit = () => {
      const el = centerRef.current; if (!el) return;
      const z = Math.max(0.08, Math.min(0.8, (el.clientWidth - 44) / 1920));
      if (isFinite(z) && z > 0) setZoom(z);
    };
    const raf = requestAnimationFrame(fit);
    window.addEventListener('resize', fit);
    return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', fit); };
  }, [inline, !!deck]);

  React.useEffect(() => {
    let alive = true;
    API.proposalDeck(caseData.id)
      .then(d => { if (alive) { setDeck(d); setPage(p => Math.min(p, Math.max(0, d.pages.length - 1))); } })
      .catch(e => { if (!alive) return; setErr((e && e.message) || t('de.loadFail')); if (onLoadError) onLoadError(e); });
    return () => { alive = false; };
  }, [caseData.id]);

  const key = (p, i) => p + ':' + i;
  const textOf = (p, sh) => (key(p, sh.i) in edits ? edits[key(p, sh.i)] : sh.text);
  const boxOf = (p, sh) => geo[key(p, sh.i)] || { x: sh.x, y: sh.y, w: sh.w, h: sh.h };
  const styleOf = (p, sh) => ({ size: sh.size || 26, color: sh.color || '', bold: !!sh.bold, align: sh.align || 'left', ...(styles[key(p, sh.i)] || {}) });
  const setStyle = (p, i, patch) => setStyles(x => ({ ...x, [key(p, i)]: { ...(x[key(p, i)] || {}), ...patch } }));
  const dirty = Object.keys(edits).length + Object.keys(geo).length + Object.keys(ops).length + Object.keys(styles).length;

  if (controlRef) controlRef.current = { prepare: () => {
    if (!deck || err) throw new Error(t("studio.notLoaded"));
    if (saving.current || dirty) throw new Error(t("studio.saveManualFirst"));
    return deck.attId;
  } };

  /* extras＝このページへの追加要素（{add:'text'|'image',...}）。追加は即保存→再読込で
     本物の図形になり、以後は既存の編集（ドラッグ・文字・見た目）がそのまま使える */
  const save = async (extras) => {
    const hasExtras = Array.isArray(extras) && extras.length > 0;
    if ((!dirty && !hasExtras) || saving.current || locked || !deck) return;
    saving.current = true; setBusy(true);
    try {
      const byPage = {};
      const put = (k, patch) => {
        const [p, i] = k.split(':').map(Number);
        const list = (byPage[p] = byPage[p] || []);
        const hit = list.find(x => x.i === i);
        if (hit) Object.assign(hit, patch); else list.push({ i, ...patch });
      };
      Object.keys(edits).forEach(k => put(k, { text: edits[k] }));
      Object.keys(geo).forEach(k => put(k, geo[k]));
      Object.keys(ops).forEach(k => put(k, ops[k]));
      Object.keys(styles).forEach(k => put(k, styles[k]));
      if (hasExtras) { const list = (byPage[page] = byPage[page] || []); extras.forEach(a => list.push(a)); }
      const pages = Object.keys(byPage).map(p => ({ p: Number(p) + 1, slots: byPage[p] }));
      const r = await API.proposalDeckSave(caseData.id, pages, deck.attId);
      syncProposalResult(caseData.id, r);
      /* 保存後は新しいPPTXを読み直す。edits を消すだけだと画面上の deck が保存前のままで、
         直したはずの文字が元に戻ったように見えてしまう。 */
      const fresh = await API.proposalDeck(caseData.id);
      setDeck(fresh);
      setEdits({}); setGeo({}); setOps({}); setStyles({});
      showToast(hasExtras ? t('de.added') : t('de.saved'));
    } catch (e) { showToast((e && e.message) || t('de.saveFail'), 'x'); }
    setBusy(false); saving.current = false;
  };
  // テキスト枠の追加（スライド中央）→ 保存後にドラッグ・編集で調整
  const addText = () => save([{ add: 'text', x: 660, y: 500, w: 600, h: 60, text: t('de.newTextDefault'), size: 26, color: '0b0b2e', bold: false, align: 'left' }]);
  // 画像の追加：図形を選択中ならその枠にはめる（スクショ差し込み）、未選択ならスライド中央
  const addImageData = (dataUrl) => {
    const pgc = deck && deck.pages[page];
    const shc = pgc && (pgc.shapes || []).find(x => x.i === sel);
    const b = shc ? boxOf(page, shc) : { x: 468, y: 234, w: 984, h: 612 };
    save([{ add: 'image', x: b.x, y: b.y, w: b.w, h: b.h, data: dataUrl }]);
  };
  const addImageRef = React.useRef(null); addImageRef.current = addImageData;
  const fileRef = React.useRef(null);
  const pickImage = (e) => {
    const f = e.target.files && e.target.files[0];
    e.target.value = '';
    if (!f) return;
    if (f.size > 8 * 1024 * 1024) { showToast(t('de.imgTooBig'), 'x'); return; }
    const rd = new FileReader();
    rd.onload = () => addImageRef.current(String(rd.result));
    rd.readAsDataURL(f);
  };
  /* クリップボードのスクショを ⌘V で直接貼り付け（画像のときだけ横取りする） */
  React.useEffect(() => {
    const onPaste = (e) => {
      if (blocked.current) return;
      const items = (e.clipboardData && e.clipboardData.items) || [];
      for (const it of items) {
        if (it.type && it.type.indexOf('image/') === 0) {
          const f = it.getAsFile();
          if (f) {
            if (f.size > 8 * 1024 * 1024) { showToast(t('de.imgTooBig'), 'x'); return; }
            e.preventDefault();
            const rd = new FileReader();
            rd.onload = () => addImageRef.current(String(rd.result));
            rd.readAsDataURL(f);
          }
          return;
        }
      }
    };
    window.addEventListener('paste', onPaste);
    return () => window.removeEventListener('paste', onPaste);
  }, []);

  /* --- 1スライドの描画：中身は1920×1080の等倍で組み、CSS transform でまとめて縮小する。
     数値縮小（font-size×scale）だと日本語環境のブラウザ最小フォント（約10px）に当たって
     サムネイルで文字が縮まず枠外に溢れる。transform は最小フォント制限を受けない --- */
  const renderSlide = (pg, pi, scale, interactive) => {
    const BW = deck.w || 1920, BH = deck.h || 1080;
    const W = BW * scale, H = BH * scale;
    const media = deck.media || {};
    const bgUrl = pg.bgImage && media[pg.bgImage] ? media[pg.bgImage] : '';
    return (
      <div style={{ position: 'relative', width: W, height: H, background: pg.bg ? '#' + pg.bg : '#fff',
        backgroundImage: bgUrl ? `url(${bgUrl})` : 'none', backgroundSize: 'cover', backgroundPosition: 'center',
        /* contain:paint＝transformされた子でも必ずこの箱で描画を打ち切る（Safari等で
           overflow:hidden をtransform子がすり抜けて枠外にはみ出す既知挙動への対策） */
        borderRadius: 6, overflow: 'hidden', contain: 'paint', border: '1px solid #e4e7ef', flex: '0 0 auto' }}>
      {/* 内側（transform要素自身）のoverflow:hiddenは縮小前の座標系で効く＝ブラウザ差なくスライド境界で確実に切れる */}
      <div style={{ position: 'absolute', left: 0, top: 0, width: BW, height: BH, transform: `scale(${scale})`, transformOrigin: '0 0', overflow: 'hidden' }}>
        {/* 画像（ロゴなど）。取れなかったものは出さない */}
        {(pg.pics || []).map((p, k) => (media[p.src] ? (
          <img key={'p' + k} src={media[p.src]} alt="" style={{ position: 'absolute', left: p.x, top: p.y, width: p.w, height: p.h, objectFit: 'contain' }} />
        ) : null))}
        {(pg.shapes || []).filter(sh => !(ops[key(pi, sh.i)] || {}).del).map(sh => {
          const t = textOf(pi, sh);
          const bx = boxOf(pi, sh);
          const st = styleOf(pi, sh);
          const common = { position: 'absolute', left: bx.x, top: bx.y, width: bx.w, height: bx.h };
          const on = interactive && sel === sh.i;
          /* ドラッグで移動、右下のつまみで大きさ変更。1920×1080基準の値に戻して保存する */
          const startDrag = (mode) => (e) => {
            if (!interactive) return;
            e.preventDefault(); e.stopPropagation();
            setSel(sh.i);
            drag.current = { mode, i: sh.i, p: pi, sx: e.clientX, sy: e.clientY, box: { ...bx }, scale,
              others: (pg.shapes || []).filter(o => o.i !== sh.i).map(o => boxOf(pi, o)) };
          };
          return (
            <div key={sh.i}
              onMouseDown={interactive ? startDrag('move') : undefined}
              /* クリックが中央キャンバスまでバブリングすると「空クリックで選択解除」が発火し、
                 選択した瞬間にパネルが消える（文字が直せない）。図形上のクリックはここで止める */
              onClick={interactive ? (e) => e.stopPropagation() : undefined}
              style={{ ...common,
                background: sh.fillGrad ? `linear-gradient(${sh.fillGrad[2]}deg, #${sh.fillGrad[0]}, #${sh.fillGrad[1]})` : (sh.fill ? '#' + sh.fill : 'transparent'),
                /* 角丸・行間・縦揃え・字間はPPTXの実値（サーバが抽出）で描く。
                   固定値だとピルの角が足りない／文字が上に張り付く等、実物と見た目がずれる */
                borderRadius: sh.rad || (sh.round ? 10 : 0),
                fontSize: st.size || 26, fontWeight: st.bold ? 700 : 400,
                fontFamily: "'Noto Sans JP', 'Inter', sans-serif",
                letterSpacing: sh.cs || 0,
                color: st.color ? '#' + st.color : '#0b0b2e', textAlign: st.align || 'left',
                lineHeight: sh.lh || (st.size && bx.h < st.size * 1.4 ? 1 : 1.45),
                display: 'flex', flexDirection: 'column',
                justifyContent: sh.valign === 'middle' ? 'center' : (sh.valign === 'bottom' ? 'flex-end' : 'flex-start'),
                /* 枠よりフォントが大きい1行もの（目次80px・章扉282px等）はPowerPointと同じく
                   枠からはみ出して描く。hiddenだと数字の下が刈り取られる */
                overflow: st.size && bx.h < st.size * 1.4 ? 'visible' : 'hidden',
                whiteSpace: 'pre-wrap', wordBreak: 'break-word',
                /* 枠線・つまみは transform 縮小後も見える太さを保つため 1/scale で逆補正する */
                outline: on ? `${2 / scale}px solid #4a5af0` : (interactive ? `${1 / scale}px dashed rgba(91,87,216,.22)` : 'none'),
                cursor: interactive ? 'move' : 'default', userSelect: 'none' }}>
              <div style={sh.textGrad
                ? { width: '100%', background: `linear-gradient(${sh.textGrad[2]}deg, #${sh.textGrad[0]}, #${sh.textGrad[1]})`, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }
                : { width: '100%' }}>{t}</div>
              {on && DECK_HANDLES.map(h => (
                <div key={h.k} onMouseDown={startDrag('r:' + h.k)}
                  style={{ position: 'absolute', left: h.l, top: h.t, right: h.r, bottom: h.b, transform: (h.tf ? h.tf + ' ' : '') + `scale(${1 / scale})`,
                    width: 10, height: 10, background: '#fff', border: '2px solid #4a5af0', borderRadius: 2, cursor: h.c }} />
              ))}
            </div>
          );
        })}
        {interactive && guides && guides.map((g, k) => (
          <div key={'g' + k} style={{ position: 'absolute', background: '#ff3b8d', pointerEvents: 'none',
            ...(g.v !== undefined ? { left: g.v, top: 0, width: 1 / scale, height: '100%' } : { top: g.h, left: 0, height: 1 / scale, width: '100%' }) }} />
        ))}
      </div>
      </div>
    );
  };

  React.useEffect(() => {
    const move = (e) => {
      if (blocked.current) return;
      const g = drag.current;
      if (!g) return;
      const dx = (e.clientX - g.sx) / g.scale, dy = (e.clientY - g.sy) / g.scale;
      const b = g.box;
      let nb;
      if (g.mode === 'move') nb = { x: Math.round(b.x + dx), y: Math.round(b.y + dy), w: b.w, h: b.h };
      else {
        const k = g.mode.slice(2);
        let { x, y, w, h } = b;
        if (k.includes('e')) w = Math.max(40, Math.round(b.w + dx));
        if (k.includes('s')) h = Math.max(24, Math.round(b.h + dy));
        if (k.includes('w')) { const nx = Math.round(b.x + dx); w = Math.max(40, b.w - (nx - b.x)); x = nx; }
        if (k.includes('n')) { const ny = Math.round(b.y + dy); h = Math.max(24, b.h - (ny - b.y)); y = ny; }
        nb = { x, y, w, h };
      }
      /* PowerPointと同じく、他の図形やスライド中央に近づくと吸い付く（8px以内） */
      const gl = [];
      if (g.mode === 'move' && g.others) {
        const SNAP = 10;
        const vx = [0, 960, 1920, ...g.others.flatMap(o => [o.x, o.x + o.w / 2, o.x + o.w])];
        const vy = [0, 540, 1080, ...g.others.flatMap(o => [o.y, o.y + o.h / 2, o.y + o.h])];
        const mx = [nb.x, nb.x + nb.w / 2, nb.x + nb.w];
        const my = [nb.y, nb.y + nb.h / 2, nb.y + nb.h];
        for (let a = 0; a < 3; a++) {
          const hit = vx.find(v => Math.abs(v - mx[a]) <= SNAP);
          if (hit !== undefined) { nb.x = Math.round(hit - (a === 0 ? 0 : a === 1 ? nb.w / 2 : nb.w)); gl.push({ v: hit }); break; }
        }
        for (let a = 0; a < 3; a++) {
          const hit = vy.find(v => Math.abs(v - my[a]) <= SNAP);
          if (hit !== undefined) { nb.y = Math.round(hit - (a === 0 ? 0 : a === 1 ? nb.h / 2 : nb.h)); gl.push({ h: hit }); break; }
        }
      }
      setGuides(gl.length ? gl : null);
      setGeo(x => ({ ...x, [g.p + ':' + g.i]: nb }));
    };
    const up = () => { drag.current = null; setGuides(null); };
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
    return () => { window.removeEventListener('mousemove', move); window.removeEventListener('mouseup', up); };
  }, []);

  /* キーボード操作。文字入力中（textarea/input）は横取りしない */
  React.useEffect(() => {
    const onKey = (e) => {
      if (blocked.current) return;
      const tag = (e.target && e.target.tagName) || '';
      if (tag === 'TEXTAREA' || tag === 'INPUT') return;
      if (sel === null || !deck) return;
      const sh = (deck.pages[page].shapes || []).find(x => x.i === sel);
      if (!sh) return;
      const k = key(page, sel);
      const b = geo[k] || { x: sh.x, y: sh.y, w: sh.w, h: sh.h };
      const step = e.shiftKey ? 10 : 1;
      const nudge = { ArrowLeft: [-step, 0], ArrowRight: [step, 0], ArrowUp: [0, -step], ArrowDown: [0, step] }[e.key];
      if (nudge) { e.preventDefault(); setGeo(x => ({ ...x, [k]: { ...b, x: b.x + nudge[0], y: b.y + nudge[1] } })); return; }
      if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); setOps(x => ({ ...x, [k]: { del: true } })); setSel(null); return; }
      if ((e.metaKey || e.ctrlKey) && (e.key === 'd' || e.key === 'D')) { e.preventDefault(); setOps(x => ({ ...x, [k]: { dup: true } })); }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [sel, page, deck, geo]);

  const pg = deck && deck.pages[page];
  const selShape = pg && (pg.shapes || []).find(x => x.i === sel);

  return (
    <div style={inline
      ? { position: 'relative', height: '100%', display: 'flex', flexDirection: 'column' }
      : { position: 'fixed', inset: 0, background: 'rgba(16,18,28,.62)', zIndex: 300, display: 'flex', flexDirection: 'column' }}
      onClick={inline ? undefined : onClose}>
      <div inert={locked || busy ? "" : undefined} onClick={(e) => e.stopPropagation()} style={{ background: '#f6f7f9', flex: '1 1 auto', display: 'flex', flexDirection: 'column', overflow: 'hidden', ...(inline ? { borderRadius: 12, border: '1px solid #e2e5ea' } : {}) }}>
        {/* ヘッダ */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 16px', background: '#fff', borderBottom: '1px solid #e8eaee', flexWrap: 'wrap' }}>
          <span style={{ fontSize: 14, fontWeight: 700, color: '#1c1f26' }}>{t('de.title')}</span>
          {deck && <span style={{ fontSize: 11.5, color: '#8b919b' }}>{deck.name}{t('de.pages', { n: deck.pages.length })}</span>}
          {(() => { const pd = caseData.proposalDesign || {};   // 作成・最終編集スタンプ
            return (pd.createdBy || pd.editBy) ? (
              <span style={{ fontSize: 11, color: '#a8aeb8' }}>
                {pd.createdBy ? `${t('de.metaCreated')}：${pd.createdBy}・${String(pd.createdAt || '').slice(0, 16)}` : ''}
                {pd.editBy ? `　${t('de.metaEdited')}：${pd.editBy}・${String(pd.at || '').slice(0, 16)}` : ''}
              </span>
            ) : null; })()}
          <span style={{ marginLeft: 'auto', display: 'flex', gap: 8, alignItems: 'center' }}>
            <button onClick={addText} disabled={busy || !deck}
              style={{ fontSize: 12.5, fontWeight: 600, padding: '7px 12px', borderRadius: 8, border: '1px solid #d9d5f2', background: '#fff', color: '#4a5af0', cursor: 'pointer' }}>＋ {t('de.addText')}</button>
            <button onClick={() => fileRef.current && fileRef.current.click()} disabled={busy || !deck} title={t('de.pasteHint')}
              style={{ fontSize: 12.5, fontWeight: 600, padding: '7px 12px', borderRadius: 8, border: '1px solid #d9d5f2', background: '#fff', color: '#4a5af0', cursor: 'pointer' }}>＋ {t('de.addImage')}</button>
            <input ref={fileRef} type="file" accept="image/*" style={{ display: 'none' }} onChange={pickImage} />
            <span style={{ fontSize: 11.5, color: '#8b919b' }}>{t('de.zoom')}</span>
            <input type="range" min={0.25} max={0.8} step={0.01} value={zoom} onChange={e => setZoom(Number(e.target.value))} style={{ width: 110 }} />
            <button onClick={() => save()} disabled={!dirty || busy}
              style={{ fontSize: 12.5, fontWeight: 600, padding: '7px 16px', borderRadius: 8, border: 'none', cursor: dirty ? 'pointer' : 'default',
                background: dirty ? '#4a5af0' : '#dcdde3', color: '#fff' }}>{busy ? t('de.saving') : (dirty ? t('de.saveN', { n: dirty }) : t('de.save'))}</button>
            {!inline && <button onClick={onClose} style={{ fontSize: 12.5, fontWeight: 600, padding: '7px 14px', borderRadius: 8, border: '1px solid #e2e5ea', background: '#fff', color: '#5a616c', cursor: 'pointer' }}>{t('de.close')}</button>}
          </span>
        </div>

        {err && <div style={{ padding: 16, fontSize: 13, color: '#b91c1c' }}>{err}</div>}
        {!deck && !err && <div style={{ padding: 24, fontSize: 13, color: '#8b919b' }}>{t('de.loading')}</div>}

        {deck && (
          <div style={{ flex: '1 1 auto', display: 'flex', overflow: 'hidden' }}>
            {/* 左：ページ一覧 */}
            <div style={{ width: 172, flex: '0 0 auto', overflowY: 'auto', padding: 10, background: '#eef0f3', borderRight: '1px solid #e2e5ea' }}>
              {deck.pages.map((p, i) => (
                <div key={i} onClick={() => { setPage(i); setSel(null); }}
                  style={{ marginBottom: 8, cursor: 'pointer', border: '2px solid ' + (page === i ? '#4a5af0' : 'transparent'), borderRadius: 8, padding: 2 }}>
                  <div style={{ fontSize: 10, color: '#8b919b', marginBottom: 2 }}>{i + 1}</div>
                  {renderSlide(p, i, 0.074, false)}
                </div>
              ))}
            </div>

            {/* 中央：選択中のスライド */}
            <div ref={centerRef} style={{ flex: '1 1 auto', overflow: 'auto', padding: 20, display: 'flex', justifyContent: 'center', alignItems: 'flex-start' }}
              onClick={() => setSel(null)}>
              {pg && renderSlide(pg, page, zoom, true)}
            </div>

            {/* 右：選択中の文字を編集 */}
            <div style={{ width: 300, flex: '0 0 auto', overflowY: 'auto', padding: 14, background: '#fff', borderLeft: '1px solid #e8eaee' }}>
              {!selShape && <div style={{ fontSize: 12, color: '#a8aeb8', lineHeight: 1.7 }}>{t('de.hint')}<br /><br />{t('de.hint2')}</div>}
              {selShape && (
                <>
                  <div style={{ fontSize: 11.5, fontWeight: 700, color: '#3b414b', marginBottom: 6 }}>
                    {t('de.slot', { p: page + 1, i: selShape.i })}
                    <span style={{ fontWeight: 400, color: '#a8aeb8' }}>（{Math.round(selShape.size || 0)}px・{selShape.w}×{selShape.h}）</span>
                  </div>
                  <textarea value={textOf(page, selShape)} rows={10}
                    onChange={e => setEdits(x => ({ ...x, [key(page, selShape.i)]: e.target.value }))}
                    style={{ width: '100%', fontSize: 12.5, lineHeight: 1.7, padding: 9, borderRadius: 8, border: '1px solid #e4e0f5', resize: 'vertical', fontFamily: 'inherit' }} />
                  <div style={{ fontSize: 11, color: '#a8aeb8', marginTop: 6, lineHeight: 1.6 }}>
                    {t('de.origChars', { n: (selShape.text || '').length })}
                  </div>
                  {/* 位置・大きさ（ドラッグでも変えられる） */}
                  <div style={{ marginTop: 10, fontSize: 11.5, fontWeight: 700, color: '#3b414b' }}>{t('de.geo')}</div>
                  <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: 5 }}>
                    {[['x', 'X'], ['y', 'Y'], ['w', t('de.w')], ['h', t('de.h')]].map(([k, l]) => (
                      <label key={k} style={{ fontSize: 11, color: '#8b919b', display: 'flex', alignItems: 'center', gap: 3 }}>{l}
                        <input type="number" value={boxOf(page, selShape)[k]}
                          onChange={e => setGeo(x => ({ ...x, [key(page, selShape.i)]: { ...boxOf(page, selShape), [k]: Number(e.target.value) || 0 } }))}
                          style={{ width: 62, fontSize: 11.5, padding: '3px 5px', borderRadius: 6, border: '1px solid #e4e0f5' }} />
                      </label>
                    ))}
                  </div>
                  {/* 見た目：文字サイズ・色・太字・揃え（PPTXへ書き戻される） */}
                  <div style={{ marginTop: 10, fontSize: 11.5, fontWeight: 700, color: '#3b414b' }}>{t('de.style')}</div>
                  <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', marginTop: 5 }}>
                    <label style={{ fontSize: 11, color: '#8b919b', display: 'flex', alignItems: 'center', gap: 3 }}>{t('de.fontSize')}
                      <input type="number" min={8} max={300} value={Math.round(styleOf(page, selShape).size)}
                        onChange={e => setStyle(page, selShape.i, { size: Math.max(8, Math.min(300, Number(e.target.value) || selShape.size || 26)) })}
                        style={{ width: 56, fontSize: 11.5, padding: '3px 5px', borderRadius: 6, border: '1px solid #e4e0f5' }} />px
                    </label>
                    <label style={{ fontSize: 11, color: '#8b919b', display: 'flex', alignItems: 'center', gap: 3 }}>{t('de.fontColor')}
                      <input type="color" value={'#' + (styleOf(page, selShape).color || '0b0b2e')}
                        onChange={e => setStyle(page, selShape.i, { color: e.target.value.replace('#', '') })}
                        style={{ width: 30, height: 24, padding: 0, border: '1px solid #e4e0f5', borderRadius: 5, background: '#fff', cursor: 'pointer' }} />
                    </label>
                    <label style={{ fontSize: 11, color: '#8b919b', display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
                      <input type="checkbox" checked={styleOf(page, selShape).bold}
                        onChange={e => setStyle(page, selShape.i, { bold: e.target.checked })} />{t('de.bold')}
                    </label>
                  </div>
                  <div style={{ display: 'flex', gap: 4, marginTop: 6 }}>
                    {[['left', t('de.alignL')], ['center', t('de.alignC')], ['right', t('de.alignR')]].map(([a, l]) => (
                      <button key={a} onClick={() => setStyle(page, selShape.i, { align: a })}
                        style={{ fontSize: 11, fontWeight: 600, padding: '4px 10px', borderRadius: 6, cursor: 'pointer',
                          border: '1px solid ' + (styleOf(page, selShape).align === a ? '#4a5af0' : '#e2e5ea'),
                          background: styleOf(page, selShape).align === a ? '#eef0fd' : '#fff',
                          color: styleOf(page, selShape).align === a ? '#4a5af0' : '#5a616c' }}>{l}</button>
                    ))}
                  </div>
                  <div style={{ display: 'flex', gap: 6, marginTop: 8, flexWrap: 'wrap' }}>
                    <button onClick={() => { setOps(x => ({ ...x, [key(page, selShape.i)]: { dup: true } })); }}
                      style={{ fontSize: 11.5, fontWeight: 600, padding: '5px 11px', borderRadius: 7, border: '1px solid #d9d5f2', background: '#fff', color: '#4a5af0', cursor: 'pointer' }}>{t('de.dup')}</button>
                    <button onClick={() => { setOps(x => ({ ...x, [key(page, selShape.i)]: { del: true } })); setSel(null); }}
                      style={{ fontSize: 11.5, fontWeight: 600, padding: '5px 11px', borderRadius: 7, border: '1px solid #f3d6d6', background: '#fff', color: '#b91c1c', cursor: 'pointer' }}>{t('de.del')}</button>
                  </div>
                  <div style={{ fontSize: 11, color: '#a8aeb8', marginTop: 6, lineHeight: 1.7 }}>
                    {t('de.ops1')}<br />{t('de.ops2')}
                  </div>
                  {(key(page, selShape.i) in edits || key(page, selShape.i) in geo || key(page, selShape.i) in styles) && (
                    <button onClick={() => { setEdits(x => { const y = { ...x }; delete y[key(page, selShape.i)]; return y; }); setGeo(x => { const y = { ...x }; delete y[key(page, selShape.i)]; return y; }); setStyles(x => { const y = { ...x }; delete y[key(page, selShape.i)]; return y; }); }}
                      style={{ marginTop: 8, fontSize: 11.5, fontWeight: 600, padding: '5px 11px', borderRadius: 7, border: '1px solid #e2e5ea', background: '#fff', color: '#5a616c', cursor: 'pointer' }}>{t('de.resetSlot')}</button>
                  )}
                </>
              )}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

Object.assign(window, { DeckEditor });
