/* ============================================================
   ナレッジ（知識庫）— 2庫を管理し、AI（推薦・エントリ提案信）に RAG で供給
   ・proposal = エントリ提案信 生成用
   ・recommend = アポ推薦（成件機率判定）用
   入力：テキスト / URL（サーバーが本文抽出）/ ファイル（.txt 等＝そのまま、PDF＝pdf.js で抽出）
   ============================================================ */

/* PDF からテキスト抽出（pdf.js を CDN から遅延ロード。無建置の方針に合わせる） */
async function loadPdfJs() {
  if (window.pdfjsLib) return window.pdfjsLib;
  await new Promise((resolve, reject) => {
    const s = document.createElement('script');
    s.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
    s.onload = resolve; s.onerror = () => reject(new Error(t('kb.err.pdfLibLoad')));
    document.head.appendChild(s);
  });
  if (!window.pdfjsLib) throw new Error(t('kb.err.pdfLibInit'));
  window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
  return window.pdfjsLib;
}

async function extractFileText(file) {
  const name = (file.name || '').toLowerCase();
  const isPdf = file.type === 'application/pdf' || name.endsWith('.pdf');
  if (isPdf) {
    const pdfjsLib = await loadPdfJs();
    const buf = await file.arrayBuffer();
    const pdf = await pdfjsLib.getDocument({ data: buf }).promise;
    let text = '';
    for (let p = 1; p <= pdf.numPages; p++) {
      const page = await pdf.getPage(p);
      const tc = await page.getTextContent();
      text += tc.items.map(it => it.str).join(' ') + '\n';
    }
    return text.trim();
  }
  // テキスト系（.txt/.md/.csv/.json 等）はそのまま読む
  return (await file.text()).trim();
}

/* 2グループ：[素材]＝AI生成の入力（RAG）／[社内ナレッジ]＝人が読む・案件に紐づけ可 */
/* 成果物ごとの4グループ。各グループは〈材料＝AIが読む資料〉→〈設定＝AIの作り方〉の順。
   設定タブ（proposalSet / quoteSet）はナレッジ件数を持たないので、件数バッジは出さない。 */
const KB_TABS = [
  // ── 会社資料（提案書・見積書・社内FAQのAIが共通で参照する自社の資産）──
  { key: 'achievement', group: '会社資料', get label(){return window.t("label.extra61");}, desc: '導入実績・ポートフォリオ。業種・課題・技術でタグ付けされ、提案書AIが同業の実績を自動で引きます（公式サイトの実績は月1回自動取込）' },
  { key: 'company', group: '会社資料', label: () => '会社情報', desc: () => '公式サイト（alion.jp）から自動生成した自社情報。提案書・見積もり・商談提案・社内FAQのAIが常に参照します（見積書・社内ナレッジ側からも使われます）' },
  // ── 提案書 ──
  { key: 'proposal', group: '提案書', label: () => t('kb.tab.proposal'), desc: () => t('kb.tab.proposal.desc') },
  { key: 'recommend', group: '提案書', label: () => t('kb.tab.recommend'), desc: () => t('kb.tab.recommend.desc') },
  { key: 'proposalSet', group: '提案書', label: () => '提案書の設定', desc: () => '提案書の作り方をここで決めます。①使う社内スキル（.skill）②スライドの骨格＝テンプレート ③AI分析の方針。過去提案書の学習状況もここに出ます', set: true },
  // ── 見積書 ──
  { key: 'quote', group: '見積書', label: () => t('kb.tab.quote'), desc: () => '上＝アップした見積書の素材（AIのお手本）、下＝見積書から自動抽出した機能ごとの単価カタログ（機能マスタ）' },
  { key: 'quoteSet', group: '見積書', label: () => '見積書の設定', desc: () => '見積もりAIがどんな観点で分析・分解するかの方針。区切り記号・単位・単価帯などの自社ルールもここに書けます', set: true },
  // ── メール ──
  { key: 'mailgen', group: 'メール', get label(){return window.t("label.extra62");}, desc: 'あなた専用。署名・名乗り・口調・よく使う言い回しを登録すると、AIメール生成であなたの分だけ参照されます（他の人の画面・他の人のメール生成には一切出ません）' },
  // ── 社内ナレッジ ──
  { key: 'howto', group: '社内ナレッジ', get label(){return window.t("label.extra63");}, desc: '業務の手順・マニュアル。人が読む社内ドキュメントです（メンバーも追加できます）' },
  { key: 'faq', group: '社内ナレッジ', label: 'FAQ', desc: 'よくある質問と回答。営業・対応のナレッジ共有に' },
  { key: 'case', group: '社内ナレッジ', get label(){return window.t("label.extra64");}, desc: '過去案件の事例・成功/失敗の振り返り。案件に紐づけて残せます' },
  { key: 'minutes', group: '社内ナレッジ', get label(){return window.t("label.extra65");}, desc: '会議・打合せの議事録。案件に紐づけて残せます' },
];
const KB_GROUPS = ['会社資料', '提案書', '見積書', 'メール', '社内ナレッジ'];
const SETTINGS_KBS = ['proposalSet', 'quoteSet'];   // ナレッジ一覧ではなく設定カードを出すタブ
const SOURCE_KBS = ['proposal', 'recommend', 'quote', 'company'];      // AI素材（追加=管理者）
const INTERNAL_KBS = ['howto', 'case', 'faq', 'minutes', 'achievement'];    // 社内ナレッジ（追加=メンバーも可・案件紐づけ可）
const PERSONAL_KBS = ['mailgen'];   // 個人専用KB（本人のみ閲覧・本人のメール生成にのみ反映。bootstrapで他人分はそもそもクライアントに来ない）
const isInternalKB = (k) => INTERNAL_KBS.includes(k);
const isPersonalKB = (k) => PERSONAL_KBS.includes(k);
const kbLabel = (v) => (typeof v === 'function' ? v() : v);
const KB_ICONS = { proposal: 'mail', recommend: 'spark', quote: 'attach', company: 'customers', featureMaster: 'chart', proposalSet: 'settings', quoteSet: 'settings', howto: 'edit', case: 'cases', faq: 'help', minutes: 'inbox', achievement: 'chart', mailgen: 'mail' };

/* AI分析の方針テンプレート（チップで選ぶとテキスト欄に下書きが入る。その後編集して保存） */
const PROPOSAL_GUIDANCE_TEMPLATES = [
  { name: '標準（バランス）', text: '・事実に忠実に。読み取れないことは創作せず「（記録からは不明）」とする\n・方向性は理由つきで明確に（新規／既存へAI追加／スモールスタート など）\n・威嚇感は「動かない代償・経営リスク」を案件文脈で具体的に\n・期待感は単なる効率化で終わらせず、その先の変化まで描く\n・差別化は「なぜ自社か」＋顧客がまだ気づいていない価値を必ず入れる\n・価値は相手別に：社長＝ROI・回収期間・経営指標／主管＝KPI・可視化・報告時間／現場＝操作の簡単さ・残業/ミス削減' },
  { name: 'コスト訴求（中小企業）', text: '・中小企業向けにコスト訴求を強める。初期費用とランニングを明確に\n・ROIと投資回収期間を具体的な数値で（社長向けを最優先）\n・スモールスタート・段階導入で初期負担を抑える提案を必ず入れる\n・差別化は「低コストで始められる」「補助金活用」も視野に\n・現場向けは「今のやり方を大きく変えない」安心感を重視' },
  { name: '差別化・競合対抗', text: '・競合を強く意識した差別化を必ず入れる（他社にない強み・実績）\n・顧客がまだ気づいていない価値・将来の可能性を前面に\n・威嚇感では「競合の先行導入」「現状維持の機会損失」を具体的に\n・社長向けは市場競争力・中長期の優位性を強調\n・決め手になりそうな自社の得意領域を明確化' },
  { name: '現場価値・使いやすさ', text: '・現場社員の価値を最優先（操作が簡単・残業が減る・ミスが減る）\n・「入力が楽になる」「反復作業の自動化」を具体的に\n・主管向けは報告作成時間の短縮・管理負担の軽減\n・導入時の現場の抵抗・教育コストへの配慮を懸念点で拾う\n・デモは現場が毎日使う画面を主役に' },
];
const QUOTE_GUIDANCE_TEMPLATES = [
  { name: '標準', text: '・目的は機能一覧づくり。価格は作らない（単価は空欄＝後で人が入力）\n・読み取れないことは「（記録からは不明）」とし、勝手な金額・項目は作らない\n・自社の過去案件の機能一覧・プラン名・規模感を踏襲（同カテゴリを優先参照）\n・各機能に：名称／何をするか／プラン名／規模S・M・L／MUST or WANT\n・costDrivers・リスク・フェーズ分け・予算感・交渉論点も整理\n・プロトタイプ添付があれば見える機能も漏れなく項目化' },
  { name: 'フル機能（漏れなく）', text: '・機能を漏れなく洗い出す（商談記録・プロトタイプ・過去案件から網羅的に）\n・非機能要件（性能・セキュリティ・保守・運用・教育）も項目化\n・外部連携・データ移行・管理画面・帳票なども個別項目で出す\n・MUSTもWANTもまず全部出してから整理（取りこぼし防止）\n・規模感S・M・Lを必ず付け、Lは分割の余地も示す' },
  { name: 'スモールスタート', text: '・初期費用を抑える観点でMUST/WANTを厳しく線引き\n・第1フェーズ（最小構成）と第2フェーズ以降を明確に分ける\n・WANT・将来拡張はオプション項目として別出し\n・保守は年間契約として必ず別項目で出す\n・段階導入で初期負担を下げる提案（phasing）を必ず入れる' },
  { name: '保守・運用重視', text: '・保守・運用を必ず独立項目で（年間契約・月額）\n・データ移行／外部連携はcostDriversに必ず挙げる\n・運用フェーズの工数（監視・問い合わせ対応・更新）も項目化\n・初期構築と運用を分けて見せる\n・人月単価は自社標準を前提に項目を分ける' },
];
/* テンプレ選択チップ列。選ぶと onPick(text) */
function GuidanceTemplates({ templates, onPick, disabled }) {
  if (disabled) return null;
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', marginBottom: 10 }}>
      <span style={{ fontSize: 11.5, color: '#9aa1ab', fontWeight: 600 }}>{window.t("extra.knowledge.templatePrefix")}</span>
      {templates.map(tp => (
        <button key={tp.name} onClick={() => onPick(tp.text)}
          style={{ border: '1px solid #e2e5ea', background: '#fff', color: '#4a5af0', fontSize: 12, fontWeight: 600, borderRadius: 999, padding: '4px 11px', cursor: 'pointer', fontFamily: 'inherit' }}>{tp.name}</button>
      ))}
    </div>
  );
}

/* AI分析設定：提案書・見積もりAI分析の「分析方針」を編集（masters.config）。テンプレ選択可。サーバーが分析時に最優先で反映 */
const PROTOTYPE_GUIDANCE_TEMPLATES = [
  { name: '標準（網羅重視）', text: '・本番システムの全機能を漏れなく洗い出す（画面・登録/編集/検索・一覧/詳細・管理・通知・権限・帳票・外部連携・ログ）\n・要件定義は機能要件＋非機能要件（性能・セキュリティ・権限・可用性・保守・法令）まで\n・記録に無い部分も一般的なベストプラクティスで補い、推奨である旨を添える\n・附加価値と、顧客が言及していない論点・リスクを必ず出す' },
  { name: 'MVP・スモールスタート', text: '・まず最小構成(MVP)で動く範囲を必須(must)に、それ以外は任意(want)に振り分ける\n・第1フェーズ／第2フェーズ以降を意識して機能を分類\n・初期コスト・初期構築の手離れを重視\n・将来拡張の余地を hiddenIssues / addedValue に明記' },
  { name: '業務システム寄り', text: '・権限/ロール・操作ログ・監査・帳票/出力・データ整合性を厚めに\n・既存システム連携・データ移行・マスタ管理を必ず項目化\n・運用フェーズ（監視・問い合わせ対応・バックアップ）も要件に\n・社内ユーザーの業務フローに沿った画面・機能を具体的に' },
  { name: '顧客体験(UX)重視', text: '・エンドユーザー目線で使いやすさ・導線・モバイル対応を重視\n・離脱を防ぐUX、入力の手間削減、わかりやすい通知を具体的に\n・附加価値はパーソナライズ・自動化・体験向上を中心に\n・アクセシビリティ・多言語など見落としがちな点を hiddenIssues に' },
];
const SHODAN_GUIDANCE_TEMPLATES = [
  { name: '標準（バランス）', text: '・これまでに刺さった点・懸念・決め手を踏まえ、次の一手に落とす\n・ゴールは「この商談で何を獲るか」を1つに絞る（次フェーズ合意／決裁者同席 など）\n・アジェンダは時間配分つきで現実的に。見せる順番は相手の関心が高い順に\n・想定問答は相手が必ず聞く懸念（費用・決裁・現場）を先回り\n・クロージングは具体的な依頼（日程・同席・合意）まで言い切る' },
  { name: 'クロージング重視', text: '・この商談で「次の合意」を必ず取りにいく前提で組む\n・クロージングの一手は具体的に（次回日程・決裁者同席・PoC着手の口頭合意）\n・想定問答は反論処理を厚めに（高い／決裁が通らない／今じゃない）\n・次のアクションは誰が・何を・いつまでにを明確に\n・価格は価値合意の後。先に出しすぎない' },
  { name: '関係構築・ヒアリング（初期）', text: '・まだ要件が固まっていない初期商談向け。聞く設計を重視\n・アジェンダはヒアリング項目（課題・体制・予算・決裁プロセス・期限）中心に\n・一番刺さる訴求は売り込みより「一緒に整理しましょう」の姿勢\n・次のアクションは宿題の握り（情報提供・次回までの確認事項）\n・注意点で踏み込みすぎ・即見積もりの危険を拾う' },
  { name: '決裁者攻略', text: '・決裁者（社長/役員）を動かすことを最優先に組む\n・一番刺さる訴求はROI・経営インパクト・競争優位で（現場メリットは添える程度）\n・見せる順番は経営指標→投資対効果→リスクの順\n・クロージングは「決裁者の同席/承認の場」を取りにいく\n・想定問答は投資判断の論点（回収期間・他社比較・失敗リスク）に絞る' },
];
/* 提案書スキル（.skill）の差し替えアップロード：スキルは頻繁に更新されるため、
   最新の .skill(zip) をここから上げるだけで「提案書作成」「デザインPPTX」が即最新版で動く。
   保存先はサーバのVolume＝デプロイでも消えない。管理者のみ。 */
/* 提案書デザイン — 版型（スライドの種類）を選び、その見た目をその場で調整する1画面。
   以前は「全体の配色を決めるカード」と「版型を並べるカード」が別々で、版型ごとの調整もできなかった。
   全体設定を土台に、版型ごとの上書き（色・余白・文字サイズ・列数）を持てるようにしている。 */
function DeckDesignCard() {
  const { showToast, can } = useStore();
  const editable = can('manageKnowledge');
  const [d, setD] = React.useState(null);
  const [def, setDef] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [dirty, setDirty] = React.useState(false);
  const [kind, setKind] = React.useState('cover');   // 選択中の版型
  const [scope, setScope] = React.useState('all');   // all=全体 / one=この版型
  const wrapRef = React.useRef(null);
  const [pw, setPw] = React.useState(900);
  /* 幅は「入れ物の実寸」に必ず収める。flexアイテムに minWidth:0 を入れないと、
     測った幅を子に指定→入れ物が広がる→また測る、で右にはみ出していく。 */
  /* ★deps=[] だと初回レンダー（d 未読込で return null）の時点で wrapRef が null のまま
     エフェクトが空振りし、Observer が永遠に付かず pw=900 固定→外枠からはみ出していた。
     d が入って実DOMがマウントされた後に必ず計測・監視を張り直す */
  React.useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    const fit = () => {
      const w = Math.floor(el.getBoundingClientRect().width);
      if (w > 0) setPw(Math.max(320, Math.min(1400, w)));
    };
    fit();
    const raf = requestAnimationFrame(fit);                  // フォント・レイアウト確定後にもう一度
    const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(fit) : null;
    if (ro) ro.observe(el);
    window.addEventListener('resize', fit);
    return () => { cancelAnimationFrame(raf); if (ro) ro.disconnect(); window.removeEventListener('resize', fit); };
  }, [!!d]);
  React.useEffect(() => { API.deckDesign().then(r => { setD({ layouts: {}, ...r.design }); setDef(r.defaults); }).catch(() => {}); }, []);
  if (!d) return null;

  const ov = (d.layouts || {})[kind] || {};                 // この版型の上書き
  const eff = { ...d, ...ov };                               // 実際に効いている値
  const setAll = (k) => (v) => { setD(x => ({ ...x, [k]: v })); setDirty(true); };
  const setOne = (k) => (v) => {
    setD(x => ({ ...x, layouts: { ...(x.layouts || {}), [kind]: { ...((x.layouts || {})[kind] || {}), [k]: v } } }));
    setDirty(true);
  };
  const clearOne = (k) => {
    setD(x => { const L = { ...((x.layouts || {})[kind] || {}) }; delete L[k];
      const all = { ...(x.layouts || {}) }; if (Object.keys(L).length) all[kind] = L; else delete all[kind];
      return { ...x, layouts: all }; });
    setDirty(true);
  };
  const set = (k) => (scope === 'all' ? setAll(k) : setOne(k));
  const cur = (k) => (scope === 'all' ? d[k] : (ov[k] !== undefined ? ov[k] : d[k]));
  const overridden = (k) => scope === 'one' && ov[k] !== undefined;
  const save = async () => {
    setBusy(true);
    try { const r = await API.deckDesignSave(d); setD({ layouts: {}, ...r.design }); setDirty(false); showToast(t('dz.saved')); }
    catch (e) { showToast((e && e.message) || t('dz.saveFail'), 'x'); }
    setBusy(false);
  };
  const row = { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 7 };
  const lab = (k, name) => (
    <span style={{ fontSize: 11.5, color: overridden(k) ? '#4a5af0' : '#5a616c', width: 96, flex: '0 0 auto', fontWeight: overridden(k) ? 700 : 400 }}>
      {name}{overridden(k) ? ' •' : ''}
    </span>
  );
  const colorRow = (k, name) => (
    <div style={row} key={k}>
      {lab(k, name)}
      <input type="color" value={'#' + String(cur(k) || '').replace('#', '')} disabled={!editable}
        onChange={e => set(k)(e.target.value.replace('#', '').toUpperCase())}
        style={{ width: 34, height: 24, padding: 0, border: '1px solid #e4e0f5', borderRadius: 6, background: '#fff', cursor: editable ? 'pointer' : 'default' }} />
      <input value={cur(k) || ''} disabled={!editable} onChange={e => set(k)(e.target.value.replace('#', '').toUpperCase())}
        style={{ width: 84, fontSize: 11.5, padding: '4px 7px', borderRadius: 6, border: '1px solid #e4e0f5', fontFamily: 'monospace' }} />
      {overridden(k) && <button onClick={() => clearOne(k)} title={t('dz.revertTitle')} style={{ fontSize: 10.5, padding: '2px 6px', borderRadius: 6, border: '1px solid #e2e5ea', background: '#fff', color: '#8b919b', cursor: 'pointer' }}>{t('dz.revert')}</button>}
    </div>
  );
  const numRow = (k, name, lo, hi, unit) => (
    <div style={row} key={k}>
      {lab(k, name)}
      <input type="range" min={lo} max={hi} value={cur(k)} disabled={!editable} onChange={e => set(k)(Number(e.target.value))} style={{ flex: '1 1 auto', maxWidth: 130 }} />
      <span style={{ fontSize: 11.5, color: '#3b414b', width: 46 }}>{cur(k)}{unit}</span>
      {overridden(k) && <button onClick={() => clearOne(k)} title={t('dz.revertTitle')} style={{ fontSize: 10.5, padding: '2px 6px', borderRadius: 6, border: '1px solid #e2e5ea', background: '#fff', color: '#8b919b', cursor: 'pointer' }}>{t('dz.revert')}</button>}
    </div>
  );
  const chip = (on) => ({ fontSize: 11.5, fontWeight: 600, padding: '5px 12px', borderRadius: 999, cursor: 'pointer', fontFamily: 'inherit',
    border: '1px solid ' + (on ? '#4a5af0' : '#e2e5ea'), background: on ? '#4a5af0' : '#fff', color: on ? '#fff' : '#3b414b' });
  const gridKinds = ['cards', 'issues', 'featuregrid'];
  return (
    <div style={{ background: '#fff', border: '1px solid #ece9f8', borderRadius: 12, padding: 16 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
        <span style={{ fontSize: 14, fontWeight: 700, color: '#1c1f26' }}>{t('dz.title')}</span>
        <span style={{ fontSize: 11.5, color: '#8b919b' }}>{t('dz.sub')}</span>
        <span style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
          {editable && def && <button onClick={() => { setD({ layouts: {}, ...def }); setDirty(true); }} style={{ fontSize: 12, fontWeight: 600, padding: '6px 12px', borderRadius: 8, border: '1px solid #e2e5ea', background: '#fff', color: '#5a616c', cursor: 'pointer' }}>{t('dz.reset')}</button>}
          {editable && <button onClick={save} disabled={!dirty || busy} style={{ fontSize: 12, fontWeight: 600, padding: '6px 14px', borderRadius: 8, border: 'none', background: dirty ? '#4a5af0' : '#dcdde3', color: '#fff', cursor: dirty ? 'pointer' : 'default' }}>{busy ? t('dz.saving') : t('dz.save')}</button>}
        </span>
      </div>

      {/* 版型ギャラリー：クリックで選択 */}
      <div style={{ display: 'flex', gap: 9, flexWrap: 'wrap', marginTop: 12, maxWidth: '100%' }}>
        {DZ_LAYOUTS.map(([k, l]) => (
          <div key={k} onClick={() => setKind(k)} style={{ cursor: 'pointer' }}>
            <div style={{ border: '2px solid ' + (kind === k ? '#4a5af0' : 'transparent'), borderRadius: 7, padding: 1 }}>
              <LayoutThumb kind={k} d={{ ...d, ...((d.layouts || {})[k] || {}) }} w={134} />
            </div>
            <div style={{ fontSize: 10.5, color: kind === k ? '#4a5af0' : '#7b828d', fontWeight: kind === k ? 700 : 500, textAlign: 'center', marginTop: 2 }}>
              {t('dz.k.' + k)}{(d.layouts || {})[k] ? ' •' : ''}
            </div>
          </div>
        ))}
      </div>

      <div style={{ display: 'flex', gap: 20, marginTop: 16, flexWrap: 'wrap' }}>
        {/* 設定 */}
        <div style={{ flex: '0 0 300px' }}>
          <div style={{ display: 'flex', gap: 6, marginBottom: 10 }}>
            <button style={chip(scope === 'all')} onClick={() => setScope('all')}>{t('dz.scopeAll')}</button>
            <button style={chip(scope === 'one')} onClick={() => setScope('one')}>{t('dz.scopeOne')}（{t('dz.k.' + kind)}）</button>
          </div>
          {colorRow('primary', t('dz.c.primary'))}
          {colorRow('tint', t('dz.c.tint'))}
          {colorRow('primaryL', t('dz.c.primaryL'))}
          {colorRow('ink', t('dz.c.ink'))}
          {colorRow('muted', t('dz.c.muted'))}
          {colorRow('cyan', t('dz.c.cyan'))}
          {colorRow('warn', t('dz.c.warn'))}
          {colorRow('tint3', t('dz.c.tint3'))}
          {scope === 'all' && colorRow('dark', t('dz.c.dark'))}
          {scope === 'all' && colorRow('accent', t('dz.c.accent'))}
          {scope === 'all' && colorRow('grayL', t('dz.c.grayL'))}
          {scope === 'all' && colorRow('grayM', t('dz.c.grayM'))}
          {scope === 'all' && colorRow('blueL2', t('dz.c.blueL2'))}
          <div style={{ height: 6 }} />
          {numRow('margin', t('dz.n.margin'), 24, 140, 'px')}
          {numRow('titleSize', t('dz.n.titleSize'), 32, 96, 'px')}
          {numRow('bodySize', t('dz.n.bodySize'), 16, 40, 'px')}
          {scope === 'one' && gridKinds.includes(kind) && numRow('cols', t('dz.n.cols'), 1, 4, '')}
          {scope === 'all' && (<>
            <div style={row}><span style={{ fontSize: 11.5, color: '#5a616c', width: 96 }}>{t('dz.f.ja')}</span>
              <select value={d.fontJa} disabled={!editable} onChange={e => setAll('fontJa')(e.target.value)} style={{ fontSize: 11.5, padding: '4px 7px', borderRadius: 6, border: '1px solid #e4e0f5' }}>
                {['Noto Sans JP Medium', 'Noto Sans JP', 'Yu Gothic', 'Meiryo', 'Hiragino Sans'].map(f => <option key={f}>{f}</option>)}
              </select></div>
            <div style={row}><span style={{ fontSize: 11.5, color: '#5a616c', width: 96 }}>{t('dz.f.jaBold')}</span>
              <select value={d.fontJaBold || ''} disabled={!editable} onChange={e => setAll('fontJaBold')(e.target.value)} style={{ fontSize: 11.5, padding: '4px 7px', borderRadius: 6, border: '1px solid #e4e0f5' }}>
                {['Noto Sans JP Bold', 'Noto Sans JP', 'Yu Gothic Bold', 'Hiragino Sans W6'].map(f => <option key={f}>{f}</option>)}
              </select></div>
            <div style={row}><span style={{ fontSize: 11.5, color: '#5a616c', width: 96 }}>{t('dz.f.en')}</span>
              <select value={d.fontEn} disabled={!editable} onChange={e => setAll('fontEn')(e.target.value)} style={{ fontSize: 11.5, padding: '4px 7px', borderRadius: 6, border: '1px solid #e4e0f5' }}>
                {['Inter Bold', 'Inter', 'Arial', 'Helvetica', 'Roboto'].map(f => <option key={f}>{f}</option>)}
              </select></div>
            <div style={row}><span style={{ fontSize: 11.5, color: '#5a616c', width: 96 }}>{t('dz.bg.cover')}</span>
              <select value={d.coverBg} disabled={!editable} onChange={e => setAll('coverBg')(e.target.value)} style={{ fontSize: 11.5, padding: '4px 7px', borderRadius: 6, border: '1px solid #e4e0f5' }}>
                <option value="grad">{t('dz.bg.grad')}</option><option value="solid">{t('dz.bg.solid')}</option></select></div>
            <div style={row}><span style={{ fontSize: 11.5, color: '#5a616c', width: 96 }}>{t('dz.bg.section')}</span>
              <select value={d.sectionBg} disabled={!editable} onChange={e => setAll('sectionBg')(e.target.value)} style={{ fontSize: 11.5, padding: '4px 7px', borderRadius: 6, border: '1px solid #e4e0f5' }}>
                <option value="dark">{t('dz.bg.dark')}</option><option value="primary">{t('dz.bg.primary')}</option></select></div>
          </>)}
          {scope === 'one' && (
            <div style={{ fontSize: 11, color: '#8b919b', marginTop: 8, lineHeight: 1.7 }}>
              {t('dz.oneNote')}
            </div>
          )}
        </div>
        {/* プレビュー */}
        <div ref={wrapRef} style={{ flex: '1 1 420px', minWidth: 0, overflow: 'hidden' }}>
          <div style={{ maxWidth: '100%', overflow: 'hidden' }}><LayoutThumb kind={kind} d={eff} w={pw} /></div>
          <div style={{ fontSize: 11, color: '#a8aeb8', marginTop: 6 }}>
            {t('dz.preview', { pct: Math.round(pw / 1920 * 100) })}
          </div>
        </div>
      </div>
    </div>
  );
}
/* 提案書テンプレート＝「版型（スライドの見た目の種類）」と「使う順番」の2つだけ。
   以前は過去提案書ごとに構成の一覧を作っていたが、文字のリストが並ぶだけで版型が見えなかった。
   ここでは版型を絵で並べ、その下で自社の並び（何ページ目にどの版型を使うか）を1本だけ持つ。 */
const DZ_LAYOUTS = [
  ['cover', '表紙'], ['section', '中表紙'], ['kpi', '数値4枚'], ['cards', 'カード並び'],
  ['issues', '課題グリッド'], ['asistobe', '現状→あるべき姿'], ['duo', '対比2枚'], ['phases', '段階アプローチ'],
  ['steps', '手順・フェーズ'], ['compare', '比較表'], ['roi', '投資対効果'], ['roadmap', 'ロードマップ'],
  ['pricing', '費用プラン'], ['paytable', '支払い表'], ['featuregrid', 'サービス6枚'], ['demo', 'デモ画面'],
  ['links', 'リンク集'], ['company', '会社概要'], ['strengths', '強み6枚'], ['clients', '実績・取引先'],
  ['casestudy', '導入実績'], ['caseimpact', '効果事例'], ['imgpoints', '項目＋画像'],
  ['campaign', 'キャンペーン'], ['bullets', '箇条書き'], ['closing', '最終ページ'],
];
/* ALION標準の並び（実物の提案書45枚から起こした既定値）。［標準の並びを作る］で入る */
const DZ_STANDARD_ORDER = [
  ['表紙', 'cover'], ['目次', 'bullets'],
  ['01 エグゼクティブサマリー', 'section'], ['要旨とKPI', 'kpi'], ['選ばれる3つの理由', 'cards'],
  ['02 現状の課題', 'section'], ['現状の課題', 'issues'], ['目指す姿（AS-IS → TO-BE）', 'asistobe'],
  ['03 ソリューション全体像', 'section'], ['段階的アプローチ', 'phases'], ['仕組み・データ基盤', 'cards'], ['AIがやること・人がやること', 'duo'],
  ['04 デモ画面と機能説明', 'section'], ['ライブプロトタイプ', 'links'],
  ['デモ画面①', 'demo'], ['デモ画面②', 'demo'], ['デモ画面③', 'demo'], ['デモ画面④', 'demo'],
  ['デモ画面⑤', 'demo'], ['デモ画面⑥', 'demo'], ['デモ画面⑦', 'demo'], ['デモ画面⑧', 'demo'],
  ['05 期待効果', 'section'], ['期待効果の全体像', 'cards'], ['投資対効果の試算', 'roi'],
  ['06 ALION紹介・実績', 'section'], ['ALION株式会社について', 'company'], ['ALIONの強み', 'strengths'],
  ['他社との違い', 'compare'], ['補助金サポート', 'cards'], ['自社ブランドサーバー', 'imgpoints'],
  ['開発実績・クライアント', 'clients'], ['導入実績：SWise', 'casestudy'], ['導入実績：ACCESS', 'casestudy'],
  ['導入実績：GMO', 'casestudy'], ['導入実績：東急不動産', 'casestudy'],
  ['AI導入効果事例①', 'caseimpact'], ['AI導入効果事例②', 'caseimpact'],
  ['07 費用プラン', 'section'], ['AI上流工程支援サービス', 'featuregrid'], ['費用プラン', 'pricing'],
  ['期間限定キャンペーン', 'campaign'], ['支払いスケジュール', 'paytable'],
  ['08 導入スケジュール・次のステップ', 'section'], ['導入ロードマップ', 'roadmap'], ['次のステップ', 'steps'],
  ['最終ページ', 'closing'],
];
/* 版型の見た目。1920×1080の実寸で組み立てて、外側を transform で縮小する。
   以前は座標も文字サイズも個別に縮小していたため、掛け違いで枠が狭くなり文字が切れていた。
   実寸で描けば縮尺は1箇所（scale）だけになり、拡大しても文字と枠の関係が崩れない。 */
function LayoutThumb({ kind, d, w = 240 }) {
  const K = w / 1920, H = Math.round(1080 * K);
  const C = (k) => '#' + String((d && d[k]) || '').replace('#', '');
  const m = (d && d.margin) || 56;
  const TS = (d && d.titleSize) || 57, BS = (d && d.bodySize) || 26;
  const CW = 1920 - m * 2;
  const T = (o) => ({ position: 'absolute', left: o.x, top: o.y, width: o.w, fontSize: o.s, fontWeight: o.b ? 700 : 400,
    color: o.c, lineHeight: 1.3, textAlign: o.a || 'left', letterSpacing: o.cs || 0, whiteSpace: o.nw ? 'nowrap' : 'normal' });
  const B = (o) => ({ position: 'absolute', left: o.x, top: o.y, width: o.w, height: o.h, background: o.f, borderRadius: o.r === undefined ? 14 : o.r });
  /* 実物の縦位置：英字ラベル y=40／タイトル y=70／リード文 y=170（2行）／本文は y=300 から */
  const head = (eyebrow, title, msg) => (<>
    <div style={T({ x: m, y: 40, w: CW, s: 24, b: 1, c: C('primary'), cs: 3, nw: 1 })}>{eyebrow}</div>
    <div style={T({ x: m, y: 70, w: CW, s: TS, b: 1, c: C('ink'), nw: 1 })}>{title}</div>
    {msg && <div style={T({ x: m, y: 170, w: CW, s: 30, c: C('ink') })}>{msg}</div>}
  </>);
  /* 実物のカードはアイコンではなく「青い番号→太字の見出し→小さめの本文」。余白は左右36・上32 */
  const cardGrid = (n, cols, label, top) => {
    const rows = Math.ceil(n / cols), gap = 27, y0 = top || 277;
    const gw = (CW - gap * (cols - 1)) / cols, gh = (1080 - y0 - 16 - gap * (rows - 1)) / rows;
    return Array.from({ length: n }).map((_, k) => (
      <div key={k} style={B({ x: m + (k % cols) * (gw + gap), y: y0 + Math.floor(k / cols) * (gh + gap), w: gw, h: gh, f: C('tint'), r: 12 })}>
        <div style={T({ x: 36, y: 37, w: gw - 72, s: 30, b: 1, c: C('primary'), nw: 1 })}>{String(k + 1).padStart(2, '0')}</div>
        <div style={T({ x: 36, y: 75, w: gw - 72, s: 30, b: 1, c: C('ink') })}>{label}</div>
        <div style={T({ x: 36, y: 164, w: gw - 72, s: 23, c: C('ink') })}>説明文がここに入ります。実際の内容に置き換わります。</div>
      </div>));
  };
  const logoImg = <img src="/assets/proposal-design/alion-logo-white.png" alt="" style={{ position: 'absolute', left: m, top: m, width: 84, height: 'auto' }} />;
  /* 実物の表紙グラデを実測（左上#0364FE→右下#59C8E7・対角・濃紺は入らない）。中間色は2点の線形補間で一致する */
  const gradient = `linear-gradient(135deg, ${C('primary')} 0%, ${C('accent')} 100%)`;
  const page = (style, children) => (<div style={{ position: 'absolute', inset: 0, ...style }}>{children}</div>);
  const inner = {
    /* 表紙（実物p1実測）：御中y=323/32px→タイトルy=406/93px→リードy=651/32px→左下に日付+社名26px。ロゴは84×51 */
    cover: page({ background: (d && d.coverBg) === 'solid' ? C('primary') : gradient }, <>
      {logoImg}
      <div style={T({ x: m, y: 323, w: CW, s: 32, b: 1, c: '#fff', nw: 1 })}>◯◯株式会社　御中</div>
      <div style={{ ...T({ x: m, y: 406, w: 1808, s: 93, b: 1, c: '#fff' }), lineHeight: 1.17 }}>◯◯ナレッジAI<br />導入提案書</div>
      <div style={T({ x: m, y: 651, w: 1808, s: 32, b: 1, c: '#fff' })}>提案の一言サマリーが、<br />ここに2行で入ります</div>
      <div style={T({ x: m, y: 984, w: 900, s: 26, b: 1, c: '#fff', nw: 1 })}>2026.08　ALION株式会社</div></>),
    /* 中表紙（実物p3実測）：ENラベルy=409シアン24px→タイトルy=476/75px白→章番号は右中段282pxグラデ文字（青→シアン） */
    section: page({ background: (d && d.sectionBg) === 'primary' ? C('primary') : C('dark') }, <>
      <div style={{ ...T({ x: 1420, y: 452, w: 444, s: 282, b: 1, a: 'right', nw: 1 }), lineHeight: 0.85, background: `linear-gradient(100deg, ${C('primary')}, ${C('accent')})`, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>02</div>
      <div style={T({ x: m, y: 409, w: 1300, s: 24, b: 1, c: C('accent'), cs: 3, nw: 1 })}>CURRENT ISSUES</div>
      <div style={{ ...T({ x: m, y: 476, w: 1170, s: 75, b: 1, c: '#fff' }), lineHeight: 1.17 }}>章タイトルが<br />ここに入ります</div></>),
    /* 最終ページ（実物p46実測）：左寄せ。THANK YOU y=338→タイトル63px y=395→メッセージ27px y=628→左下に社名/URL 21px */
    closing: page({ background: (d && d.coverBg) === 'solid' ? C('primary') : gradient }, <>
      {logoImg}
      <div style={T({ x: m, y: 338, w: CW, s: 24, b: 1, c: C('accent'), cs: 3, nw: 1 })}>THANK YOU</div>
      <div style={{ ...T({ x: m, y: 395, w: 1808, s: 63, b: 1, c: '#fff' }), lineHeight: 1.3 }}>◯◯株式会社様の挑戦に、<br />伴走します。</div>
      <div style={T({ x: m, y: 628, w: 1140, s: 27, c: '#fff' })}>お礼と、次の一歩へのメッセージがここに入ります。</div>
      <div style={T({ x: m, y: 996, w: 900, s: 21, c: '#fff', nw: 1 })}>ALION株式会社　／　https://alion.jp</div></>),
    /* KPI（実物p4実測）：F6F8FCカード4枚 432×259・値37pxグラデ中央・ラベル26px太字→番号付き3カラム(35px/32px/23px)→注記22px */
    kpi: page({ background: '#fff' }, <>{head('EXECUTIVE SUMMARY', 'この提案の要点', 'ここに提案の骨子を2〜3行で述べます。数値は下の4枚で示します。')}
      {[0, 1, 2, 3].map(k => { const gw = (CW - 27 * 3) / 4; return (
        <div key={k} style={B({ x: m + k * (gw + 27), y: 339, w: gw, h: 259, f: C('tint'), r: 14 })}>
          <div style={{ ...T({ x: 0, y: 54, w: gw, s: 37, b: 1, a: 'center', nw: 1 }), lineHeight: 1, background: `linear-gradient(100deg, ${C('primary')}, ${C('cyan')})`, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>約524万円/年</div>
          <div style={T({ x: 20, y: 116, w: gw - 40, s: 26, b: 1, c: C('ink'), a: 'center' })}>創出価値（想定）</div></div>); })}
      {[0, 1, 2].map(k => { const gw = (CW - 47 * 2) / 3, gx = m + k * (gw + 47); return (<div key={k}>
        <div style={{ ...T({ x: gx + 6, y: 657, w: 60, s: 35, b: 1, c: C('primary'), nw: 1 }), lineHeight: 1 }}>{'0' + (k + 1)}</div>
        <div style={T({ x: gx + 70, y: 646, w: gw - 70, s: 32, b: 1, c: C('ink') })}>選ばれる理由の見出し</div>
        <div style={T({ x: gx + 70, y: 740, w: gw - 70, s: 23, c: C('ink') })}>その理由の説明文がここに入ります。具体的な内容に置き換わります。</div></div>); })}
      <div style={T({ x: m, y: 1007, w: CW, s: 22, c: C('ink'), nw: 1 })}>※数値はすべて想定値です。要件定義フェーズで貴社の実データにもとづき確定します。</div></>),
    cards: page({ background: '#fff' }, <>{head('POINT', 'カード並び', 'ポイントを並べて示します。')}{cardGrid(3, Number(d && d.cols) || 3, '見出し')}</>),
    issues: page({ background: '#fff' }, <>{head('ISSUE', '現状の課題', 'これまでの商談で伺った内容を整理しました。')}{cardGrid(6, Number(d && d.cols) || 3, '課題の見出し')}</>),
    /* サービス6枚（実物p39実測）：帯EEF2FA h=139（金額44px青＋縦罫＋参考メモ）→3×2カードF6F8FC（青番号30px→見出し30px→本文25px） */
    featuregrid: page({ background: '#fff' }, <>{head('PRICING・UPSTREAM SUPPORT', 'AI上流工程支援サービス', '専門家として伴走するサービスです。')}
      <div style={B({ x: m, y: 245, w: CW, h: 139, f: C('tint2'), r: 16 })}>
        <div style={T({ x: 96, y: 40, w: 480, s: 44, b: 1, c: C('primary'), nw: 1 })}>月額 ¥200,000</div>
        <div style={B({ x: 524, y: 30, w: 2, h: 79, f: '#E6EAF2', r: 0 })} />
        <div style={T({ x: 574, y: 32, w: CW - 640, s: 24, c: C('muted') })}>他社参考例：CTO採用相当 月額¥1,000,000〜　最大90%のコスト削減で同等の知見</div></div>
      {(() => { const cols = Number(d && d.cols) || 3, gap = 20, y0 = 408; const gw = (CW - gap * (cols - 1)) / cols, gh = (1080 - y0 - m - gap) / 2;
        return Array.from({ length: 6 }).map((_, k) => (
          <div key={k} style={B({ x: m + (k % cols) * (gw + gap), y: y0 + Math.floor(k / cols) * (gh + gap), w: gw, h: gh, f: C('tint'), r: 12 })}>
            <div style={T({ x: 32, y: 30, w: gw - 64, s: 30, b: 1, c: C('primary'), nw: 1 })}>{'0' + (k + 1)}</div>
            <div style={T({ x: 32, y: 64, w: gw - 64, s: 30, b: 1, c: C('ink'), nw: 1 })}>サービスの見出し</div>
            <div style={T({ x: 32, y: 108, w: gw - 64, s: 25, c: C('ink') })}>内容の説明文がここに入ります。実際の内容に置き換わります。</div></div>)); })()}</>),
    /* 現状→あるべき姿（実物p7実測）：見出し灰/青30px・6行・左F4F5F8/右EEF4FF h87・シアン矢印 */
    asistobe: page({ background: '#fff' }, <>{head('AS-IS → TO-BE', '目指す姿', '現状と、AIで到達したい状態を対比しました。')}
      <div style={T({ x: m, y: 318, w: 856, s: 30, b: 1, c: '#9AA3B8', cs: 1.5, nw: 1 })}>現状（AS-IS）</div>
      <div style={T({ x: 1008, y: 318, w: 856, s: 30, b: 1, c: C('primary'), cs: 1.5, nw: 1 })}>目指す姿（TO-BE）</div>
      {[0, 1, 2, 3, 4, 5].map(k => (<div key={k}>
        <div style={B({ x: m, y: 382 + k * 108, w: 856, h: 87, f: '#F4F5F8', r: 14 })}>
          <div style={T({ x: 30, y: 28, w: 796, s: 26, c: '#47506A', nw: 1 })}>現状の一文がここに入ります</div></div>
        <div style={T({ x: 932, y: 404 + k * 108, w: 56, s: 30, b: 1, c: C('cyan'), a: 'center', nw: 1 })}>→</div>
        <div style={B({ x: 1008, y: 382 + k * 108, w: 856, h: 87, f: C('primaryL'), r: 14 })}>
          <div style={T({ x: 30, y: 28, w: 796, s: 26, b: 1, c: C('ink'), nw: 1 })}>目指す状態の一文がここに入ります</div></div></div>))}</>),
    /* 段階的アプローチ（実物p9実測）：EEF4FDカード3枚・グラデPHASEピル・見出し38px・本文26px */
    phases: page({ background: '#fff' }, <>{head('PHASED APPROACH', '段階的アプローチ', '小さく始めて、効果を確かめながら広げていく3段階でご提案します。')}
      {[0, 1, 2].map(k => { const gw = (CW - 30 * 2) / 3, gx = m + k * (gw + 30); return (
        <div key={k} style={B({ x: gx, y: 318, w: gw, h: 605, f: '#EEF4FD', r: 16 })}>
          <div style={{ ...B({ x: 39, y: 39, w: 294, h: 50, f: C('primary'), r: 25 }), background: `linear-gradient(90deg, ${['#005FFF,#2E9BF0', '#2E9BF0,#5ECEE5', '#3B2BFF,#2E9BF0'][k]})` }}>
            <div style={T({ x: 0, y: 13, w: 294, s: 24, b: 1, c: '#fff', a: 'center', nw: 1 })}>PHASE {k + 1}・{['初期導入', '現場拡張', '全社展開'][k]}</div></div>
          <div style={T({ x: 39, y: 108, w: gw - 78, s: 38, b: 1, c: C('ink') })}>この段階の見出しが2行で入ります</div>
          <div style={T({ x: 39, y: 222, w: gw - 78, s: 26, c: C('ink') })}>この段階でやることの説明段落がここに入ります。対象・進め方・狙いを具体的に書きます。</div>
        </div>); })}
      <div style={T({ x: m, y: 954, w: CW, s: 22, c: C('ink'), nw: 1 })}>下段の補足がここに入ります。</div></>),
    /* 強み6枚（実物p27実測）：EEF4FFカード583×253・キーワード52px青→見出し28px青→本文24px・下に青帯フロー */
    strengths: page({ background: '#fff' }, <>{head('OUR STRENGTHS', 'ALIONの強み', '要件整理から保守まで一気通貫で対応します。')}
      {[0, 1, 2, 3, 4, 5].map(k => { const gw = (CW - 30 * 2) / 3, gx = m + (k % 3) * (gw + 30), gy = 317 + Math.floor(k / 3) * 289; return (
        <div key={k} style={B({ x: gx, y: gy, w: gw, h: 253, f: C('primaryL'), r: 14 })}>
          <div style={{ ...T({ x: 34, y: 37, w: gw - 68, s: 52, b: 1, c: C('primary'), nw: 1 }), lineHeight: 1 }}>{['100+', '実務', '早期', '柔軟', '保守', '事例'][k]}</div>
          <div style={T({ x: 34, y: 91, w: gw - 68, s: 28, b: 1, c: C('primary'), nw: 1 })}>強みの見出しが入ります</div>
          <div style={T({ x: 34, y: 138, w: gw - 68, s: 24, c: C('ink') })}>強みの説明文がここに入ります。</div></div>); })}
      <div style={B({ x: m, y: 936, w: CW, h: 88, f: C('primary'), r: 12 })}>
        <div style={T({ x: 0, y: 26, w: CW, s: 32, b: 1, c: '#fff', a: 'center', nw: 1 })}>提案 <span style={{ color: C('blueL2') }}>→</span> 要件定義 <span style={{ color: C('blueL2') }}>→</span> 開発 <span style={{ color: C('blueL2') }}>→</span> 納品 <span style={{ color: C('blueL2') }}>→</span> 保守運用　まで一貫サポート</div></div></>),
    /* 項目＋画像（実物p30実測）：左=角丸アイコン62px＋見出し34px＋本文26px×3段、右=画像枠680×384 */
    imgpoints: page({ background: '#fff' }, <>{head('NVIDIA GB10', '自社ブランド(OEM)のサーバー提供が可能', 'ローカル環境でのAI構築をトータルでサポートします。')}
      {[0, 1, 2].map(k => (<div key={k}>
        <div style={{ ...B({ x: m, y: 472 + k * 125, w: 62, h: 62, f: C('primary'), r: 14 }), background: `linear-gradient(135deg, ${C('primary')}, ${C('cyan')})` }}>
          <div style={T({ x: 0, y: 16, w: 62, s: 28, b: 1, c: '#fff', a: 'center', nw: 1 })}>✓</div></div>
        <div style={T({ x: 144, y: 469 + k * 125, w: 760, s: 34, b: 1, c: C('ink'), nw: 1 })}>項目の見出し</div>
        <div style={T({ x: 144, y: 518 + k * 125, w: 760, s: 26, c: C('ink'), nw: 1 })}>説明の一文がここに入ります。</div></div>))}
      <div style={B({ x: 1098, y: 451, w: 680, h: 384, f: C('tint2'), r: 12 })}>
        <div style={T({ x: 0, y: 178, w: 680, s: 24, c: C('muted'), a: 'center', nw: 1 })}>ここに製品写真を配置（Canvaで貼る）</div></div></>),
    /* 手順・フェーズ（実物p45実測）：カードF4F6FB・グラデ大番号64px→見出し30px→本文23px・▶ #C2CBDE つなぎ */
    steps: page({ background: '#fff' }, <>{head('NEXT STEPS', '次のステップ', 'まずはデモを触っていただくところから始めます。')}
      {[0, 1, 2, 3].map(k => { const gw = (CW - 55 * 3) / 4, gx = m + k * (gw + 55); return (<div key={k}>
        <div style={B({ x: gx, y: 380, w: gw, h: 527, f: C('tint3'), r: 14 })} />
        <div style={{ ...T({ x: gx + 33, y: 425, w: 160, s: 64, b: 1, nw: 1 }), lineHeight: 1, background: `linear-gradient(100deg, ${C('primary')}, ${C('cyan')})`, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>{'0' + (k + 1)}</div>
        <div style={T({ x: gx + 33, y: 504, w: gw - 66, s: 30, b: 1, c: C('ink') })}>やることの見出し</div>
        <div style={T({ x: gx + 33, y: 556, w: gw - 66, s: 23, c: C('ink') })}>具体的な内容の説明がここに入ります。</div>
        {k < 3 && <div style={T({ x: gx + gw + 16, y: 622, w: 24, s: 24, c: '#C2CBDE', nw: 1 })}>▶</div>}</div>); })}</>),
    /* 比較表（実物p28実測）：薄地ヘッダEEF2FA・ゼブラ白/F4F6FB・ALION列は上下通しの青・マーカー4色32px・凡例・薄青の結論帯 */
    compare: page({ background: '#fff' }, <>{head('WHY ALION', '他のAI・開発の選択肢と、何が違うのか', '評価軸を「AI・システム開発全般」に置いて、主要な選択肢と整理しました。')}
      {(() => {
        const y0 = 281, headH = 65, rowH = 94, axisW = 452, alionW = 254, cellW = (CW - axisW - alionW) / 5;
        const names = ['評価軸', '汎用AI', 'AIベンダ', '大手SIer', '業務SaaS', 'コンサル', 'ALION'];
        const marks = [['△', '◎', '○', '△', '△', '◎'], ['✕', '△', '○', '✕', '◎', '◎'], ['✕', '△', '◎', '✕', '✕', '◎'], ['◎', '○', '✕', '○', '△', '○'], ['✕', '△', '△', '△', '✕', '◎']];
        const mkc = { '◎': '#2A6FDB', '○': '#3A4256', '△': '#F5A623', '✕': '#B6BCCB' };
        const cx = (i) => m + (i === 0 ? 0 : axisW + (i - 1) * cellW), cw = (i) => (i === 0 ? axisW : (i === 6 ? alionW : cellW));
        return (<>
          {names.map((nm, i) => (<div key={'h' + i} style={B({ x: cx(i), y: y0, w: cw(i), h: headH, f: i === 6 ? C('primary') : C('tint2'), r: 0 })}>
            <div style={T({ x: i === 0 ? 26 : 6, y: i === 6 ? 18 : 20, w: cw(i) - 12, s: i === 6 ? 26 : 22, b: 1, c: i === 6 ? '#fff' : (i === 0 ? C('muted') : C('grayM')), a: i ? 'center' : 'left', nw: 1 })}>{nm}</div></div>))}
          {marks.map((row, ri) => (<div key={'r' + ri}>
            {ri % 2 === 1 && <div style={B({ x: m, y: y0 + headH + ri * rowH, w: CW - alionW, h: rowH, f: C('tint3'), r: 0 })} />}
            <div style={B({ x: cx(6), y: y0 + headH + ri * rowH, w: alionW, h: rowH, f: C('primary'), r: 0 })} />
            <div style={T({ x: cx(0) + 26, y: y0 + headH + ri * rowH + 30, w: axisW - 40, s: 23, b: 1, c: C('ink'), nw: 1 })}>評価の項目</div>
            {row.map((mk2, ci) => <div key={ci} style={T({ x: cx(ci + 1), y: y0 + headH + ri * rowH + 28, w: cw(ci + 1), s: 32, b: 1, c: ci === 5 ? '#fff' : mkc[mk2], a: 'center', nw: 1 })}>{mk2}</div>)}</div>))}
          <div style={T({ x: m + 26, y: y0 + headH + 5 * rowH + 16, w: 900, s: 22, c: C('muted'), nw: 1 })}>◎ 優れる　○ 対応　△ 限定的　✕ 非対応</div>
          <div style={B({ x: m, y: y0 + headH + 5 * rowH + 60, w: CW, h: 92, f: C('primaryL'), r: 0 })}>
            <div style={T({ x: 130, y: 30, w: CW - 260, s: 26, b: 1, c: C('ink'), nw: 1 })}>結論の一文がここに入ります。</div></div></>);
      })()}</>),
    /* 投資対効果（実物p24実測）：左=初年度費用パネルF6F8FC・右=年間効果パネルEEF4FF 2×2（値36px青）・合計48px・下に青帯→補足→注記 */
    roi: page({ background: '#fff' }, <>{head('VALUE SIMULATION', '導入費用を、削減効果で着実に回収していく試算', '初期導入フェーズの費用と、そこから生まれる年間の効果を並べた試算です。')}
      <div style={B({ x: m, y: 230, w: 730, h: 554, f: C('tint'), r: 14 })}>
        <div style={B({ x: 34, y: 32, w: 464, h: 51, f: '#E7EEFF', r: 8 })}>
          <div style={T({ x: 24, y: 13, w: 430, s: 22, b: 1, c: C('primary'), nw: 1 })}>初年度の費用（クラウド構成での想定）</div></div>
        {[0, 1, 2].map(k => (<div key={k}>
          <div style={T({ x: 34, y: 97 + k * 115, w: 662, s: 26, b: 1, c: C('ink'), nw: 1 })}>費用の項目{k + 1}</div>
          <div style={T({ x: 34, y: 127 + k * 115, w: 662, s: 32, b: 1, c: C('primary'), nw: 1 })}>約400万円</div>
          <div style={T({ x: 34, y: 165 + k * 115, w: 662, s: 20, c: C('ink'), nw: 1 })}>前提の一言がここに入ります</div></div>))}
        <div style={T({ x: 34, y: 483, w: 200, s: 26, b: 1, c: C('ink'), nw: 1 })}>初年度費用</div>
        <div style={T({ x: 183, y: 458, w: 400, s: 48, b: 1, c: C('ink'), nw: 1 })}>約520万円</div></div>
      <div style={B({ x: 814, y: 230, w: 1920 - 56 - 814, h: 554, f: C('primaryL'), r: 14 })}>
        <div style={B({ x: 34, y: 32, w: 440, h: 51, f: '#E7EEFF', r: 8 })}>
          <div style={T({ x: 24, y: 13, w: 410, s: 22, b: 1, c: C('primary'), nw: 1 })}>年間の削減効果・創出価値（想定値）</div></div>
        {[0, 1, 2, 3].map(k => { const cw2 = (1920 - 56 - 814 - 96) / 2, cx2 = 34 + (k % 2) * (cw2 + 28), cy2 = 94 + Math.floor(k / 2) * 151; return (<div key={k}>
          <div style={T({ x: cx2, y: cy2, w: cw2, s: 36, b: 1, c: C('primary'), nw: 1 })}>約60万円/年</div>
          <div style={T({ x: cx2, y: cy2 + 43, w: cw2, s: 25, b: 1, c: C('ink'), nw: 1 })}>効果の見出し</div>
          <div style={T({ x: cx2, y: cy2 + 75, w: cw2, s: 20, c: C('ink') })}>算定根拠の一文がここに入ります。</div></div>); })}
        <div style={T({ x: 34, y: 483, w: 160, s: 26, b: 1, c: C('ink'), nw: 1 })}>年間効果</div>
        <div style={T({ x: 157, y: 458, w: 400, s: 48, b: 1, c: C('ink'), nw: 1 })}>約524万円</div></div>
      <div style={B({ x: m, y: 807, w: CW, h: 89, f: C('primary'), r: 12 })}>
        <div style={T({ x: 0, y: 27, w: CW, s: 30, b: 1, c: '#fff', a: 'center', nw: 1 })}>初年度費用 約520万円　→　年間効果 約524万円　→　回収の目安 最大1年以内</div></div>
      <div style={T({ x: m, y: 906, w: CW, s: 21, c: C('ink'), nw: 1 })}>削減効果の補足段落がここに入ります。</div>
      <div style={T({ x: m, y: 977, w: CW, s: 19, c: C('ink'), nw: 1 })}>※すべて想定値です。前提は要件定義フェーズで確定します。</div></>),
    /* ロードマップ（実物p44実測）：上に横タイムライン（罫線DDE3EE・青丸27px・時期20px青・ラベル23px）＋下にPHASE詳細カード3枚F4F6FB */
    roadmap: page({ background: '#fff' }, <>{head('ROADMAP', '導入ロードマップ', 'ご要望のスケジュールから逆算した計画です。')}
      <div style={B({ x: 128, y: 337, w: 1663, h: 5, f: '#DDE3EE', r: 0 })} />
      {[0, 1, 2, 3, 4].map(k => { const cx2 = 200 + k * ((1663 - 144) / 4); return (<div key={k}>
        <div style={B({ x: cx2 - 13, y: 327, w: 27, h: 27, f: C('primary'), r: 14 })} />
        <div style={T({ x: cx2 - 130, y: 367, w: 260, s: 20, b: 1, c: C('primary'), a: 'center', nw: 1 })}>{['NOW', '2027 Q2', '2027 Q3–Q4', '2028 Q1', '2028 Q2〜'][k]}</div>
        <div style={T({ x: cx2 - 130, y: 405, w: 260, s: 23, b: 1, c: C('ink'), a: 'center', nw: 1 })}>{['提案', 'PoC・要件定義', '本開発・テスト', 'リリース', '拡張'][k]}</div></div>); })}
      {[0, 1, 2].map(k => { const gw = (CW - 27 * 2) / 3, gx = m + k * (gw + 27); return (
        <div key={k} style={B({ x: gx, y: 518, w: gw, h: 478, f: C('tint3'), r: 14 })}>
          <div style={T({ x: 36, y: 32, w: gw - 72, s: 20, b: 1, c: '#8A93AB', cs: 2, nw: 1 })}>PHASE {k + 1}</div>
          <div style={T({ x: 36, y: 69, w: gw - 72, s: 32, b: 1, c: C('primary'), nw: 1 })}>{['要件定義（約3ヶ月）', '本開発・テスト（約6ヶ月）', '拡張'][k]}</div>
          <div style={T({ x: 36, y: 125, w: gw - 72, s: 23, c: C('ink') })}>この段階でやることの説明段落がここに入ります。対象・進め方・決めることを具体的に書きます。</div></div>); })}</>),
    /* 費用プラン（実物p40実測）：カードF4F6FB 563×650・STEPピル（グラデ／最終段は濃紺）・名38px→価格32px青→本文26px・▶つなぎ */
    pricing: page({ background: '#fff' }, <>{head('PRICING・SMALL START', '費用プラン ― スモールスタート構成', '上流工程支援から着手できる3ステップの構成です。')}
      {[0, 1, 2].map(k => { const gw = (CW - 53 * 2) / 3, gx = m + k * (gw + 53); return (<div key={k}>
        <div style={B({ x: gx, y: 307, w: gw, h: 650, f: C('tint3'), r: 14 })} />
        {/* ピルの実測色：STEP1/2=#2E9BF0→#5ECEE5 の明るいグラデ／STEP3=#050531（実物p40のXML・ピクセル両実測） */}
        <div style={{ ...B({ x: gx + 39, y: 349, w: 145, h: 50, f: C('primary'), r: 25 }), background: k === 2 ? C('dark') : 'linear-gradient(90deg, #2E9BF0, #5ECEE5)' }}>
          <div style={T({ x: 0, y: 13, w: 145, s: 24, b: 1, c: '#fff', a: 'center', nw: 1 })}>STEP {k + 1}</div></div>
        <div style={T({ x: gx + 39, y: 424, w: gw - 78, s: 38, b: 1, c: C('ink'), nw: 1 })}>{['AI上流工程支援', '本開発', '運用・改善'][k]}</div>
        <div style={T({ x: gx + 39, y: 487, w: gw - 78, s: 32, b: 1, c: C('primary'), nw: 1 })}>{['月額20万円〜', 'フェーズ1 約400〜600万円', '月額20〜30万円'][k]}</div>
        <div style={T({ x: gx + 39, y: 545, w: gw - 78, s: 26, c: C('ink') })}>このプランに含まれる内容の説明がここに入ります。</div>
        {k < 2 && <div style={T({ x: gx + gw + 15, y: 615, w: 24, s: 26, c: '#C2CBDE', nw: 1 })}>▶</div>}</div>); })}
      <div style={T({ x: m, y: 977, w: CW, s: 19, c: C('ink'), nw: 1 })}>※金額はすべて概算であり、要件定義フェーズで確定します。</div></>),
    /* 支払い表（実物p42実測）：上に横タイムライン→薄地ヘッダEEF2FA・ゼブラ白/F4F6FB・金額列EEF4FF/E2ECFF交互・金額28px青右寄せ→合計帯→補足→注記 */
    paytable: page({ background: '#fff' }, <>{head('PAYMENT SCHEDULE', 'お支払いスケジュール', '開発費は着手時と納品時の2回、運用はリリース後の月額のみです。')}
      <div style={B({ x: 128, y: 251, w: 1663, h: 5, f: '#DDE3EE', r: 0 })} />
      {[0, 1, 2, 3, 4, 5].map(k => { const cx2 = 200 + k * ((1663 - 144) / 5); return (<div key={k}>
        <div style={B({ x: cx2 - 11, y: 242, w: 22, h: 22, f: C('primary'), r: 11 })} />
        <div style={T({ x: cx2 - 110, y: 278, w: 220, s: 22, b: 1, c: C('primary'), a: 'center', nw: 1 })}>{['2027.04', '2027.05–06', '2027.07', '2027.08–11', '2027.12', '2028.01〜'][k]}</div>
        <div style={T({ x: cx2 - 110, y: 316, w: 220, s: 21, b: 1, c: C('ink'), a: 'center', nw: 1 })}>{['ご契約・上流着手', '上流工程支援', '本開発着手', '開発・テスト', 'リリース・納品', '運用'][k]}</div></div>); })}
      {[0, 1, 2, 3, 4, 5].map(r => { const y = 389 + r * 54, amtW = Math.round(CW * .217); return (<div key={r}>
        {r > 0 && r % 2 === 0 && <div style={B({ x: m, y, w: CW - amtW, h: 54, f: C('tint3'), r: 0 })} />}
        {r === 0 && <div style={B({ x: m, y, w: CW, h: 54, f: C('tint2'), r: 0 })} />}
        {r > 0 && <div style={B({ x: m + CW - amtW, y, w: amtW, h: 54, f: r % 2 ? C('primaryL') : '#E2ECFF', r: 0 })} />}
        <div style={T({ x: m + 26, y: y + 15, w: 340, s: r === 0 ? 22 : 23, b: 1, c: r === 0 ? C('muted') : C('ink'), nw: 1 })}>{r === 0 ? '時期' : `2027年${r + 3}月`}</div>
        <div style={T({ x: m + Math.round(CW * .198) + 12, y: y + 15, w: 700, s: r === 0 ? 22 : 23, b: r === 0 ? 1 : 0, c: r === 0 ? C('muted') : C('ink'), nw: 1 })}>{r === 0 ? '内容' : '上流工程支援 月額'}</div>
        <div style={T({ x: m + CW - amtW, y: y + 13, w: amtW - 20, s: r === 0 ? 22 : 28, b: 1, c: r === 0 ? C('muted') : C('primary'), a: 'right', nw: 1 })}>{r === 0 ? '　' : '¥200,000'}</div>
        {r === 0 && <div style={T({ x: m + CW - amtW + 26, y: y + 15, w: amtW - 40, s: 22, b: 1, c: C('muted'), nw: 1 })}>お支払い額</div>}</div>); })}
      <div style={B({ x: m, y: 803, w: CW, h: 80, f: C('primaryL'), r: 0 })}>
        <div style={T({ x: 26, y: 26, w: 90, s: 22, b: 1, c: C('primary'), a: 'center', nw: 1 })}>合計</div>
        <div style={T({ x: 127, y: 26, w: CW - 160, s: 25, c: C('ink'), nw: 1 })}>初期費用 合計 約400万円　＋　運用 月額 約20万円　＝　3年総額 約1,120万円</div></div>
      <div style={T({ x: m, y: 906, w: CW, s: 21, c: C('ink'), nw: 1 })}>費用の前提や補足の段落がここに入ります。</div>
      <div style={T({ x: m, y: 977, w: CW, s: 19, c: C('ink'), nw: 1 })}>※金額・時期はすべて想定値です。</div></>),
    /* デモ画面（実物p13実測）：左=主張42px（読点の後を青）y=217→リード34px太字y=358→本文30px y=487／右=スクショ枠 x=880 y=265 984×612 */
    demo: page({ background: '#fff' }, <>{head('DEMO・AI CHAT', 'AIチャット', '')}
      <div style={T({ x: m, y: 217, w: 766, s: 42, b: 1, c: C('ink') })}>聞きたいことを、<span style={{ color: C('primary') }}>そのまま自然文で聞ける</span></div>
      <div style={T({ x: m, y: 358, w: 766, s: 34, b: 1, c: C('ink') })}>何ができる画面かの要約が、2〜3行でここに入ります。</div>
      <div style={T({ x: m, y: 487, w: 766, s: 30, c: C('ink') })}>画面の具体的な説明。実際の項目名・区分・数値を入れた説明文がここに段落で入ります。狙いまで書き切ります。</div>
      <div style={B({ x: 880, y: 265, w: 984, h: 612, f: C('tint2'), r: 12 })}>
        <div style={T({ x: 0, y: 290, w: 984, s: 26, c: C('muted'), a: 'center', nw: 1 })}>ここに画面のスクショを配置（Canvaで貼る）</div></div></>),
    /* 対比2枚（実物p10実測）：左=濃紺#050531・右=F6F8FC、チップ＋見出し38px＋箇条書き32px */
    duo: page({ background: '#fff' }, <>{head('AI × HUMAN', 'AIがやること・人がやること', 'AIは集める・整える・提示するまでを担い、判断は人が持ちます。')}
      {[0, 1].map(k => { const pw2 = (CW - 33) / 2, px2 = m + k * (pw2 + 33); return (
        <div key={k} style={B({ x: px2, y: 307, w: pw2, h: 660, f: k ? C('tint') : C('dark'), r: 16 })}>
          <div style={B({ x: 45, y: 42, w: 72, h: 47, f: k ? '#fff' : C('primary'), r: 23 })}>
            <div style={T({ x: 0, y: 12, w: 72, s: 20, b: 1, c: k ? C('primary') : '#fff', a: 'center', nw: 1 })}>{k ? '人' : 'AI'}</div></div>
          <div style={T({ x: 134, y: 44, w: pw2 - 170, s: 38, b: 1, c: k ? C('ink') : '#fff', nw: 1 })}>{k ? '人が判断すること' : 'AIがやること'}</div>
          {[0, 1, 2, 3, 4].map(j => <div key={j} style={T({ x: 45, y: 108 + j * 77, w: pw2 - 90, s: 32, c: k ? C('ink') : '#E8F0FF', nw: 1 })}>・ やること・判断することの一文</div>)}
        </div>); })}
      <div style={T({ x: m, y: 987, w: CW, s: 22, c: C('ink'), nw: 1 })}>下段の補足がここに入ります。</div></>),
    /* リンク集（実物p12実測）：EEF4FDパネル・URL 40px青Inter＋角丸アイコン・主要画面リスト4列 */
    links: page({ background: '#fff' }, <>{head('LIVE PROTOTYPE', 'ライブプロトタイプ', 'PCとスマートフォンで操作できるデモを公開しています。')}
      <div style={B({ x: m, y: 307, w: CW, h: 631, f: '#EEF4FD', r: 20 })}>
        {[0, 1].map(k => (<div key={k}>
          <div style={{ ...B({ x: 349, y: 48 + k * 120, w: 92, h: 92, f: C('primary'), r: 20 }), background: `linear-gradient(135deg, ${C('primary')}, ${C('cyan')})` }} />
          <div style={T({ x: 512, y: 55 + k * 120, w: 1200, s: 20, b: 1, c: '#8A93AB', cs: 2, nw: 1 })}>{k ? 'MOBILE — FIELD DEMO' : 'PC — DESKTOP DEMO'}</div>
          <div style={T({ x: 512, y: 100 + k * 120, w: 1240, s: 40, b: 1, c: C('primary'), nw: 1 })}>https://demo-url.vercel.app{k ? '/mobile' : '/'}</div></div>))}
        <div style={T({ x: 64, y: 336, w: CW - 128, s: 30, b: 1, c: '#8A93AB', a: 'center', cs: 3, nw: 1 })}>公開中の主要画面</div>
        {[0, 1, 2, 3].map(c2 => [0, 1, 2].map(r2 => (
          <div key={c2 + '-' + r2} style={T({ x: 226, y: 403 + r2 * 52, w: 330, s: 27, b: 1, c: C('ink'), nw: 1, ...{ x: 226 + c2 * 352 } })}>画面名がここに入ります</div>)))}
      </div>
      <div style={T({ x: m, y: 954, w: CW, s: 22, c: C('ink'), nw: 1 })}>役割切替などの補足がここに入ります。</div></>),
    /* 会社概要（実物p26実測）：青べた数値4枚 432×230（値84px白）＋白枠カード3枚（head36px青） */
    company: page({ background: '#fff' }, <>{head('COMPANY OVERVIEW', 'ALION株式会社について', '日台をつなぐAI開発の専業集団です。')}
      {[0, 1, 2, 3].map(k => { const gw = (CW - 27 * 3) / 4, gx = m + k * (gw + 27); return (
        <div key={k} style={B({ x: gx, y: 340, w: gw, h: 230, f: C('primary'), r: 16 })}>
          <div style={{ ...T({ x: 20, y: 61, w: gw - 40, s: 84, b: 1, c: '#fff', a: 'center', nw: 1 }), lineHeight: 1 }}>{['70+', '200+', '3', '60+'][k]}</div>
          <div style={T({ x: 20, y: 158, w: gw - 40, s: 26, b: 1, c: '#fff', a: 'center', nw: 1 })}>数値の説明ラベル</div></div>); })}
      {[0, 1, 2].map(k => { const gw = (CW - 30 * 2) / 3, gx = m + k * (gw + 30); return (
        <div key={k} style={{ ...B({ x: gx, y: 690, w: gw, h: 257, f: '#fff', r: 16 }), border: '1px solid #E6EAF2' }}>
          <div style={T({ x: 34, y: 26, w: gw - 68, s: 36, b: 1, c: C('primary'), nw: 1 })}>強みの見出し</div>
          <div style={T({ x: 34, y: 78, w: gw - 68, s: 28, c: C('ink') })}>強みの説明文がここに入ります。</div></div>); })}</>),
    /* 実績・取引先（実物p31実測）：数値カードF6F8FC 4枚（値108pxグラデ）＋主な取引先ロゴ帯（Canva貼付枠） */
    clients: page({ background: '#fff' }, <>{head('TRACK RECORD', '開発実績・クライアント', '幅広い実績と高い継続率。大手との取引が信頼の裏付けです。')}
      {[0, 1, 2, 3].map(k => { const gw = (CW - 27 * 3) / 4, gx = m + k * (gw + 27); return (
        <div key={k} style={B({ x: gx, y: 330, w: gw, h: 227, f: C('tint'), r: 16 })}>
          <div style={{ ...T({ x: 20, y: 48, w: gw - 40, s: 108, b: 1, a: 'center', nw: 1 }), lineHeight: 1, background: `linear-gradient(100deg, ${C('primary')}, ${C('cyan')})`, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>{['200+', '70+', '95%', '10+'][k]}</div>
          <div style={T({ x: 20, y: 154, w: gw - 40, s: 30, b: 1, c: C('ink'), a: 'center', nw: 1 })}>数値の説明ラベル</div></div>); })}
      <div style={T({ x: m, y: 620, w: CW, s: 22, b: 1, c: C('grayM'), a: 'center', cs: 2, nw: 1 })}>主な取引先</div>
      <div style={B({ x: 252, y: 683, w: 1416, h: 273, f: C('tint2'), r: 16 })}>
        <div style={T({ x: 0, y: 122, w: 1416, s: 24, c: C('muted'), a: 'center', nw: 1 })}>ここにクライアントロゴを配置（Canvaで貼る）</div></div></>),
    /* 導入実績の個票（実物p32実測）：左=ロゴ枠→見出し46px→本文29px→リンクピル、右=スクショ枠840×718 */
    casestudy: page({ background: '#fff' }, <>{head('TRACK RECORD', '取引・導入実績：○○', '')}
      <div style={B({ x: m, y: 317, w: 450, h: 114, f: C('tint2'), r: 10 })}>
        <div style={T({ x: 0, y: 46, w: 450, s: 20, c: C('muted'), a: 'center', nw: 1 })}>クライアントロゴ（Canvaで貼る）</div></div>
      <div style={T({ x: m, y: 453, w: 687, s: 46, b: 1, c: C('ink') })}>実績を言い切る見出しが2行で入ります</div>
      <div style={T({ x: m, y: 603, w: 687, s: 29, c: C('ink') })}>サービスや導入内容の説明がここに段落で入ります。実際の内容に置き換わります。</div>
      <div style={B({ x: m, y: 847, w: 175, h: 62, f: '#E7EEFF', r: 31 })}>
        <div style={T({ x: 0, y: 16, w: 175, s: 30, b: 1, c: C('primary'), a: 'center', nw: 1 })}>url.jp</div></div>
      <div style={B({ x: 911, y: 254, w: 840, h: 718, f: C('tint2'), r: 12 })}>
        <div style={T({ x: 0, y: 345, w: 840, s: 26, c: C('muted'), a: 'center', nw: 1 })}>ここに製品スクショを配置（Canvaで貼る）</div></div></>),
    /* 効果事例（実物p36実測）：左=スクショ枠865×578、右=大数値84px青＋単位32px＋補足26px ×3段 */
    caseimpact: page({ background: '#fff' }, <>{head('CASE STUDY ・ IMPACT', 'AI導入効果事例①　｜　○○システム', '手作業を大幅に削減し、効果を見える化しました。')}
      <div style={B({ x: m, y: 331, w: 865, h: 578, f: C('tint2'), r: 12 })}>
        <div style={T({ x: 0, y: 275, w: 865, s: 26, c: C('muted'), a: 'center', nw: 1 })}>ここに画面スクショを配置（Canvaで貼る）</div></div>
      {[0, 1, 2].map(k => (<div key={k}>
        <div style={{ ...T({ x: 981, y: 409 + k * 169, w: 300, s: 84, b: 1, c: C('primary'), nw: 1 }), lineHeight: 1 }}>{['133', '336', 'KPI'][k]}</div>
        <div style={T({ x: [1131, 1131, 1098][k], y: 432 + k * 169, w: 600, s: 32, b: 1, c: C('ink'), nw: 1 })}>{['時間/月 削減', '万円/年 コスト削減', 'を自動集計'][k]}</div>
        <div style={T({ x: 981, y: 496 + k * 169, w: 880, s: 26, c: C('ink'), nw: 1 })}>算定根拠の一文がここに入ります</div></div>))}
      <div style={T({ x: m, y: 987, w: CW, s: 22, c: C('ink'), nw: 1 })}>※導入効果の試算例です。</div></>),
    /* キャンペーン（実物p41実測）：中央ピルE3E8FF→見出し96px（後半を青）→本文34px中央 */
    campaign: page({ background: '#fff' }, <>{head('LIMITED CAMPAIGN', '期間限定キャンペーン', '早期にご決定いただいた場合の特典をご用意しています。')}
      <div style={B({ x: 846, y: 464, w: 228, h: 59, f: '#E3E8FF', r: 29 })}>
        <div style={T({ x: 0, y: 15, w: 228, s: 26, b: 1, c: C('primary'), a: 'center', nw: 1 })}>期間限定特典</div></div>
      <div style={T({ x: 0, y: 557, w: 1920, s: 96, b: 1, c: C('ink'), a: 'center', nw: 1 })}>上流工程支援 <span style={{ color: C('primary') }}>1ヶ月分が無料</span></div>
      <div style={T({ x: 410, y: 705, w: 1100, s: 34, c: C('ink'), a: 'center' })}>条件の説明がここに2行ほどで入ります。</div></>),
    /* 箇条書き（実物p2目次実測）：2カラム・グラデ大番号80px Inter・項目40px太字・行間隔166px */
    bullets: page({ background: '#fff' }, <>{head('CONTENTS', '目次', '')}
      {[0, 1, 2, 3, 4, 5, 6, 7].map(k => { const col = Math.floor(k / 4), x = m + 6 + col * 946, y = 334 + (k % 4) * 166; return (<div key={k}>
        <div style={{ ...T({ x, y, w: 130, s: 80, b: 1, nw: 1 }), lineHeight: 1, background: `linear-gradient(100deg, ${C('primary')}, ${C('cyan')})`, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>{'0' + (k + 1)}</div>
        <div style={T({ x: x + 143, y: y + 6, w: 640, s: 40, b: 1, c: C('ink'), nw: 1 })}>章タイトルが入ります</div>
        <div style={T({ x: x + 143, y: y + 63, w: 640, s: 24, b: 1, c: C('muted'), cs: 1, nw: 1 })}>SECTION TITLE</div></div>); })}</>),
  };
  return (
    <div style={{ position: 'relative', width: w, height: H, borderRadius: 6, overflow: 'hidden', border: '1px solid #e4e7ef', background: '#fff', flex: '0 0 auto' }}>
      <div style={{ position: 'absolute', top: 0, left: 0, width: 1920, height: 1080, transform: `scale(${K})`, transformOrigin: '0 0' }}>
        {inner[kind] || inner.cards}
      </div>
    </div>
  );
}
function ProposalTemplateCard() {
  const { showToast, can } = useStore();
  const editable = can('manageKnowledge');
  const [design, setDesign] = React.useState(null);
  const [tpl, setTpl] = React.useState(null);      // 使う順番（1本だけ）
  const [busy, setBusy] = React.useState('');
  const [openOrder, setOpenOrder] = React.useState(false);
  const load = async () => {
    try { const r = await API.deckDesign(); setDesign(r.design); } catch (_) {}
    try { const r = await API.proposalTemplates(); setTpl((r.templates || []).find(t => t.kind !== 'design') || null); } catch (_) {}
  };
  React.useEffect(() => { load(); }, []);
  const btn = { fontSize: 12, fontWeight: 600, padding: '6px 12px', borderRadius: 8, border: '1px solid #d9d5f2', background: '#fff', color: '#4a5af0', cursor: 'pointer' };
  const inp = { fontSize: 12, padding: '5px 8px', borderRadius: 7, border: '1px solid #e4e0f5', color: '#1c1f26', background: '#fff' };
  const saveOrder = async (slides, name) => {
    setBusy('save');
    try {
      const r = await API.proposalTemplateAct({ action: 'save', id: (tpl && tpl.id) || undefined, name: name || (tpl && tpl.name) || 'ALION標準の並び', desc: `全${slides.length}枚`, slides });
      const t = (r.templates || []).find(x => x.kind !== 'design');
      setTpl(t || null);
      if (t && !t.isDefault) { const r2 = await API.proposalTemplateAct({ action: 'default', id: t.id }); setTpl((r2.templates || []).find(x => x.id === t.id) || t); }
      showToast(window.t('tpl.saved'));
    } catch (e) { showToast((e && e.message) || '失敗しました', 'x'); }
    setBusy('');
  };
  const makeStandard = () => saveOrder(DZ_STANDARD_ORDER.map(([title, kind]) => ({ title, kind, chars: 0 })), 'ALION標準の並び');
  const slides = (tpl && tpl.slides) || [];
  const setRow = (i, patch) => setTpl(t => ({ ...t, slides: t.slides.map((x, j) => (j === i ? { ...x, ...patch } : x)) }));
  const move = (i, dd) => setTpl(t => { const a = t.slides.slice(); const j = i + dd; if (j < 0 || j >= a.length) return t; const tmp = a[i]; a[i] = a[j]; a[j] = tmp; return { ...t, slides: a }; });
  const addRow = (i) => setTpl(t => { const a = t.slides.slice(); a.splice(i + 1, 0, { title: '', kind: 'cards', chars: 0 }); return { ...t, slides: a }; });
  const delRow = (i) => setTpl(t => ({ ...t, slides: t.slides.filter((_, j) => j !== i) }));

  return (
    <div style={{ background: '#fff', border: '1px solid #ece9f8', borderRadius: 12, padding: 16 }}>
      <div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
          <span style={{ fontSize: 14, fontWeight: 700, color: '#1c1f26' }}>{t('tpl.order')}</span>
          <span style={{ fontSize: 11.5, color: '#8b919b' }}>{t('tpl.orderSub')}</span>
          <span style={{ fontSize: 11.5, color: '#8b919b' }}>{tpl ? tpl.name + t('tpl.count', { n: slides.length }) : t('tpl.none')}</span>
          <span style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
            {editable && !tpl && <button style={{ ...btn, background: '#4a5af0', color: '#fff', borderColor: '#4a5af0' }} disabled={!!busy} onClick={makeStandard}>{t('tpl.makeStd')}</button>}
            {/* 版型を追加・変更したとき、保存済みの並びが旧割当のまま残る。ワンクリックで最新の標準割当へ置き換えられるように */}
            {editable && tpl && <button style={btn} disabled={!!busy} onClick={() => { if (window.confirm(t('tpl.remakeConfirm'))) makeStandard(); }}>{t('tpl.remakeStd')}</button>}
            {editable && tpl && <button style={btn} onClick={() => setOpenOrder(o => !o)}>{openOrder ? t('tpl.close') : t('tpl.edit')}</button>}
            {editable && tpl && openOrder && <button style={{ ...btn, background: '#4a5af0', color: '#fff', borderColor: '#4a5af0' }} disabled={!!busy} onClick={() => saveOrder(slides)}>{t('tpl.save')}</button>}
          </span>
        </div>
        {tpl && !openOrder && (
          <div style={{ fontSize: 11.5, color: '#a8aeb8', marginTop: 6, lineHeight: 1.7 }}>
            {slides.slice(0, 14).map((x, i) => `${i + 1}.${x.title || x.kind}`).join(' / ')}{slides.length > 14 ? t('tpl.more', { n: slides.length }) : ''}
          </div>
        )}
        {tpl && openOrder && (
          <div style={{ maxHeight: 420, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 4, marginTop: 10 }}>
            {slides.map((r, i) => (
              <div key={i} style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap', background: '#fff', border: '1px solid #f0eefb', borderRadius: 8, padding: '5px 7px' }}>
                <span style={{ fontSize: 11, color: '#a8aeb8', width: 24, flex: '0 0 auto', textAlign: 'right' }}>{i + 1}</span>
                <input value={r.title} onChange={e => setRow(i, { title: e.target.value })} placeholder={t('tpl.rowTitle')} style={{ ...inp, flex: '1 1 200px' }} />
                <select value={r.kind} onChange={e => setRow(i, { kind: e.target.value })} style={{ ...inp, flex: '0 0 auto' }}>
                  {DZ_LAYOUTS.map(([k]) => <option key={k} value={k}>{t('dz.k.' + k)}</option>)}
                </select>
                <span style={{ display: 'flex', gap: 2, flex: '0 0 auto' }}>
                  <button onClick={() => move(i, -1)} style={{ ...btn, padding: '3px 7px' }}>↑</button>
                  <button onClick={() => move(i, 1)} style={{ ...btn, padding: '3px 7px' }}>↓</button>
                  <button onClick={() => addRow(i)} style={{ ...btn, padding: '3px 7px' }}>＋</button>
                  <button onClick={() => delRow(i)} style={{ ...btn, padding: '3px 7px', color: '#b91c1c', borderColor: '#f3d6d6' }}>×</button>
                </span>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}
function SkillFilesCard() {
  const {isAdmin,navigate}=useStore();
  return <Card pad={18} title={window.t("extra.knowledge.skills")}><p style={{fontSize:12.5,color:'#7b828d',lineHeight:1.8}}>{window.t("extra.knowledge.skillsHint")}</p>{isAdmin&&<Button size="sm" onClick={()=>navigate('skills')}>{window.t("extra.knowledge.openSkills")}</Button>}</Card>;
}

function AiPromptSettings({ only }) {
  const show = (k) => !Array.isArray(only) || only.includes(k);
  const { saveProposalGuidance, saveQuoteGuidance, saveProtoGuidance, saveShodanGuidance, can } = useStore();
  const D = window.APP_DATA;
  const editable = can('manageKnowledge');
  const [text, setText] = React.useState(D.proposalGuidance || '');
  const [dirty, setDirty] = React.useState(false);
  const [qtext, setQtext] = React.useState(D.quoteGuidance || '');
  const [qdirty, setQdirty] = React.useState(false);
  const [ptext, setPtext] = React.useState(D.prototypeGuidance || '');
  const [pdirty, setPdirty] = React.useState(false);
  const [stext, setStext] = React.useState(D.shodanGuidance || '');
  const [sdirty, setSdirty] = React.useState(false);
  const pickP = (txt) => { if (text.trim() && !window.confirm(window.t("label.extra66"))) return; setText(txt); setDirty(true); };
  const pickQ = (txt) => { if (qtext.trim() && !window.confirm(window.t("label.extra66"))) return; setQtext(txt); setQdirty(true); };
  const pickPr = (txt) => { if (ptext.trim() && !window.confirm(window.t("label.extra66"))) return; setPtext(txt); setPdirty(true); };
  const pickS = (txt) => { if (stext.trim() && !window.confirm(window.t("label.extra66"))) return; setStext(txt); setSdirty(true); };
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      {show('proposal') && <Card pad={18} title={window.t("extra.guidance.proposal")}
        action={editable ? <Button size="sm" variant="primary" icon="check" disabled={!dirty} onClick={() => { saveProposalGuidance(text.trim()); setDirty(false); }}>{t('btn.save')}</Button> : null}>
        <div style={{ fontSize: 12.5, color: '#7b828d', marginBottom: 12, lineHeight: 1.7 }}>{window.t("extra.guidance.proposalPrefix")}<b>{window.t("extra.guidance.proposalView")}</b>{window.t("extra.guidance.proposalSuffix")}<b>{window.t("extra.guidance.fixed")}</b>{window.t("extra.guidance.fixedHint")}</div>
        <GuidanceTemplates templates={PROPOSAL_GUIDANCE_TEMPLATES} onPick={pickP} disabled={!editable} />
        <textarea value={text} onChange={(e) => { setText(e.target.value); setDirty(true); }} disabled={!editable} rows={10}
          placeholder={window.t("extra.guidance.placeholder0")}
          style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.8, fontSize: 13.5 }} />
      </Card>}
      {show('quote') && <Card pad={18} title={window.t("extra.guidance.quote")}
        action={editable ? <Button size="sm" variant="primary" icon="check" disabled={!qdirty} onClick={() => { saveQuoteGuidance(qtext.trim()); setQdirty(false); }}>{t('btn.save')}</Button> : null}>
        <div style={{ fontSize: 12.5, color: '#7b828d', marginBottom: 12, lineHeight: 1.7 }}>{window.t("extra.guidance.quotePrefix")}<b>{window.t("extra.guidance.quoteView")}</b>{window.t("extra.guidance.quoteSuffix")}<b>{window.t("extra.guidance.fixed")}</b>{window.t("extra.guidance.structureHint")}</div>
        <GuidanceTemplates templates={QUOTE_GUIDANCE_TEMPLATES} onPick={pickQ} disabled={!editable} />
        <textarea value={qtext} onChange={(e) => { setQtext(e.target.value); setQdirty(true); }} disabled={!editable} rows={10}
          placeholder={window.t("extra.guidance.placeholder1")}
          style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.8, fontSize: 13.5 }} />
      </Card>}
      {show('prototype') && <Card pad={18} title={window.t("extra.guidance.prototype")}
        action={editable ? <Button size="sm" variant="primary" icon="check" disabled={!pdirty} onClick={() => { saveProtoGuidance(ptext.trim()); setPdirty(false); }}>{t('btn.save')}</Button> : null}>
        <div style={{ fontSize: 12.5, color: '#7b828d', marginBottom: 12, lineHeight: 1.7 }}>{window.t("extra.guidance.prototypePrefix")}<b>{window.t("extra.guidance.prototypeView")}</b>{window.t("extra.guidance.prototypeSuffix")}<b>{window.t("extra.guidance.fixed")}</b>{window.t("extra.guidance.structureHint")}</div>
        <GuidanceTemplates templates={PROTOTYPE_GUIDANCE_TEMPLATES} onPick={pickPr} disabled={!editable} />
        <textarea value={ptext} onChange={(e) => { setPtext(e.target.value); setPdirty(true); }} disabled={!editable} rows={10}
          placeholder={window.t("extra.guidance.placeholder2")}
          style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.8, fontSize: 13.5 }} />
      </Card>}
      {show('shodan') && <Card pad={18} title={window.t("extra.guidance.meeting")}
        action={editable ? <Button size="sm" variant="primary" icon="check" disabled={!sdirty} onClick={() => { saveShodanGuidance(stext.trim()); setSdirty(false); }}>{t('btn.save')}</Button> : null}>
        <div style={{ fontSize: 12.5, color: '#7b828d', marginBottom: 12, lineHeight: 1.7 }}>{window.t("extra.guidance.meetingPrefix")}<b>{window.t("extra.guidance.meetingView")}</b>{window.t("extra.guidance.meetingSuffix")}<b>{window.t("extra.guidance.fixed")}</b>{window.t("extra.guidance.structureHint")}</div>
        <GuidanceTemplates templates={SHODAN_GUIDANCE_TEMPLATES} onPick={pickS} disabled={!editable} />
        <textarea value={stext} onChange={(e) => { setStext(e.target.value); setSdirty(true); }} disabled={!editable} rows={10}
          placeholder={window.t("extra.guidance.placeholder3")}
          style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.8, fontSize: 13.5 }} />
      </Card>}
    </div>
  );
}

/* 種別ごとの入力フォーム（テキスト入力時）。構造化して入れた内容は本文に整形して保存＝AI検索でもそのまま使える。
   schema が無い種別（手順・素材系）は従来どおり単一の本文テキストエリア。titleSrc: タイトル未入力時の自動補完元。 */
const KB_FIELDS = {
  case: [
    { k: 'overview', get label(){return window.t("label.extra67");}, titleSrc: true },
    { k: 'category', get label(){return window.t("cd.category");}, cats: true }, // 実績と同様、案件と同じカテゴリで分類
    { k: 'challenge', get label(){return window.t("label.extra68");}, rows: 3 },
    { k: 'solution', get label(){return window.t("label.extra69");}, rows: 4 },
    { k: 'result', get label(){return window.t("label.extra70");}, rows: 3, required: true },
    { k: 'imageUrl', get label(){return window.t("label.extra71");}, get placeholder(){return window.t("label.extra72");} },
  ],
  faq: [
    { k: 'question', get label(){return window.t("faq.post.field.title");}, rows: 2, required: true, titleSrc: true },
    { k: 'answer', get label(){return window.t("label.extra73");}, rows: 7, required: true },
  ],
  minutes: [
    { k: 'date', get label(){return window.t("calendar.label.date");}, placeholder: '2026-06-16' },
    { k: 'attendees', get label(){return window.t("label.extra74");}, get placeholder(){return window.t("label.extra75");} },
    { k: 'body', get label(){return window.t("label.extra76");}, rows: 9, required: true, titleSrc: true },
  ],
  achievement: [
    { k: 'client', get label(){return window.t("extra.case.clientAndTitle");}, required: true, titleSrc: true },
    { k: 'customer', get label(){return window.t("label.extra77");}, get placeholder(){return window.t("label.extra78");} },
    { k: 'industry', get label(){return window.t("cust2.label.industry");} },
    { k: 'category', get label(){return window.t("cd.category");}, cats: true }, // 案件と同じカテゴリで分類（AIマッチ・検索が効きやすい）
    { k: 'kind', get label(){return window.t("cd.categoryPlaceholder");}, type: 'select', options: ['受託開発', '自社サービス', '共同開発', '保守・運用', 'PoC・コンサル', 'その他'] },
    { k: 'link', get label(){return window.t("label.extra79");}, get placeholder(){return window.t("label.extra80");} },
    { k: 'imageUrl', get label(){return window.t("label.extra71");}, get placeholder(){return window.t("label.extra81");} },
    { k: 'period', get label(){return window.t("extra.common.period");}, get placeholder(){return window.t("label.extra82");} },
    { k: 'body', get label(){return window.t("extra.case.contentScaleTech");}, rows: 5 },
    { k: 'result', get label(){return window.t("extra.case.results");}, rows: 3 },
  ],
};

/* ナレッジ追加モーダル */
function KnowledgeAddModal({ kb, onClose }) {
  const { addKnowledge, showToast, cases } = useStore();
  const [type, setType] = React.useState('text');
  const [caseId, setCaseId] = React.useState(''); // 社内ナレッジを案件に紐づける（任意）
  const [title, setTitle] = React.useState('');
  const [text, setText] = React.useState('');
  const [fields, setFields] = React.useState({}); // 種別別フォームの入力値（KB_FIELDS）
  const schema = KB_FIELDS[kb]; // この種別の構造化フォーム（無ければ単一本文）
  const [url, setUrl] = React.useState('');
  const [fileName, setFileName] = React.useState('');
  const [fileText, setFileText] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const fileRef = React.useRef(null);
  // URL：サイト内リンク探索＋複選
  const [links, setLinks] = React.useState(null);
  const [checked, setChecked] = React.useState({});
  const [discovering, setDiscovering] = React.useState(false);
  const [prog, setProg] = React.useState('');
  const selCount = (type === 'url' && links) ? links.filter(l => checked[l.url]).length : 0;

  const discover = async () => {
    if (!/^https?:\/\//.test(url.trim())) { setErr(t('kb.err.urlFormat')); return; }
    setErr(''); setDiscovering(true);
    try {
      const r = await API.discoverKnowledgeUrls(url.trim());
      setLinks(r.links || []);
      const ck = {}; (r.links || []).forEach((l, i) => { ck[l.url] = i === 0; }); // 先頭（このページ）だけ既定ON
      setChecked(ck);
    } catch (e) { setErr(e.message || t('kb.err.fetchLinks')); }
    setDiscovering(false);
  };

  const onFile = async (e) => {
    const f = e.target.files && e.target.files[0];
    e.target.value = '';
    if (!f) return;
    setErr('');
    try {
      const extracted = await extractFileText(f);
      if (!extracted) { setErr(t('kb.err.noTextExtracted')); return; }
      setFileName(f.name); setFileText(extracted);
      if (!title.trim()) setTitle(f.name.replace(/\.[^.]+$/, ''));
    } catch (e2) { setErr(e2.message || t('kb.err.fileRead')); }
  };

  const submit = async () => {
    if (busy) return;
    setErr('');
    // URL：サイト内リンクを複選した場合はバッチ取込（選択した各URLを個別にインデックス化）
    if (type === 'url' && selCount > 0) {
      const sel = links.filter(l => checked[l.url]);
      setBusy(true); let ok = 0;
      for (let i = 0; i < sel.length; i++) {
        setProg(t('kb.importingProgress', { i: i + 1, total: sel.length }));
        try { await addKnowledge({ kb, type: 'url', url: sel[i].url, title: (sel[i].text && sel[i].text[0] !== '（') ? sel[i].text : '', caseId: caseId || undefined }); ok++; } catch (_) {}
      }
      setProg(''); setBusy(false);
      const failed = sel.length - ok;
      if (ok) { showToast(failed > 0 ? t('kb.toast.urlsAddedPartial', { n: ok, f: failed }) : t('kb.toast.urlsAdded', { n: ok })); onClose(); } else setErr(t('kb.err.addFailed'));
      return;
    }
    const payload = { kb, title: title.trim(), type, caseId: caseId || undefined };
    if (type === 'text' && schema) {
      // 種別別フォーム：必須チェック→本文に整形（■見出し＋値）。タイトル未入力なら titleSrc から補完
      // フィールド値を文字列化（カテゴリ等の配列は「・」で連結）
      const fieldVal = (f) => { const r = fields[f.k]; return Array.isArray(r) ? r.join('・') : (r || '').trim(); };
      for (const f of schema) { if (f.required && !fieldVal(f)) { setErr(f.label + 'を入力してください'); return; } }
      // 画像URLは本文(RAG)に混ぜず、メタとして渡してカードでサムネイル表示する
      const imgUrl = (fields.imageUrl || '').trim();
      if (imgUrl) payload.imageUrl = imgUrl;
      if (Array.isArray(fields.category) && fields.category.length) payload.categories = fields.category; // 表示・絞り込み用に構造化保存（本文にも残してRAGに効かせる）
      const composed = schema.filter(f => f.k !== 'imageUrl' && fieldVal(f)).map(f => `■${f.label}\n${fieldVal(f)}`).join('\n\n');
      if (!composed.trim()) { setErr('内容を入力してください'); return; }
      payload.text = composed;
      if (!payload.title) { const ts = schema.find(f => f.titleSrc); const v = ts && (fields[ts.k] || '').trim(); if (v) payload.title = v.replace(/\s+/g, ' ').slice(0, 40); }
    }
    else if (type === 'text') { if (!text.trim()) { setErr(t('kb.err.bodyRequired')); return; } payload.text = text; }
    else if (type === 'url') { if (!/^https?:\/\//.test(url.trim())) { setErr(t('kb.err.urlFormat')); return; } payload.url = url.trim(); }
    else { if (!fileText) { setErr(t('kb.err.fileRequired')); return; } payload.type = 'doc'; payload.text = fileText; }
    setBusy(true);
    try { await addKnowledge(payload); onClose(); }
    catch (e) { setErr(e.message || t('kb.err.addFailed')); setBusy(false); }
  };

  const TypeBtn = ({ v, icon, label }) => (
    /* label はここでは固定の表示文字（種別ボタン） */
    <button onClick={() => setType(v)} style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 7, padding: '9px 0', borderRadius: 9,
      border: '1px solid ' + (type === v ? '#4a5af0' : '#e2e5ea'), background: type === v ? '#eef0fe' : '#fff', color: type === v ? '#4a5af0' : '#5b626d',
      cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 600 }}>
      <Icon name={icon} size={15} stroke={2} />{label}
    </button>
  );

  return (
    <Modal open onClose={onClose} width={560}
      title={t('kb.addModal.title', { kb: kbLabel(KB_TABS.find(t => t.key === kb).label) })}
      subtitle={t('kb.addModal.subtitle')}
      footer={<><Button variant="subtle" onClick={onClose}>{t('btn.cancel')}</Button>
        <Button variant="primary" icon="plus" onClick={submit} disabled={busy}>{busy ? (prog || t('kb.importing')) : (selCount > 0 ? t('kb.addSelected', { n: selCount }) : t('kb.addBtn'))}</Button></>}>
      <input ref={fileRef} type="file" accept=".txt,.md,.csv,.json,.pdf,text/*,application/pdf" onChange={onFile} style={{ display: 'none' }} />
      <Field label={t('kb.field.type')}>
        <div style={{ display: 'flex', gap: 8 }}>
          <TypeBtn v="text" icon="edit" label={t('kb.type.text')} />
          <TypeBtn v="url" icon="link" label={t('kb.type.url')} />
          <TypeBtn v="file" icon="attach" label={t('kb.type.file')} />
        </div>
      </Field>
      <Field label={t('kb.field.title')} hint={t('kb.field.title.hint')}><TextInput value={title} onChange={(e) => setTitle(e.target.value)} placeholder={t('kb.field.title.placeholder')} /></Field>
      {isInternalKB(kb) && (
        <Field label={window.t("extra.knowledge.linkCase")} hint={window.t("extra.knowledge.linkHint")}>
          <select value={caseId} onChange={(e) => setCaseId(e.target.value)} style={{ ...inputStyle, appearance: 'none', WebkitAppearance: 'none', cursor: 'pointer' }}>
            <option value="">{window.t("extra.knowledge.noLink")}</option>
            {(cases || []).slice().sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || '')).map(c => <option key={c.id} value={c.id}>{c.title}</option>)}
          </select>
        </Field>
      )}
      {type === 'text' && schema && schema.map(f => (
        <Field key={f.k} label={f.label} required={f.required}>
          {f.cats
            ? <CategoryTags value={Array.isArray(fields[f.k]) ? fields[f.k] : []} onChange={(v) => setFields(s => ({ ...s, [f.k]: v }))} editable options={window.APP_DATA.CATEGORIES} />
            : f.type === 'select'
            ? <select value={fields[f.k] || ''} onChange={(e) => setFields(s => ({ ...s, [f.k]: e.target.value }))} style={{ ...inputStyle, appearance: 'none', WebkitAppearance: 'none', cursor: 'pointer' }}>
                <option value="">{window.t("cust2.selectDefault")}</option>
                {(f.options || []).map(o => <option key={o} value={o}>{o}</option>)}
              </select>
            : f.rows && f.rows > 1
            ? <textarea value={fields[f.k] || ''} onChange={(e) => setFields(s => ({ ...s, [f.k]: e.target.value }))} rows={f.rows} placeholder={f.placeholder || ''}
                style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.6 }} />
            : <TextInput value={fields[f.k] || ''} onChange={(e) => setFields(s => ({ ...s, [f.k]: e.target.value }))} placeholder={f.placeholder || ''} />}
        </Field>
      ))}
      {type === 'text' && !schema && (
        <Field label={t('kb.field.body')} required>
          <textarea value={text} onChange={(e) => setText(e.target.value)} rows={9} placeholder={t('kb.field.body.placeholder')}
            style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.6 }} />
        </Field>
      )}
      {type === 'url' && (
        <Field label={t('kb.field.url')} required hint={t('kb.field.url.hint')}>
          <div style={{ display: 'flex', gap: 8 }}>
            <div style={{ flex: 1 }}><TextInput value={url} onChange={(e) => { setUrl(e.target.value); setLinks(null); }} placeholder="https://alion.jp/" /></div>
            <Button variant="default" icon="search" onClick={discover} disabled={discovering || !url.trim()}>{discovering ? t('kb.discovering') : t('kb.discoverLinks')}</Button>
          </div>
          {links && (
            <div style={{ marginTop: 10, border: '1px solid #e6e8ec', borderRadius: 10, overflow: 'hidden' }}>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 12px', background: '#f6f7f9', fontSize: 12, color: '#5b626d', fontWeight: 600 }}>
                <span>{t('kb.sameDomain', { total: links.length, sel: selCount })}</span>
                <span>
                  <button onClick={() => { const ck = {}; links.forEach(l => (ck[l.url] = true)); setChecked(ck); }} style={{ ...linkBtn, marginRight: 12 }}>{t('kb.selectAll')}</button>
                  <button onClick={() => setChecked({})} style={linkBtn}>{t('kb.deselectAll')}</button>
                </span>
              </div>
              <div style={{ maxHeight: 240, overflowY: 'auto' }}>
                {links.map(l => (
                  <label key={l.url} style={{ display: 'flex', alignItems: 'flex-start', gap: 9, padding: '8px 12px', borderTop: '1px solid #f4f5f7', cursor: 'pointer' }}>
                    <input type="checkbox" checked={!!checked[l.url]} onChange={(e) => setChecked(c => ({ ...c, [l.url]: e.target.checked }))} style={{ marginTop: 3, accentColor: '#4a5af0', flex: '0 0 auto' }} />
                    <span style={{ minWidth: 0, flex: 1 }}>
                      {l.text && <div style={{ fontSize: 12.5, color: '#2b2f38', fontWeight: 500 }}>{l.text}</div>}
                      <div style={{ fontSize: 11, color: '#9aa1ab', fontFamily: 'var(--mono)', wordBreak: 'break-all' }}>{l.url}</div>
                    </span>
                  </label>
                ))}
                {links.length === 0 && <div style={{ padding: 16, textAlign: 'center', color: '#9aa1ab', fontSize: 12.5 }}>{t('kb.noSameDomainLinks')}</div>}
              </div>
            </div>
          )}
          {prog && <div style={{ fontSize: 12, color: '#4a5af0', marginTop: 8 }}>{prog}</div>}
        </Field>
      )}
      {type === 'file' && (
        <Field label={t('kb.type.file')} required hint={t('kb.field.file.hint')}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <Button variant="default" icon="attach" onClick={() => fileRef.current && fileRef.current.click()}>{t('kb.selectFile')}</Button>
            {fileName && <span style={{ fontSize: 12.5, color: '#5b626d' }}>{fileName} <span style={{ color: '#9aa1ab' }}>{t('kb.charCount', { n: fileText.length.toLocaleString() })}</span></span>}
          </div>
        </Field>
      )}
      {err && <div style={{ fontSize: 12.5, color: '#dc2626', marginTop: 4 }}>{err}</div>}
    </Modal>
  );
}

/* 見積書ファイル→base64（data:接頭辞は除く）。機能マスタの取込で使用 */
function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const r = new FileReader();
    r.onload = () => resolve(String(r.result || '').replace(/^data:[^,]*,/, ''));
    r.onerror = () => reject(new Error('ファイルの読み込みに失敗しました'));
    r.readAsDataURL(file);
  });
}

/* ナレッジカードのプレビュー整形：構造化フォーム由来の「■ラベル\n値」本文を、
   短い項目（業種・種別・期間・リンク等）＝ラベル付きメタチップ／長い本文＝2行省略 に分けて表示。
   ■が無い普通のテキストは従来どおり2行省略。カテゴリ項目はカード上部のチップと重複するため省く */
function KbPreview({ text }) {
  const plain = { fontSize: 12, color: '#7b828d', marginTop: 4, lineHeight: 1.6, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' };
  const s = String(text || '');
  if (s.indexOf('■') < 0) return <div style={plain}>{s}</div>;
  const fields = s.split('■').map(p => p.trim()).filter(Boolean).map(p => {
    const nl = p.indexOf('\n');
    if (nl < 0) { const sp = p.indexOf(' '); return sp < 0 ? { label: p, value: '' } : { label: p.slice(0, sp), value: p.slice(sp + 1).trim() }; }
    return { label: p.slice(0, nl).trim(), value: p.slice(nl + 1).replace(/\s+/g, ' ').trim() };
  }).filter(f => f.label && f.value);
  if (!fields.length) return <div style={plain}>{s}</div>;
  const meta = fields.filter(f => f.label !== 'カテゴリ' && f.value.length <= 46).slice(0, 4);
  const long = fields.filter(f => f.value.length > 46);
  return (
    <div style={{ marginTop: 5 }}>
      {meta.length > 0 && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: long.length ? 5 : 0 }}>
          {meta.map(f => (
            <span key={f.label} style={{ display: 'inline-flex', alignItems: 'baseline', gap: 5, fontSize: 11, background: '#f6f7f9', border: '1px solid #eef0f3', borderRadius: 6, padding: '2px 8px', maxWidth: 340 }}>
              <span style={{ color: '#9aa1ab', fontWeight: 700, whiteSpace: 'nowrap' }}>{f.label}</span>
              <span style={{ color: '#5b626d', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{f.value}</span>
            </span>
          ))}
        </div>
      )}
      {long.length > 0 && <div style={{ ...plain, marginTop: 0 }}>{long.map(f => f.value).join(' ／ ')}</div>}
    </div>
  );
}

/* 機能マスタの1行（インライン編集。countが変わると key で再マウントし、取込後の平均単価を反映） */
function FeatureMasterRow({ row, editable, onSave, onRemove }) {
  const [name, setName] = React.useState(row.name);
  const [price, setPrice] = React.useState(row.unitPrice);
  const [unit, setUnit] = React.useState(row.unit || '式');
  const [cat, setCat] = React.useState(row.category || '');
  const inp = { border: '1px solid #e6e8ec', borderRadius: 7, padding: '5px 8px', fontSize: 12.5, fontFamily: 'inherit', width: '100%', boxSizing: 'border-box', background: editable ? '#fff' : '#f6f7f9', color: '#1f2430', outline: 'none' };
  const blur = (val, key, orig) => { if (val !== orig) onSave({ [key]: val }); };
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 120px 70px 130px 56px 32px', gap: 8, alignItems: 'center', padding: '8px 14px', borderBottom: '1px solid #f4f5f7' }}>
      <div style={{ minWidth: 0 }}>
        <input style={inp} value={name} disabled={!editable} onChange={e => setName(e.target.value)} onBlur={() => blur(name.trim(), 'name', row.name)} />
        {row.sources && row.sources.length > 0 && (() => { const qs = [...new Set(row.sources.map(s => s.q).filter(Boolean))]; return (
          <div title={row.sources.map(s => (s.q || '?') + (s.price ? '  ¥' + Number(s.price).toLocaleString() : '') + (s.at ? '  ' + s.at : '')).join('\n')}
            style={{ fontSize: 10.5, color: '#aab0ba', marginTop: 3, display: 'flex', alignItems: 'center', gap: 4, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
            <Icon name="attach" size={9} stroke={2} style={{ flex: '0 0 auto' }} /><span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{window.t("extra.knowledge.source")}{qs.length}{window.t("extra.knowledge.countPrefix")}{qs.join('、')}</span>
          </div>
        ); })()}
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 3 }}>
        <span style={{ fontSize: 12, color: '#9aa1ab' }}>¥</span>
        <input style={{ ...inp, textAlign: 'right' }} type="number" value={price} disabled={!editable}
          onChange={e => setPrice(e.target.value)} onBlur={() => blur(Math.round(Number(price) || 0), 'unitPrice', row.unitPrice)} />
      </div>
      <input style={inp} value={unit} disabled={!editable} onChange={e => setUnit(e.target.value)} onBlur={() => blur(unit.trim() || '式', 'unit', row.unit)} />
      <input style={inp} value={cat} list="fm-cats" disabled={!editable} placeholder="—" onChange={e => setCat(e.target.value)} onBlur={() => blur(cat.trim(), 'category', row.category || '')} />
      <span style={{ fontSize: 11, fontWeight: 700, color: '#4a5af0', background: '#eef0fe', padding: '2px 0', borderRadius: 999, textAlign: 'center' }} title={window.t("extra.catalog.frequencyHint")}>{row.count || 1}{window.t("extra.unit.times")}</span>
      {editable ? <IconButton name="x" size={14} title={window.t("btn.delete")} onClick={onRemove} /> : <span />}
    </div>
  );
}

/* 機能マスタ（料金カタログ）：見積書をアップ→AIが機能ごとの単価を抽出→同名は平均単価でまとめて会社の資産に。
   見積書タブの「マスタから追加」で参照される。追加・編集は管理者（manageKnowledge）。 */
/* 見積書の備考テンプレート管理（ナレッジ→見積もり用）。masters.config.quoteNoteTemplates に保存し、
   見積書画面の備考「テンプレートから挿入」で選べる。追加・編集・削除は manageKnowledge 権限 */
function QuoteNoteTemplatesPanel() {
  const { can, showToast } = useStore();
  const D = window.APP_DATA;
  const editable = can('manageKnowledge');
  const [list, setList] = React.useState(() => (D.quoteNoteTemplates || []).slice());
  const [editing, setEditing] = React.useState(null);   // {id?, name, text}（新規はid無し）
  const persist = (next) => {
    setList(next);
    D.quoteNoteTemplates = next;
    API.post('masters', { id: 'config', quoteNoteTemplates: next });   // jsonbマージ＝サーバ無改修
  };
  const save = () => {
    const name = String(editing.name || '').trim(), text = String(editing.text || '').trim();
    if (!name || !text) { showToast(t('kb.qnt.needBoth'), 'x'); return; }
    const row = { id: editing.id || ('qnt' + Date.now()), name: name.slice(0, 40), text: text.slice(0, 2000) };
    persist(editing.id ? list.map(x => x.id === row.id ? row : x) : [...list, row]);
    setEditing(null); showToast(t('kb.qnt.saved'));
  };
  const remove = (row) => { if (window.confirm(t('kb.qnt.delConfirm', { name: row.name }))) persist(list.filter(x => x.id !== row.id)); };
  const inp = { border: '1px solid #e6e8ec', borderRadius: 8, padding: '7px 10px', fontSize: 12.5, fontFamily: 'inherit', width: '100%', boxSizing: 'border-box', outline: 'none', color: '#1f2430', background: '#fff' };
  return (
    <Card>
      {list.length === 0 && !editing && (
        <div style={{ fontSize: 12.5, color: '#9aa1ab', lineHeight: 1.8, padding: '6px 0 10px' }}>{t('kb.qnt.empty')}</div>
      )}
      {list.map(row => (
        <div key={row.id} style={{ display: 'flex', gap: 10, alignItems: 'flex-start', padding: '10px 0', borderBottom: '1px solid #f2f3f6' }}>
          <div style={{ minWidth: 0, flex: 1 }}>
            <div style={{ fontSize: 13, fontWeight: 700, color: '#1f2430' }}>{row.name}</div>
            <div style={{ fontSize: 12, color: '#7b828d', whiteSpace: 'pre-wrap', marginTop: 3, lineHeight: 1.7 }}>{row.text.length > 160 ? row.text.slice(0, 160) + '…' : row.text}</div>
          </div>
          {editable && (
            <span style={{ flex: '0 0 auto', display: 'flex', gap: 6 }}>
              <Button size="sm" variant="default" onClick={() => setEditing({ ...row })}>{t('btn.edit')}</Button>
              <Button size="sm" variant="default" onClick={() => remove(row)}>{t('btn.delete')}</Button>
            </span>
          )}
        </div>
      ))}
      {editing && (
        <div style={{ background: '#f8f9fb', border: '1px solid #e8eaee', borderRadius: 10, padding: 12, marginTop: 10, display: 'flex', flexDirection: 'column', gap: 8 }}>
          <input style={inp} value={editing.name} placeholder={t('kb.qnt.namePh')} onChange={e => setEditing(x => ({ ...x, name: e.target.value }))} />
          <textarea style={{ ...inp, resize: 'vertical' }} rows={5} value={editing.text} placeholder={t('kb.qnt.textPh')} onChange={e => setEditing(x => ({ ...x, text: e.target.value }))} />
          <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
            <Button size="sm" variant="default" onClick={() => setEditing(null)}>{t('btn.cancel')}</Button>
            <Button size="sm" variant="primary" onClick={save}>{t('btn.save')}</Button>
          </div>
        </div>
      )}
      {editable && !editing && (
        <div style={{ marginTop: 10 }}>
          <Button size="sm" variant="primary" icon="plus" onClick={() => setEditing({ name: '', text: '' })}>{t('kb.qnt.add')}</Button>
        </div>
      )}
    </Card>
  );
}

function FeatureMasterPanel() {
  const { mergeFeatureMaster, updateFeatureRow, addFeatureRow, removeFeatureRow, can, showToast, addKnowledge, cases } = useStore();
  const D = window.APP_DATA;
  const editable = can('manageKnowledge');
  const [busy, setBusy] = React.useState(false);
  const [q, setQ] = React.useState('');
  const [review, setReview] = React.useState(null); // 抽出結果の確認モーダル { items:[{_id,_include,_src,name,unit,unitPrice,taxRate,category}], errs:[] }
  const fileRef = React.useRef(null);
  const all = D.featureMaster || [];
  const fmNormLocal = (s) => String(s || '').trim().toLowerCase().replace(/\s+/g, ''); // 新規/更新バッジ用（store の fmNorm と同ロジック）
  const existingNames = new Set(all.map(f => fmNormLocal(f.name)));
  const rows = all.slice()
    .filter(r => !q || (r.name || '').includes(q) || (r.category || '').includes(q))
    .sort((a, b) => (b.count || 0) - (a.count || 0) || String(a.name).localeCompare(String(b.name)));

  // 見積書をアップ → AIが機能を抽出 → 即マージせず「確認モーダル」を開く（1機能ずつ見て取捨選択できる）
  const onFiles = async (files) => {
    const list = Array.from(files || []);
    if (!list.length) return;
    setBusy(true);
    const collected = []; const materials = []; const errs = [];
    for (const file of list) {
      try {
        const content = await fileToBase64(file);
        const res = await API.quoteExtract({ name: file.name, mime: file.type, content });
        (res.items || []).forEach((it, i) => collected.push({
          _id: file.name + ':' + i + ':' + Math.random().toString(36).slice(2, 6),
          _include: true, _src: file.name,
          name: String(it.name || '').trim(), unit: it.unit || '式',
          unitPrice: Math.round(Number(it.unitPrice) || 0),
          taxRate: [10, 8, 0].includes(Number(it.taxRate)) ? Number(it.taxRate) : 10, category: it.category || '',
        }));
        // 統合：同じ見積書を「見積もり用」の素材にも保存できるよう本文テキストも抽出（PDFはpdf.js／画像は空＝素材保存スキップ）
        let text = ''; try { text = await extractFileText(file); } catch (_) {}
        materials.push({ name: file.name, text: text || '' });
      } catch (e) { errs.push(file.name + '：' + (e.message || '読み取り失敗')); }
    }
    setBusy(false);
    if (fileRef.current) fileRef.current.value = '';
    if (!collected.length) { showToast(errs.length ? errs.join(' / ') : '金額のある機能が読み取れませんでした（見積書のPDF/画像をご確認ください）', 'x'); return; }
    if (errs.length) showToast(errs.join(' / '), 'x');
    setReview({ items: collected, materials, errs, saveMaterial: materials.some(m => m.text) });
  };
  const patchReview = (id, patch) => setReview(rv => rv ? { ...rv, items: rv.items.map(it => (it._id === id ? { ...it, ...patch } : it)) } : rv);
  const setAllInclude = (v) => setReview(rv => rv ? { ...rv, items: rv.items.map(it => ({ ...it, _include: v })) } : rv);
  const confirmReview = async () => {
    const sel = (review.items || []).filter(it => it._include && it.name.trim() && Number(it.unitPrice) > 0)
      .map(it => ({ name: it.name.trim(), unit: (it.unit || '式').trim() || '式', unitPrice: Math.round(Number(it.unitPrice) || 0), taxRate: it.taxRate, category: (it.category || '').trim(), source: it._src }));
    if (!sel.length) { showToast(window.t("label.extra83"), 'x'); return; }
    const r = mergeFeatureMaster(sel);
    // 統合：チェックが付いていれば同じ見積書を「見積もり用」の素材(RAG)にも保存
    let savedMat = 0;
    if (review.saveMaterial) {
      for (const m of (review.materials || [])) {
        if (!m.text) continue;
        try { await addKnowledge({ kb: 'quote', type: 'doc', title: (m.name || '見積書').replace(/\.[^.]+$/, ''), text: m.text }); savedMat++; } catch (_) {}
      }
    }
    setReview(null);
    showToast('機能マスタに ' + r.added + '件追加・' + r.updated + '件更新' + (savedMat ? '／見積もり用に' + savedMat + '件保存' : '') + 'しました');
  };
  // 案件内で作った見積書（quoteSheet.items＝構造化済み）から一括収集→確認モーダルへ。AI不要で正確、出典＝案件名
  const importFromCaseQuotes = () => {
    const collected = [];
    (cases || []).forEach(c => {
      const qs = c.quoteSheet; const items = (qs && qs.items) || [];
      const src = (qs && String(qs.subject || '').trim()) || c.title || '案件見積書';
      items.forEach((it, i) => {
        if (it.type === 'text') return;
        const name = String(it.name || '').trim(); const price = Math.round(Number(it.unitPrice) || 0);
        if (!name || price <= 0) return;
        collected.push({ _id: c.id + ':' + i + ':' + Math.random().toString(36).slice(2, 6), _include: true, _src: src,
          name, unit: it.unit || '式', unitPrice: price, taxRate: [10, 8, 0].includes(Number(it.taxRate)) ? Number(it.taxRate) : 10, category: '' });
      });
    });
    if (!collected.length) { showToast(window.t("label.extra84"), 'x'); return; }
    setReview({ items: collected, materials: [], errs: [], saveMaterial: false, fromCases: true });
  };

  return (
    <div>
      <datalist id="fm-cats">{(D.CATEGORIES || []).map(c => <option key={c} value={c} />)}</datalist>
      <Card pad={16} style={{ marginBottom: 12 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
          <input ref={fileRef} type="file" accept=".pdf,image/*" multiple style={{ display: 'none' }} onChange={e => onFiles(e.target.files)} />
          <Button variant="primary" icon="attach" disabled={!editable || busy} onClick={() => fileRef.current && fileRef.current.click()}>
            {busy ? window.t("extra.catalog.reading") : window.t("extra.catalog.import")}
          </Button>
          {editable && <Button variant="default" icon="cases" disabled={busy} onClick={importFromCaseQuotes} title={window.t("extra.catalog.importCasesHint")}>{window.t("extra.catalog.importCases")}</Button>}
          {editable && <Button variant="default" icon="plus" disabled={busy} onClick={() => addFeatureRow({})}>{window.t("extra.catalog.addRow")}</Button>}
          <div style={{ flex: 1 }} />
          <div style={{ display: 'flex', alignItems: 'center', gap: 7, background: '#fff', border: '1px solid #e2e5ea', borderRadius: 8, padding: '6px 10px', width: 220 }}>
            <Icon name="search" size={14} stroke={2} style={{ color: '#9aa1ab' }} />
            <input value={q} onChange={e => setQ(e.target.value)} placeholder={window.t("extra.catalog.search")} style={{ border: 'none', outline: 'none', fontSize: 12.5, width: '100%', fontFamily: 'inherit', background: 'transparent' }} />
          </div>
        </div>
        <div style={{ fontSize: 11.5, color: '#9aa1ab', marginTop: 9, lineHeight: 1.6 }}>{window.t("extra.catalog.hintPrefix")}<b style={{ color: '#4a5af0' }}>{window.t("extra.catalog.average")}</b>{window.t("extra.catalog.hintSuffix")}</div>
      </Card>
      <Card pad={0}>
       {/* スマホ：固定列(計408px+1fr)は幅390で溢れるため、表全体を横スクロール枠に入れる */}
       <div style={{ overflowX: 'auto' }}>
        <div style={{ minWidth: 640 }}>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 120px 70px 130px 56px 32px', gap: 8, padding: '10px 14px', borderBottom: '1px solid #f0f1f4', fontSize: 11.5, fontWeight: 700, color: '#9aa1ab' }}>
          <div>{window.t("extra.catalog.name")}</div><div style={{ textAlign: 'right' }}>{window.t("extra.catalog.price")}</div><div>{window.t("cd.qs.colUnit")}</div><div>{window.t("cd.category")}</div><div style={{ textAlign: 'center' }}>{window.t("extra.catalog.frequency")}</div><div />
        </div>
        {rows.length === 0 && (
          <div style={{ padding: '44px 0', textAlign: 'center', color: '#9aa1ab', fontSize: 13 }}>
            {all.length === 0 ? window.t("extra.catalog.empty") : window.t("extra.catalog.noMatches")}
          </div>
        )}
        {rows.map(r => (
          <FeatureMasterRow key={r.id + ':' + (r.count || 0)} row={r} editable={editable}
            onSave={patch => updateFeatureRow(r.id, patch)} onRemove={() => removeFeatureRow(r.id)} />
        ))}
        {all.length > 0 && (
          <div style={{ padding: '10px 14px', fontSize: 11.5, color: '#9aa1ab', borderTop: '1px solid #f4f5f7' }}>
            {all.length}{window.t("settings.colFeature")}{q ? '（' + rows.length + window.t("extra.catalog.shownSuffix") : ''}
          </div>
        )}
        </div>
       </div>
      </Card>

      {/* 抽出結果の確認モーダル：AIが読み取った機能を1つずつ確認・修正・取捨選択してから機能マスタへ */}
      {review && (() => {
        const rvInp = { border: '1px solid #e6e8ec', borderRadius: 7, padding: '5px 8px', fontSize: 12.5, fontFamily: 'inherit', width: '100%', boxSizing: 'border-box', outline: 'none', color: '#1f2430', background: '#fff' };
        const linkBtn = { border: 'none', background: 'transparent', color: '#4a5af0', fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit', fontSize: 12, padding: 0 };
        const selCount = review.items.filter(it => it._include).length;
        return (
          <Modal open onClose={() => setReview(null)} width={720}
            title={window.t("extra.catalog.review")}
            subtitle={(review.fromCases ? (""+window.t("extra.catalog.fromCases")+" ") + review.items.length + (" "+window.t("extra.catalog.rowsCollected")+"") : (""+window.t("extra.catalog.fromQuote")+" ") + review.items.length + (" "+window.t("extra.catalog.featuresRead")+"")) + window.t("extra.catalog.reviewHint")}
            footer={<>
              <Button variant="subtle" onClick={() => setReview(null)}>{window.t("btn.cancel")}</Button>
              <Button variant="primary" icon="check" disabled={!editable || selCount === 0} onClick={confirmReview}>{window.t("extra.catalog.selectedPrefix")}{selCount}{window.t("extra.catalog.addSelectedSuffix")}</Button>
            </>}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8, fontSize: 12 }}>
              <button onClick={() => setAllInclude(true)} style={linkBtn}>{window.t("extra.catalog.checkAll")}</button>
              <button onClick={() => setAllInclude(false)} style={linkBtn}>{window.t("extra.catalog.uncheckAll")}</button>
              <div style={{ flex: 1 }} />
              <span style={{ color: '#9aa1ab' }}>{selCount}/{review.items.length}{window.t("extra.catalog.selected")}</span>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: '26px 1fr 118px 64px 78px', gap: 8, padding: '0 4px 6px', fontSize: 11, fontWeight: 700, color: '#9aa1ab' }}>
              <div /><div>{window.t("extra.catalog.name")}</div><div style={{ textAlign: 'right' }}>{window.t("extra.catalog.unitPrice")}</div><div>{window.t("cd.qs.colUnit")}</div><div style={{ textAlign: 'center' }}>{window.t("apo.statusFilter")}</div>
            </div>
            <div style={{ maxHeight: '50vh', overflowY: 'auto' }}>
              {review.items.map(it => { const isUpdate = existingNames.has(fmNormLocal(it.name)); return (
                <div key={it._id} style={{ display: 'grid', gridTemplateColumns: '26px 1fr 118px 64px 78px', gap: 8, alignItems: 'center', padding: '7px 4px', borderTop: '1px solid #f4f5f7', opacity: it._include ? 1 : 0.45 }}>
                  <input type="checkbox" checked={it._include} onChange={e => patchReview(it._id, { _include: e.target.checked })} style={{ width: 16, height: 16, cursor: 'pointer', accentColor: '#4a5af0' }} />
                  <input value={it.name} disabled={!editable} onChange={e => patchReview(it._id, { name: e.target.value })} style={rvInp} />
                  <div style={{ display: 'flex', alignItems: 'center', gap: 3 }}><span style={{ fontSize: 12, color: '#9aa1ab' }}>¥</span><input type="number" value={it.unitPrice} disabled={!editable} onChange={e => patchReview(it._id, { unitPrice: e.target.value })} style={{ ...rvInp, textAlign: 'right' }} /></div>
                  <input value={it.unit} disabled={!editable} onChange={e => patchReview(it._id, { unit: e.target.value })} style={rvInp} />
                  <span style={{ fontSize: 11, fontWeight: 700, color: isUpdate ? '#b45309' : '#15803d', background: isUpdate ? '#fdf0db' : '#e3f5e9', padding: '3px 0', borderRadius: 999, textAlign: 'center', whiteSpace: 'nowrap' }}>{isUpdate ? window.t("cust2.updated") : window.t("extra.catalog.new")}</span>
                </div>
              ); })}
            </div>
            {(review.materials || []).some(m => m.text) && (
              <label style={{ display: 'flex', alignItems: 'center', gap: 9, marginTop: 12, padding: '10px 12px', borderRadius: 9, border: '1px solid ' + (review.saveMaterial ? '#c7c4f5' : '#e2e5ea'), background: review.saveMaterial ? '#f6f6fe' : '#fff', cursor: 'pointer' }}>
                <input type="checkbox" checked={!!review.saveMaterial} onChange={e => setReview(rv => ({ ...rv, saveMaterial: e.target.checked }))} style={{ width: 16, height: 16, cursor: 'pointer', accentColor: '#4a5af0', flex: '0 0 auto' }} />
                <div style={{ flex: 1 }}>
                  <div style={{ fontSize: 12.5, fontWeight: 600, color: '#2b2f38' }}>{window.t("extra.catalog.saveSource")}</div>
                  <div style={{ fontSize: 11.5, color: '#9aa1ab', marginTop: 1 }}>{window.t("extra.catalog.saveSourceHint")}</div>
                </div>
              </label>
            )}
            <div style={{ fontSize: 11.5, color: '#9aa1ab', marginTop: 10, lineHeight: 1.6 }}>
              <b style={{ color: '#15803d' }}>{window.t("extra.catalog.new")}</b>{window.t("extra.catalog.newHint")}<b style={{ color: '#b45309' }}>{window.t("cust2.updated")}</b>{window.t("extra.catalog.existingHint")}<b>{window.t("extra.catalog.average")}</b>{window.t("extra.catalog.sourceHint")}</div>
          </Modal>
        );
      })()}
    </div>
  );
}

/* ナレッジ閲覧モーダル：カードクリックで全文を表示。■構造の本文は項目ごとに整形（ラベル見出し＋本文、
   URLはリンク化）、普通のテキストはそのまま全文表示。本文はAPIから全文取得（一覧のpreviewは先頭の抜粋のみのため） */
function KnowledgeViewModal({ item, canEdit, onEdit, onClose }) {
  const [text, setText] = React.useState('');
  const [loading, setLoading] = React.useState(true);
  React.useEffect(() => {
    let alive = true;
    API.knowledgeText(item.id)
      .then(r => { if (alive) { setText((r && r.text) || item.preview || ''); setLoading(false); } })
      .catch(() => { if (alive) { setText(item.preview || ''); setLoading(false); } });
    return () => { alive = false; };
  }, [item.id]);
  const isUrl = (v) => /^https?:\/\/\S+$/.test(String(v || '').trim());
  const fields = (() => {
    const s = String(text || '');
    if (s.indexOf('■') < 0) return null;
    const list = s.split('■').map(p => p.trim()).filter(Boolean).map(p => {
      const nl = p.indexOf('\n');
      if (nl < 0) { const sp = p.indexOf(' '); return sp < 0 ? { label: p, value: '' } : { label: p.slice(0, sp), value: p.slice(sp + 1).trim() }; }
      return { label: p.slice(0, nl).trim(), value: p.slice(nl + 1).trim() };
    }).filter(f => f.label && f.value);
    return list.length ? list : null;
  })();
  return (
    <Modal open onClose={onClose} width={640} title={item.title}
      footer={<>{canEdit && <Button variant="default" icon="edit" onClick={onEdit}>{t('btn.edit')}</Button>}<Button variant="primary" onClick={onClose}>{t('btn.close')}</Button></>}>
      {(item.categories || []).length > 0 && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: 10 }}>
          {item.categories.map(c => <span key={c} style={{ fontSize: 11, fontWeight: 600, color: '#4a5af0', background: '#eef0fe', border: '1px solid #e3e2fb', borderRadius: 999, padding: '2px 10px' }}>{c}</span>)}
        </div>
      )}
      {loading ? (
        <div style={{ padding: '30px 0', textAlign: 'center', color: '#9aa1ab', fontSize: 13 }}>{window.t("de.loading")}</div>
      ) : fields ? (
        <div>
          {fields.map(f => (
            <div key={f.label} style={{ padding: '10px 2px', borderBottom: '1px solid #f4f5f7' }}>
              <div style={{ fontSize: 11.5, fontWeight: 700, color: '#9aa1ab', marginBottom: 4 }}>{f.label}</div>
              {isUrl(f.value)
                ? <a href={f.value} target="_blank" rel="noreferrer" style={{ fontSize: 13, color: '#4a5af0', wordBreak: 'break-all' }}>{f.value}</a>
                : <div style={{ fontSize: 13.5, color: '#2b2f38', lineHeight: 1.8, whiteSpace: 'pre-wrap' }}>{f.value}</div>}
            </div>
          ))}
        </div>
      ) : (
        <div style={{ fontSize: 13.5, color: '#2b2f38', lineHeight: 1.85, whiteSpace: 'pre-wrap' }}>{text}</div>
      )}
      <div style={{ fontSize: 11.5, color: '#aab0ba', marginTop: 12 }}>{t('kb.charCountPlain', { n: (item.textLen || 0).toLocaleString() })} · {fmtDateTime(item.createdAt)}</div>
    </Modal>
  );
}

/* ナレッジ編集モーダル（テキスト種別）：タイトル＋本文を書き換える。個人KB（メール作成メモ）の
   署名・口調を本人が直接直すための導線。本文は全文をAPIから取得してプリフィル。 */
function KnowledgeEditModal({ item, onClose }) {
  const { updateKnowledge, showToast } = useStore();
  const [title, setTitle] = React.useState(item.title || '');
  const [text, setText] = React.useState('');
  const [loading, setLoading] = React.useState(true);
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  React.useEffect(() => {
    let alive = true;
    API.knowledgeText(item.id)
      .then(r => { if (alive) { setText(r.text || item.preview || ''); setLoading(false); } })
      .catch(() => { if (alive) { setText(item.preview || ''); setLoading(false); } });
    return () => { alive = false; };
  }, [item.id]);
  const save = async () => {
    if (busy) return;
    if (!text.trim()) { setErr(t('kb.err.bodyRequired')); return; }
    setBusy(true); setErr('');
    try { await updateKnowledge(item.id, { title: title.trim(), text: text.trim() }); onClose(); }
    catch (e) { setErr(e.message || t('kb.err.addFailed')); setBusy(false); }
  };
  return (
    <Modal open onClose={onClose} width={560} title={window.t("extra.knowledge.editMemo")} subtitle={window.t("extra.knowledge.memoHint")}
      footer={<><Button variant="subtle" onClick={onClose}>{t('btn.cancel')}</Button>
        <Button variant="primary" icon="check" onClick={save} disabled={busy || loading}>{busy ? t('kb.importing') : t('btn.save')}</Button></>}>
      <Field label={t('kb.field.title')}><TextInput value={title} onChange={(e) => setTitle(e.target.value)} placeholder={t('kb.field.title.placeholder')} /></Field>
      <Field label={t('kb.type.text')}>
        {loading
          ? <div style={{ fontSize: 12.5, color: '#9aa1ab', padding: '10px 2px' }}>{window.t("de.loading")}</div>
          : <textarea value={text} onChange={(e) => setText(e.target.value)} rows={8} style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.6 }} />}
      </Field>
      {err && <div style={{ fontSize: 12.5, color: '#dc2626', marginTop: 4 }}>{err}</div>}
    </Modal>
  );
}

function KnowledgeScreen() {
  const { knowledge, removeKnowledge, currentUser, can, navigate, showToast } = useStore();
  const D = window.APP_DATA;
  const [kb, setKb] = React.useState('achievement');
  const [q, setQ] = React.useState('');               // タイトル・内容での絞り込み
  const [unindexedOnly, setUnindexedOnly] = React.useState(false); // 素材：未索引（RAG未反映）だけ表示
  const [catFilter, setCatFilter] = React.useState(''); // カテゴリでの絞り込み
  React.useEffect(() => { setCatFilter(''); }, [kb]); // タブを切り替えたらカテゴリ絞り込みは解除
  // 素材(AI用)の追加・削除は権限設定（既定:管理者）。社内ナレッジ・個人専用KBはメンバーも追加できる
  const isAdmin = (isInternalKB(kb) || isPersonalKB(kb)) ? true : can('manageKnowledge');
  const myId = currentUser && currentUser.id;
  const mine = (k) => !isPersonalKB(k.kb) || k.ownerId === myId; // 個人KBは自分の分だけ（bootstrapで他人分は来ないが二重防御）
  const [addOpen, setAddOpen] = React.useState(false);
  const [editItem, setEditItem] = React.useState(null); // 編集中のテキスト系ナレッジ（個人KBの署名メモ等）
  const [viewItem, setViewItem] = React.useState(null); // 閲覧中のナレッジ（カードクリックで全文表示）
  // 案B：グループ(素材/社内ナレッジ)のセグメント切替。アクティブグループは現在のkbから導出
  const activeGroup = (KB_TABS.find(t => t.key === kb) || {}).group || KB_GROUPS[0];
  const switchGroup = (grp) => { const first = KB_TABS.find(t => t.group === grp); if (first) setKb(first.key); };
  const groupCount = (grp) => (knowledge || []).filter(k => !!KB_TABS.find(t => t.key === k.kb && t.group === grp)).length;
  // 未索引フィルタは素材(SOURCE_KBS)のみ意味を持つ（社内/個人KBは索引バッジを出さない）
  const unindexedActive = unindexedOnly && SOURCE_KBS.includes(kb);
  const qq = q.trim().toLowerCase();
  // この種別に存在するカテゴリ一覧（絞り込みドロップダウン用）
  const catOptions = Array.from(new Set((knowledge || []).filter(k => k.kb === kb && mine(k)).flatMap(k => k.categories || []))).sort();
  const catActive = catFilter && catOptions.includes(catFilter);
  const items = (knowledge || []).filter(k => k.kb === kb && mine(k))
    .filter(k => !qq || `${k.title || ''} ${k.preview || ''}`.toLowerCase().includes(qq))
    .filter(k => !unindexedActive || !k.embedded)
    .filter(k => !catActive || (k.categories || []).includes(catFilter))
    .slice().sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''));
  const hasFilter = !!qq || unindexedActive || catActive;
  const tab = KB_TABS.find(t => t.key === kb);
  const typeMeta = { text: { icon: 'edit', label: t('kb.type.text') }, url: { icon: 'link', label: t('kb.type.url') }, doc: { icon: 'attach', label: t('kb.type.file') } };

  const remove = async (k) => {
    if (!window.confirm(t('kb.confirm.delete', { title: k.title }))) return;
    try { await removeKnowledge(k.id); } catch (e) { showToast(e.message, 'x'); }
  };

  const right = (isAdmin && !SETTINGS_KBS.includes(kb) && kb !== 'featureMaster') ? <Button variant="primary" icon="plus" onClick={() => setAddOpen(true)}>{t('kb.addKnowledge')}</Button> : null;

  return (
    <Page title={t('nav.knowledge')} right={right}>
      {/* 案B：上段＝グループのセグメント切替（素材／社内ナレッジ）、下段＝アクティブ側のチップだけ */}
      <div style={{ display: 'flex', gap: 4, padding: 4, background: '#eceef1', borderRadius: 12, marginBottom: 12, width: 'fit-content' }}>
        {KB_GROUPS.map(grp => {
          const on = activeGroup === grp;
          return (
            <button key={grp} onClick={() => switchGroup(grp)}
              style={{ display: 'flex', alignItems: 'center', gap: 7, padding: '7px 18px', borderRadius: 9, cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 600, border: 'none',
                background: on ? '#fff' : 'transparent', color: on ? '#1c1f26' : '#7b828d', boxShadow: on ? '0 1px 2px rgba(20,22,40,.10)' : 'none' }}>
              {grp}
              <span style={{ fontSize: 11, fontWeight: 700, color: on ? '#9aa1ab' : '#aab0b9' }}>{groupCount(grp)}</span>
            </button>
          );
        })}
      </div>
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 14 }}>
        {KB_TABS.filter(t => t.group === activeGroup).map(t => (
          <button key={t.key} onClick={() => setKb(t.key)}
            style={{ display: 'flex', alignItems: 'center', gap: 7, padding: '6px 13px', borderRadius: 999, cursor: 'pointer', fontFamily: 'inherit', fontSize: 12.5, fontWeight: 600,
              border: '1px solid ' + (kb === t.key ? '#4a5af0' : '#e2e5ea'), background: kb === t.key ? '#4a5af0' : '#fff', color: kb === t.key ? '#fff' : '#3b414b' }}>
            <Icon name={KB_ICONS[t.key] || 'spark'} size={14} stroke={2} />{kbLabel(t.label)}
            {!t.set && (
              <span style={{ fontSize: 11, fontWeight: 700, color: kb === t.key ? '#fff' : '#9aa1ab', background: kb === t.key ? 'rgba(255,255,255,.2)' : '#f0f1f4', padding: '1px 7px', borderRadius: 999 }}>
                {(knowledge || []).filter(k => k.kb === t.key && mine(k)).length}
              </span>
            )}
          </button>
        ))}
      </div>

      <div style={{ fontSize: 12.5, color: '#7b828d', marginBottom: 14, lineHeight: 1.6, padding: '11px 14px', background: '#f6f7f9', borderRadius: 10 }}>
        <Icon name="spark" size={14} stroke={2} style={{ color: '#4a5af0', verticalAlign: '-2px', marginRight: 6 }} />{kbLabel(tab.desc)}
      </div>

      {kb === 'proposalSet' ? <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}><DeckDesignCard /><ProposalTemplateCard /><AiPromptSettings only={['proposal', 'prototype', 'shodan']} /><SkillFilesCard /></div> : kb === 'quoteSet' ? <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
        <AiPromptSettings only={['quote']} />
        <div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, margin: '6px 0 12px' }}>
            <div style={{ width: 34, height: 34, borderRadius: 9, background: '#eef0fe', display: 'flex', alignItems: 'center', justifyContent: 'center', flex: '0 0 auto' }}><Icon name="edit" size={17} stroke={2} style={{ color: '#4a5af0' }} /></div>
            <div>
              <div style={{ fontSize: 15, fontWeight: 700, color: '#1f2430' }}>{t('kb.qnt.title')}</div>
              <div style={{ fontSize: 12, color: '#9aa1ab', marginTop: 1 }}>{t('kb.qnt.desc')}</div>
            </div>
          </div>
          <QuoteNoteTemplatesPanel />
        </div>
      </div> : (<>
      {/* 頁内検索＋未索引フィルタ（タイトル/内容で即時絞り込み。素材は未索引だけ抽出可） */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
        <div style={{ position: 'relative', flex: 1, minWidth: 200 }}>
          <Icon name="search" size={15} stroke={2} style={{ position: 'absolute', left: 11, top: '50%', transform: 'translateY(-50%)', color: '#aab0ba' }} />
          <input value={q} onChange={(e) => setQ(e.target.value)} placeholder={t('kb.search.placeholder')}
            style={{ width: '100%', boxSizing: 'border-box', padding: '8px 30px 8px 32px', borderRadius: 9, border: '1px solid #e2e5ea', fontSize: 13, fontFamily: 'inherit', outline: 'none' }} />
          {q && <button onClick={() => setQ('')} style={{ position: 'absolute', right: 8, top: '50%', transform: 'translateY(-50%)', border: 'none', background: 'transparent', color: '#aab0ba', cursor: 'pointer', fontFamily: 'inherit', padding: 2 }}><Icon name="x" size={14} stroke={2.2} /></button>}
        </div>
        {catOptions.length > 0 && (
          <select value={catActive ? catFilter : ''} onChange={(e) => setCatFilter(e.target.value)}
            style={{ padding: '8px 12px', borderRadius: 9, border: '1px solid ' + (catActive ? '#4a5af0' : '#e2e5ea'), background: catActive ? '#eef0fe' : '#fff', color: catActive ? '#4a5af0' : '#3b414b', fontSize: 12.5, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer', appearance: 'none', WebkitAppearance: 'none' }}>
            <option value="">{t('kb.allCategories')}</option>
            {catOptions.map(c => <option key={c} value={c}>{c}</option>)}
          </select>
        )}
        {kb === 'company' && can('manageKnowledge') && (
          <Button variant="primary" icon="spark" onClick={async () => {
            if (!window.confirm(window.t("label.extra85"))) return;
            try {
              await API.companySync();
              showToast(window.t("label.extra86"));
            } catch (e) { showToast((e && e.message) || '更新を開始できませんでした', 'x'); }
          }}>{window.t("extra.knowledge.updateWeb")}</Button>
        )}
        {kb === 'case' && can('manageKnowledge') && (
          <Button variant="primary" icon="spark" onClick={async () => {
            if (!window.confirm(window.t("label.extra87"))) return;
            try { await API.kbGen('case'); showToast(window.t("label.extra88")); }
            catch (e) { showToast((e && e.message) || '開始できませんでした', 'x'); }
          }}>{window.t("extra.knowledge.fromWon")}</Button>
        )}
        {kb === 'faq' && can('manageKnowledge') && (
          <Button variant="primary" icon="spark" onClick={async () => {
            if (!window.confirm(window.t("label.extra89"))) return;
            try { await API.kbGen('faq'); showToast(window.t("label.extra90")); }
            catch (e) { showToast((e && e.message) || '開始できませんでした', 'x'); }
          }}>{window.t("extra.knowledge.fromMeetings")}</Button>
        )}
        {kb === 'achievement' && can('manageKnowledge') && (
          <Button variant="primary" icon="download" onClick={async () => {
            if (!window.confirm(window.t("label.extra91"))) return;
            try { await API.workSync(); showToast(window.t("label.extra92")); }
            catch (e) { showToast((e && e.message) || '開始できませんでした', 'x'); }
          }}>{window.t("extra.knowledge.importPortfolio")}</Button>
        )}
        {kb === 'achievement' && can('manageKnowledge') && (
          <Button variant="default" icon="spark" onClick={async () => {
            if (!window.confirm(window.t("label.extra93"))) return;
            try { await API.achievementEnrich(); showToast(window.t("label.extra94")); }
            catch (e) { showToast((e && e.message) || '開始できませんでした', 'x'); }
          }}>{window.t("extra.knowledge.tagAI")}</Button>
        )}
        {kb === 'achievement' && (
          <Button variant="default" icon="download" onClick={async () => {
            // 実績集（ポートフォリオ）を印刷用1ページで出力：全実績の全文を取得→■構造を整形→新規タブ（PDF保存は印刷から）
            const list = (knowledge || []).filter(x => x.kb === 'achievement' && mine(x)).slice().sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''));
            if (!list.length) { showToast(window.t("label.extra95"), 'x'); return; }
            showToast(window.t("label.extra96"));
            const texts = await Promise.all(list.map(x => API.knowledgeText(x.id).then(r => (r && r.text) || x.preview || '').catch(() => x.preview || '')));
            const esc = (s) => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
            const parse = (s) => String(s || '').indexOf('■') < 0 ? [] : String(s).split('■').map(p => p.trim()).filter(Boolean).map(p => { const nl = p.indexOf('\n'); return nl < 0 ? { label: p, value: '' } : { label: p.slice(0, nl).trim(), value: p.slice(nl + 1).trim() }; }).filter(f => f.label && f.value);
            const isUrl = (v) => /^https?:\/\/\S+$/.test(String(v || '').trim());
            const cardHtml = (x, txt) => {
              const fs = parse(txt);
              const get = (lb) => { const f = fs.find(y => y.label.startsWith(lb)); return f ? f.value : ''; };
              const meta = [get('種別'), get('業種'), get('時期')].filter(Boolean).map(esc).join('　·　');
              const link = get('関連リンク');
              const secs = [['内容・規模・使用技術', get('内容・規模・使用技術')], ['成果', get('成果')], ['アピールポイント', get('アピールポイント')], ['向いている案件・用途', get('向いている案件・用途')]]
                .filter(s => s[1]).map(s => `<div class="sec"><div class="sl">${esc(s[0])}</div><div class="sv">${esc(s[1])}</div></div>`).join('');
              const bodyFallback = fs.length ? '' : `<div class="sec"><div class="sv">${esc(txt).slice(0, 1200)}</div></div>`;
              return `<div class="item"><div class="ttl">${esc(x.title)}</div>
                ${(x.categories || []).length ? '<div class="cats">' + x.categories.map(c => `<span class="cat">${esc(c)}</span>`).join('') + '</div>' : ''}
                ${meta ? `<div class="meta">${meta}</div>` : ''}
                ${link && isUrl(link) ? `<div class="meta"><a href="${esc(link)}">${esc(link)}</a></div>` : ''}
                ${secs}${bodyFallback}</div>`;
            };
            const html = `<!DOCTYPE html><html lang="ja"><head><meta charset="utf-8"><title>ALION 開発実績集</title><style>
              body{font-family:-apple-system,BlinkMacSystemFont,'Hiragino Sans','Noto Sans JP',sans-serif;color:#1f2430;margin:0;padding:34px 40px;background:#fff}
              h1{font-size:21px;margin:0 0 2px}.sub{font-size:11.5px;color:#8a909b;margin-bottom:22px}
              .item{border:1px solid #e6e8ec;border-radius:12px;padding:16px 18px;margin-bottom:14px;page-break-inside:avoid}
              .ttl{font-size:14.5px;font-weight:700;margin-bottom:6px}
              .cats{margin-bottom:5px}.cat{display:inline-block;font-size:10px;font-weight:600;color:#4a5af0;background:#eef0fe;border-radius:999px;padding:1px 9px;margin-right:4px}
              .meta{font-size:11px;color:#7b828d;margin-bottom:4px}.meta a{color:#4a5af0}
              .sec{margin-top:7px}.sl{font-size:10.5px;font-weight:700;color:#9aa1ab;margin-bottom:2px}.sv{font-size:12px;line-height:1.75;white-space:pre-wrap}
              .toolbar{position:fixed;top:12px;right:14px}.toolbar button{font:600 12.5px/1 -apple-system,sans-serif;padding:9px 16px;border-radius:8px;border:none;background:#4a5af0;color:#fff;cursor:pointer}
              @media print{.toolbar{display:none}body{padding:0}}
            </style></head><body>
              <div class="toolbar"><button onclick="window.print()">印刷 / PDFに保存</button></div>
              <h1>ALION 開発実績集</h1><div class="sub">${new Date().getFullYear()}年${new Date().getMonth() + 1}月${new Date().getDate()}日時点 · ${list.length}件</div>
              ${list.map((x, i) => cardHtml(x, texts[i])).join('')}
            </body></html>`;
            const w = window.open('', '_blank');
            if (!w) { showToast(window.t("label.extra5"), 'x'); return; }
            w.document.write(html); w.document.close();
          }}>{window.t("extra.knowledge.exportPortfolio")}</Button>
        )}
        {SOURCE_KBS.includes(kb) && (
          <button onClick={() => setUnindexedOnly(v => !v)}
            style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '7px 12px', borderRadius: 999, cursor: 'pointer', fontFamily: 'inherit', fontSize: 12.5, fontWeight: 600, whiteSpace: 'nowrap',
              border: '1px solid ' + (unindexedOnly ? '#b45309' : '#e2e5ea'), background: unindexedOnly ? '#fdf0db' : '#fff', color: unindexedOnly ? '#b45309' : '#3b414b' }}>
            <Icon name="alert" size={13} stroke={2} />{t('kb.unindexedOnly')}
          </button>
        )}
      </div>
      <Card pad={0}>
        {items.length === 0 && (
          <div style={{ padding: '46px 0', textAlign: 'center', color: '#9aa1ab', fontSize: 13 }}>
            {hasFilter ? t('kb.noMatch') : <>{t('kb.empty')}{isAdmin ? t('kb.empty.adminHint') : t('kb.empty.memberHint')}</>}
            {!hasFilter && isAdmin && (
              <div style={{ marginTop: 14 }}>
                <Button variant="primary" icon="plus" onClick={() => setAddOpen(true)}>{t('kb.addKnowledge')}</Button>
              </div>
            )}
          </div>
        )}
        {items.map((k, i) => {
          const tm = typeMeta[k.type] || typeMeta.text;
          const by = D.user(k.createdBy);
          return (
            <div key={k.id} className="row-hover" onClick={() => setViewItem(k)} title={window.t("extra.knowledge.fullText")}
              style={{ display: 'flex', alignItems: 'flex-start', gap: 13, padding: '14px 18px', borderBottom: i === items.length - 1 ? 'none' : '1px solid #f4f5f7', cursor: 'pointer' }}>
              {k.imageUrl
                ? <a href={k.imageUrl} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()} style={{ flex: '0 0 auto', lineHeight: 0 }} title={tm.label}>
                    <img src={k.imageUrl} alt="" onError={(e) => { e.currentTarget.style.display = 'none'; }}
                      style={{ width: 48, height: 48, borderRadius: 10, objectFit: 'cover', background: '#f0f1f4', border: '1px solid #eceef1' }} />
                  </a>
                : <div style={{ width: 34, height: 34, borderRadius: 10, background: '#f0f1f4', display: 'flex', alignItems: 'center', justifyContent: 'center', flex: '0 0 auto' }}>
                    <Icon name={tm.icon} size={16} stroke={2} style={{ color: '#5b626d' }} />
                  </div>}
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                  <span style={{ fontSize: 13.5, fontWeight: 600, color: '#1f2430' }}>{k.title}</span>
                  <span style={{ fontSize: 10.5, fontWeight: 700, color: '#5b626d', background: '#f0f1f4', padding: '1px 7px', borderRadius: 4 }}>{tm.label}</span>
                  {isPersonalKB(k.kb)
                    ? <span style={{ fontSize: 10.5, fontWeight: 700, color: '#5b54b8', background: '#eef0fe', padding: '1px 7px', borderRadius: 4 }}>{window.t("extra.knowledge.personal")}</span>
                    : isInternalKB(k.kb)
                      ? <span style={{ fontSize: 10.5, fontWeight: 700, color: '#5b54b8', background: '#eef0fe', padding: '1px 7px', borderRadius: 4 }}>{window.t("extra.knowledge.internal")}</span>
                      : k.embedded
                        ? <span style={{ fontSize: 10.5, fontWeight: 700, color: '#15803d', background: '#e3f5e9', padding: '1px 7px', borderRadius: 4 }}>{t('kb.ragIndexed', { n: k.chunkCount })}</span>
                        : <span style={{ fontSize: 10.5, fontWeight: 700, color: '#b45309', background: '#fdf0db', padding: '1px 7px', borderRadius: 4 }}>{t('kb.notIndexed')}</span>}
                </div>
                {(k.categories || []).length > 0 && (
                  <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginTop: 6 }}>
                    {k.categories.map(c => (
                      <button key={c} onClick={(e) => { e.stopPropagation(); setCatFilter(c); }} title={window.t("extra.knowledge.filterCategory")}
                        style={{ fontSize: 10.5, fontWeight: 600, color: '#4a5af0', background: '#eef0fe', border: '1px solid #e3e2fb', borderRadius: 999, padding: '1px 9px', cursor: 'pointer', fontFamily: 'inherit' }}>
                        {c}
                      </button>
                    ))}
                  </div>
                )}
                {k.caseId && (() => { const kc = D.caseById(k.caseId); return kc ? (
                  <button onClick={(e) => { e.stopPropagation(); navigate('case', kc.id); }} style={{ display: 'inline-flex', alignItems: 'center', gap: 5, marginTop: 5, border: '1px solid #d7d5f5', background: '#f3f2fd', color: '#5b54b8', borderRadius: 6, padding: '2px 8px', fontSize: 11.5, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>
                    <Icon name="cases" size={12} stroke={2} />{kc.title}
                  </button>) : null; })()}
                {k.sourceUrl && <a href={k.sourceUrl} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()} style={{ fontSize: 11.5, color: '#4a5af0', textDecoration: 'none', fontFamily: 'var(--mono)', wordBreak: 'break-all', display: 'block', marginTop: 4 }}>{k.sourceUrl}</a>}
                <KbPreview text={k.preview} />
                <div style={{ fontSize: 11, color: '#aab0ba', marginTop: 5 }}>
                  {t('kb.charCountPlain', { n: (k.textLen || 0).toLocaleString() })} · {fmtDateTime(k.createdAt)}{by ? ` · ${by.short}` : ''}
                </div>
              </div>
              <div onClick={(e) => e.stopPropagation()} style={{ display: 'flex', alignItems: 'center', gap: 2, flex: '0 0 auto' }}>
                {(isAdmin && mine(k) && (k.type || 'text') === 'text') && <IconButton name="edit" size={15} onClick={() => setEditItem(k)} title={t('btn.edit')} />}
                {(isAdmin && mine(k)) && <IconButton name="x" size={16} onClick={() => remove(k)} title={t('btn.delete')} />}
              </div>
            </div>
          );
        })}
      </Card>
      {/* 見積もり用タブに機能マスタ（料金カタログ）を同居＝1タブに完全統合。上＝素材、下＝機能ごとの単価表 */}
      {kb === 'quote' && (
        <div style={{ marginTop: 30 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12, paddingTop: 22, borderTop: '1px solid #eceef1' }}>
            <div style={{ width: 34, height: 34, borderRadius: 9, background: '#eef0fe', display: 'flex', alignItems: 'center', justifyContent: 'center', flex: '0 0 auto' }}><Icon name="chart" size={17} stroke={2} style={{ color: '#4a5af0' }} /></div>
            <div>
              <div style={{ fontSize: 15, fontWeight: 700, color: '#1f2430' }}>{window.t("extra.catalog.title")}</div>
              <div style={{ fontSize: 12, color: '#9aa1ab', marginTop: 1 }}>{window.t("extra.catalog.description")}</div>
            </div>
          </div>
          <FeatureMasterPanel />
        </div>
      )}
      </>)}

      {addOpen && <KnowledgeAddModal kb={kb} onClose={() => setAddOpen(false)} />}
      {editItem && <KnowledgeEditModal item={editItem} onClose={() => setEditItem(null)} />}
      {viewItem && <KnowledgeViewModal item={viewItem}
        canEdit={isAdmin && mine(viewItem) && (viewItem.type || 'text') === 'text'}
        onEdit={() => { setEditItem(viewItem); setViewItem(null); }}
        onClose={() => setViewItem(null)} />}
    </Page>
  );
}

Object.assign(window, { KnowledgeScreen });
