PageSourceSearch

https://agent-navi.vercel.app/

html agent-navi.vercel.app collected 2026-10-03 06:11:36 UTC 140,090 bytes, 2,874 lines download raw bytes

1<!DOCTYPE html>
2<html lang="ja">
3<head>
4  <meta charset="UTF-8" />
5  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6  <meta name="robots" content="noindex, nofollow" />
7  <title>エージェント診断 & 一括登録ツール</title>
8  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔍</text></svg>" />
9  
9<script src="https://cdn.tailwindcss.com/3.4.17"></script>
9
10  
10<script src="https://unpkg.com/[email protected]/umd/react.production.min.js" crossorigin></script>
10
11  
11<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js" crossorigin></script>
11
12  
12<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
12
13  
13<script src="https://cdn.jsdelivr.net/npm/@supabase/[email protected]/dist/umd/supabase.min.js"></script>
13
14  
14<script>
15    const _supabaseUrl = 'https://cugaikmapidrwqmwlxzp.supabase.co';
16    const _supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImN1Z2Fpa21hcGlkcndxbXdseHpwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzM0NzAxNTMsImV4cCI6MjA4OTA0NjE1M30.znDqwz2J4uGUcaHC5F2HoPIMOuHo4r7iCprqWGl7PMY';
17    const _supabase = supabase.createClient(_supabaseUrl, _supabaseKey);
18  </script>
18
19  
19<script>
20    tailwind.config = {
21      theme: {
22        extend: {
23          colors: {
24            brand: {
25              gold: '#D4AF37',
26              'gold-dark': '#B8960C',
27              orange: '#E8735A',
28              'orange-dark': '#D4573E',
29            }
30          },
31          animation: {
32            'fade-in': 'fadeIn 0.5s ease-out',
33            'fade-in-up': 'fadeInUp 0.5s ease-out',
34            'slide-in': 'slideIn 0.4s ease-out',
35            'pulse-once': 'pulseOnce 0.3s ease-out',
36            'pop-in': 'popIn 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55)',
37            'bounce-light': 'bounceLight 0.4s ease-out',
38          },
39          keyframes: {
40            fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' } },
41            fadeInUp: { '0%': { opacity: '0', transform: 'translateY(20px)' }, '100%': { opacity: '1', transform: 'translateY(0)' } },
42            slideIn: { '0%': { opacity: '0', transform: 'translateX(30px)' }, '100%': { opacity: '1', transform: 'translateX(0)' } },
43            pulseOnce: { '0%': { transform: 'scale(1)' }, '50%': { transform: 'scale(1.05)' }, '100%': { transform: 'scale(1)' } },
44            popIn: { '0%': { transform: 'scale(0)' }, '100%': { transform: 'scale(1)' } },
45            bounceLight: { '0%': { transform: 'scale(1)' }, '40%': { transform: 'scale(1.08)' }, '100%': { transform: 'scale(1)' } },
46          }
47        }
48      }
49    }
50  </script>
50
51  <link rel="preconnect" href="https://fonts.googleapis.com">
52  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
53  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+JP:wght@400;500;600;700;800&display=swap" rel="stylesheet">
54  <style>
55    * { box-sizing: border-box; }
56    body { margin: 0; padding: 0; font-family: 'Inter', 'Noto Sans JP', -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", sans-serif; background: transparent; }
57    .stagger-1 { animation-delay: 0.1s; }
58    .stagger-2 { animation-delay: 0.2s; }
59    .stagger-3 { animation-delay: 0.3s; }
60    .stagger-4 { animation-delay: 0.4s; }
61    .stagger-5 { animation-delay: 0.5s; }
62    /* ===== Mode Select Cards ===== */
63  </style>
64</head>
65<body>
66  <div id="root"></div>
67
68  
68<script src="/data/agents-master.js"></script>
68
69  
69<script type="text/babel" data-presets="react">
70    /* @jsxRuntime classic */
71    const { useState, useReducer, useCallback, useMemo, useEffect, useRef } = React;
72
73    // ============================================================
74    // DATA: エージェントマスターデータ(外部ファイルから読込)
75    // ============================================================
76    const AGENTS_MASTER = window.AGENTS_MASTER;
77
78    // ============================================================
79    // DATA: 記事別設定(URLパラメータで切替)
80    // ============================================================
81    const ARTICLE_CONFIGS = {
82      strategy: {
83        articleId: "strategy-consul-agent", title: "戦略コンサル案件",
84        agents: [
85          { id: "senjin", priority: 1 },
86          { id: "highperfo", priority: 2 },
87          { id: "lancers_pro", priority: 3 },
88          { id: "freeconsultant_jp", priority: 10 },
89          { id: "proconnect", priority: 5 },
90          { id: "freeconsul_biz", priority: 6 },
91          { id: "strategy_bank", priority: 7 },
92          { id: "forpro", priority: 8 },
93          { id: "pro_hub", priority: 9 }
94        ]
95      },
96      biz_consul: {
97        articleId: "biz-consul-agent", title: "業務コンサル案件",
98        agents: [
99          { id: "senjin", priority: 1 },
100          { id: "highperfo", priority: 2 },
101          { id: "lancers_pro", priority: 3 },
102          { id: "freeconsultant_jp", priority: 11 },
103          { id: "pmo_jobs", priority: 5 },
104          { id: "proconnect", priority: 6 },
105          { id: "freeconsul_biz", priority: 7 },
106          { id: "strategy_bank", priority: 8 },
107          { id: "forpro", priority: 9 },
108          { id: "pro_hub", priority: 10 }
109        ]
110      },
111      it_consul: {
112        articleId: "it-consul-agent", title: "ITコンサル案件",
113        agents: [
114          { id: "senjin_it", priority: 1 },
115          { id: "highperfo", priority: 2 },
116          { id: "freeconsultant_jp", priority: 8 },
117          { id: "lancers_pro", priority: 4 },
118          { id: "pmo_jobs", priority: 5 },
119          { id: "proconnect", priority: 6 },
120          { id: "pro_hub", priority: 7 }
121        ]
122      },
123      engineering: {
124        articleId: "engineer-agent", title: "エンジニアリング案件",
125        agents: [
126          { id: "levatech", priority: 1 },
127          { id: "midworks", priority: 2 },
128          { id: "techstock", priority: 3 },
129          { id: "techfree", priority: 4 },
130          { id: "techadapt", priority: 5 },
131          { id: "itpro_partners", priority: 6 },
132          { id: "bigdata_navi", priority: 7 }
133        ]
134      },
135      sap: {
136        articleId: "sap-agent", title: "SAP案件",
137        agents: [
138          { id: "senjin_it", priority: 1 },
139          { id: "quickflow", priority: 2 },
140          { id: "highperfo", priority: 3 },
141          { id: "levatech", priority: 4 },
142          { id: "lancers_pro", priority: 5 },
143          { id: "freeconsultant_jp", priority: 8 },
144          { id: "proconnect", priority: 7 },
145          { id: "techstock", priority: 8 },
146          { id: "forpro", priority: 9 }
147        ]
148      },
149      security: {
150        articleId: "security-agent", title: "セキュリティ案件",
151        agents: [
152          { id: "senjin_security", priority: 1 },
153          { id: "levatech", priority: 2 },
154          { id: "highperfo", priority: 3 },
155          { id: "freeconsultant_jp", priority: 7 },
156          { id: "lancers_pro", priority: 5 },
157          { id: "proconnect", priority: 6 },
158          { id: "techstock", priority: 7 },
159          { id: "bigdata_navi", priority: 8 }
160        ]
161      },
162      tenshoku_strategy_consul: {
163        articleId: "tenshoku-consul-agent", title: "戦略・経営コンサル転職",
164        agents: [
165          { id: "myvision", priority: 1 },
166          { id: "yhc", priority: 2 },
167          { id: "axis", priority: 3 },
168          { id: "concord", priority: 4 },
169          { id: "assign", priority: 5 },
170          { id: "samurai", priority: 6 },
171          { id: "kotora", priority: 7 },
172          { id: "laughlogic", priority: 8 }
173        ]
174      },
175      tenshoku_it_consul: {
176        articleId: "tenshoku-it-consul-agent", title: "IT・DXコンサル転職",
177        agents: [
178          { id: "myvision", priority: 1 },
179          { id: "axis", priority: 2 },
180          { id: "yhc", priority: 3 },
181          { id: "assign", priority: 4 },
182          { id: "kotora", priority: 5 },
183          { id: "samurai", priority: 6 },
184          { id: "sap_tenshoku", priority: 7 }
185        ]
186      },
187      tenshoku_startup: {
188        articleId: "tenshoku-startup-agent", title: "スタートアップ転職",
189        agents: [
190          { id: "myvision", priority: 1 },
191          { id: "yhc", priority: 2 },
192          { id: "axis", priority: 3 },
193          { id: "assign", priority: 4 },
194          { id: "samurai", priority: 5 },
195          { id: "miracareer", priority: 6 },
196          { id: "owen_career", priority: 7 },
197          { id: "neocareer", priority: 8 }
198        ]
199      },
200      tenshoku_enterprise: {
201        articleId: "tenshoku-enterprise-agent", title: "大企業・事業会社転職",
202        agents: [
203          { id: "myvision", priority: 1 },
204          { id: "yhc", priority: 2 },
205          { id: "axis", priority: 3 },
206          { id: "assign", priority: 4 },
207          { id: "samurai", priority: 5 },
208          { id: "kotora", priority: 6 },
209          { id: "hr_career_agent", priority: 7 },
210          { id: "newma", priority: 8 },
211          { id: "twinpro", priority: 9 },
212          { id: "miracareer", priority: 10 },
213          { id: "owen_career", priority: 11 },
214          { id: "neocareer", priority: 12 }
215        ]
216      },
217      tenshoku_engineer: {
218        articleId: "tenshoku-engineer-agent", title: "エンジニア転職",
219        agents: [
220          { id: "techgo", priority: 1 },
221          { id: "techgate_expert", priority: 2 },
222          { id: "meiko_career", priority: 3 },
223          { id: "shanai_se", priority: 4 },
224          { id: "techclips", priority: 5 },
225          { id: "sap_tenshoku", priority: 6 },
226          { id: "raku_partners", priority: 7 }
227        ]
228      },
229      tenshoku_ma: {
230        articleId: "tenshoku-ma-agent", title: "M&A・財務アドバイザリー転職",
231        agents: [
232          { id: "newma", priority: 1 },
233          { id: "myvision", priority: 2 },
234          { id: "yhc", priority: 3 },
235          { id: "concord", priority: 4 },
236          { id: "ma_beginners", priority: 5 },
237          { id: "kotora", priority: 6 }
238        ]
239      },
240      tenshoku_ibd_pe: {
241        articleId: "tenshoku-ibd-pe-agent", title: "投資銀行・PEファンド転職",
242        agents: [
243          { id: "yhc", priority: 1 },
244          { id: "newma", priority: 2 },
245          { id: "myvision", priority: 3 },
246          { id: "concord", priority: 4 },
247          { id: "kotora", priority: 5 },
248          { id: "ma_beginners", priority: 6 }
249        ]
250      },
251      tenshoku_accounting: {
252        articleId: "tenshoku-accounting-agent", title: "会計・税務転職",
253        agents: [
254          { id: "twinpro", priority: 1 },
255          { id: "myvision", priority: 2 },
256          { id: "kotora", priority: 3 },
257          { id: "yhc", priority: 4 }
258        ]
259      },
260      tenshoku_20s: {
261        articleId: "tenshoku-20s-agent", title: "20代転職",
262        agents: [
263          { id: "miracareer", priority: 1 },
264          { id: "owen_career", priority: 2 },
265          { id: "neocareer", priority: 3 },
266          { id: "assign", priority: 4 },
267          { id: "myvision", priority: 5 },
268          { id: "raku_partners", priority: 6 }
269        ]
270      },
271      tenshoku_sales: {
272        articleId: "tenshoku-sales-agent", title: "営業・セールス転職",
273        agents: [
274          { id: "myvision", priority: 1 },
275          { id: "assign", priority: 2 },
276          { id: "yhc", priority: 3 },
277          { id: "samurai", priority: 4 },
278          { id: "newma", priority: 5 },
279          { id: "miracareer", priority: 6 },
280          { id: "owen_career", priority: 7 },
281          { id: "neocareer", priority: 8 }
282        ]
283      },
284      tenshoku_marketing: {
285        articleId: "tenshoku-marketing-agent", title: "マーケティング転職",
286        agents: [
287          { id: "myvision", priority: 1 },
288          { id: "axis", priority: 2 },
289          { id: "assign", priority: 3 },
290          { id: "yhc", priority: 4 },
291          { id: "samurai", priority: 5 },
292          { id: "kotora", priority: 6 }
293        ]
294      },
295      tenshoku_other: {
296        articleId: "tenshoku-other-agent", title: "その他転職",
297        agents: [
298          { id: "myvision", priority: 1 },
299          { id: "axis", priority: 2 },
300          { id: "assign", priority: 3 },
301          { id: "yhc", priority: 4 },
302          { id: "samurai", priority: 5 },
303          { id: "kotora", priority: 6 },
304          { id: "miracareer", priority: 7 },
305          { id: "owen_career", priority: 8 },
306          { id: "neocareer", priority: 9 }
307        ]
308      },
309      tenshoku_consul_firm: {
310        articleId: "tenshoku-consul-firm", title: "コンサルファーム転職",
311        agents: [
312          { id: "myvision", priority: 1 },
313          { id: "yhc", priority: 2 },
314          { id: "axis", priority: 3 },
315          { id: "concord", priority: 4 },
316          { id: "assign", priority: 5 },
317          { id: "samurai", priority: 6 },
318          { id: "kotora", priority: 7 },
319          { id: "laughlogic", priority: 8 }
320        ]
321      },
322      tenshoku_foreign: {
323        articleId: "tenshoku-foreign", title: "外資系転職",
324        agents: [
325          { id: "myvision", priority: 1 },
326          { id: "yhc", priority: 2 },
327          { id: "axis", priority: 3 },
328          { id: "concord", priority: 4 },
329          { id: "assign", priority: 5 },
330          { id: "samurai", priority: 6 },
331          { id: "kotora", priority: 7 }
332        ]
333      },
334      tenshoku_any: {
335        articleId: "tenshoku-any", title: "総合転職",
336        agents: [
337          { id: "myvision", priority: 1 },
338          { id: "assign", priority: 2 },
339          { id: "yhc", priority: 3 },
340          { id: "samurai", priority: 4 }
341        ]
342      }
343    };
344
345    // localStorageに保存済みの記事別設定があれば上書き(管理画面の変更を即反映)
346    // 注: Supabase APIからの読み込みは後述の _applyRemoteSettings() で非同期に行う
347    const _savedConfigs = (() => {
348      try { return JSON.parse(localStorage.getItem('af_configs')); } catch(e) { return null; }
349    })();
350    if (_savedConfigs) {
351      Object.keys(_savedConfigs).forEach(key => {
352        if (ARTICLE_CONFIGS[key]) {
353          ARTICLE_CONFIGS[key] = { ...ARTICLE_CONFIGS[key], ..._savedConfigs[key] };
354        }
355      });
356    }
357
358    // ============================================================
359    // DATA: 診断質問(localStorageから読み込み、フォールバックはハードコード)
360    // ============================================================
361    // Q3ブーストの旧形式(q3val のみ)→ 新形式(q2val:q3val 複合キー)に移行
362    function _migrateQ3Boosts(savedQuestions) {
363      if (!savedQuestions || !savedQuestions._Q3_AGENT_BOOSTS) return savedQuestions;
364      const optsByQ2 = savedQuestions._Q3_OPTIONS_BY_Q2 || {};
365      const oldBoosts = savedQuestions._Q3_AGENT_BOOSTS;
366      const newBoosts = {};
367      Object.keys(oldBoosts).forEach(key => {
368        if (key.includes(':')) newBoosts[key] = [...(oldBoosts[key] || [])];
369      });
370      Object.keys(oldBoosts).forEach(key => {
371        if (key.includes(':')) return;
372        if (key === 'any') return;
373        const q3val = key;
374        const boostList = oldBoosts[key] || [];
375        Object.keys(optsByQ2).forEach(q2val => {
376          const opts = optsByQ2[q2val] || [];
377          if (opts.some(o => o.value === q3val)) {
378            const composite = `${q2val}:${q3val}`;
379            if (!newBoosts[composite]) newBoosts[composite] = [...boostList];
380          }
381        });
382      });
383      return { ...savedQuestions, _Q3_AGENT_BOOSTS: newBoosts };
384    }
385
386    const _savedQuestions = (() => {
387      try {
388        const raw = JSON.parse(localStorage.getItem('af_questions'));
389        return _migrateQ3Boosts(raw);
390      } catch(e) { return null; }
391    })();
392
393    // Q1: mode別に状況を聞く
394    const Q1_FREELANCE = _savedQuestions?.Q1_FREELANCE || {
395      id: "q1", title: "今の状況は?",
396      subtitle: "最も近いものをお選びください",
397      type: "single",
398      options: [
399        { value: "searching", label: "フリーランスとして案件を探している" },
400        { value: "considering", label: "独立を検討中(まだ会社員)" },
401        { value: "ending_soon", label: "今の案件が終わりそう / 次を探したい" },
402        { value: "want_better", label: "単価を上げたい / エージェントを変えたい" },
403      ]
404    };
405
406    const Q1_TENSHOKU = _savedQuestions?.Q1_TENSHOKU || {
407      id: "q1", title: "あなたの経験レベルは?",
408      subtitle: "最も近いものをお選びください",
409      type: "single",
410      options: [
411        { value: "tenshoku_20s", label: "20代・第二新卒(社会人経験3年未満)" },
412        { value: "young_mid", label: "若手〜中堅(経験3〜10年)" },
413        { value: "senior", label: "ベテラン・管理職(経験10年以上)" },
414        { value: "career_change", label: "キャリアチェンジ(異業種からの転職)" },
415      ]
416    };
417
418    // Q2: Q1の回答に応じて選択肢が変わる
419    const Q2_FREELANCE = _savedQuestions?.Q2_FREELANCE || {
420      id: "q2", title: "どんな案件をやりたい?",
421      subtitle: "2つまで選択できます",
422      type: "multi", maxSelect: 2,
423      options: [
424        { value: "strategy", label: "戦略コンサル(戦略策定・新規事業・M&Aなど)" },
425        { value: "biz_consul", label: "業務コンサル(業務改善・BPRなど)" },
426        { value: "it_consul", label: "ITコンサル(システム導入・PMOなど)" },
427        { value: "engineering", label: "エンジニアリング(開発/インフラなど)" },
428        { value: "sap", label: "SAP案件" },
429        { value: "security", label: "セキュリティ案件" },
430      ]
431    };
432
433    const Q2_TENSHOKU = _savedQuestions?.Q2_TENSHOKU || {
434      id: "q2", title: "希望の職種は?",
435      subtitle: "2つまで選択できます",
436      type: "multi", maxSelect: 2,
437      options: [
438        { value: "tenshoku_strategy_consul", label: "戦略・経営コンサルタント" },
439        { value: "tenshoku_it_consul", label: "IT・DXコンサルタント" },
440        { value: "tenshoku_ma", label: "M&A・財務アドバイザリー(FAS/IBD/PE)" },
441        { value: "tenshoku_engineer", label: "エンジニア・IT技術職" },
442        { value: "tenshoku_sales", label: "営業・セールス" },
443        { value: "tenshoku_marketing", label: "マーケティング" },
444        { value: "tenshoku_accounting", label: "会計・税務・経理" },
445        { value: "tenshoku_other", label: "その他" },
446      ]
447    };
448
449    // Q3: フリーランスと転職で微妙にラベルを変える
450    const Q3_FREELANCE = _savedQuestions?.Q3_FREELANCE || {
451      id: "q3", title: "何を一番重視する?",
452      subtitle: "1つだけお選びください",
453      type: "single",
454      options: [
455        { value: "high_rate", label: "とにかく高単価(月150万〜)" },
456        { value: "stability", label: "案件が途切れない安定感" },
457        { value: "speed", label: "すぐに参画できるスピード" },
458        { value: "career_up", label: "キャリアアップ・スキルアップ" },
459      ]
460    };
461
462    // Q3はQ2の選択に応じて動的に選択肢を変える(Q3_OPTIONS_BY_Q2参照)
463    const Q3_TENSHOKU = _savedQuestions?.Q3_TENSHOKU || {
464      id: "q3", title: "希望の企業タイプは?",
465      subtitle: "1つお選びください",
466      type: "single",
467      options: [
468        { value: "any", label: "こだわらない" },
469      ]
470    };
471
472    // Q2選択→Q3選択肢の動的マッピング(管理画面で上書き可能)
473    const Q3_OPTIONS_BY_Q2 = _savedQuestions?._Q3_OPTIONS_BY_Q2 || {
474      tenshoku_strategy_consul: [
475        { value: "mbb", label: "MBB・戦略ファーム" },
476        { value: "big4", label: "Big4・総合ファーム" },
477        { value: "boutique", label: "ブティック系" },
478        { value: "enterprise_strategy", label: "事業会社(戦略ポジション)" },
479        { value: "any", label: "こだわらない" },
480      ],
481      tenshoku_it_consul: [
482        { value: "accenture_big4_dx", label: "アクセンチュア・Big4 DX" },
483        { value: "foreign_it", label: "外資IT系コンサル" },
484        { value: "japan_sier", label: "日系SIer上流" },
485        { value: "enterprise_dx", label: "事業会社(DX推進)" },
486        { value: "any", label: "こだわらない" },
487      ],
488      tenshoku_ma: [
489        { value: "foreign_ibd", label: "外資投資銀行(IBD)" },
490        { value: "japan_ibd", label: "日系証券IBD" },
491        { value: "fas", label: "FAS(Big4アドバイザリー)" },
492        { value: "pe_fund", label: "PEファンド" },
493        { value: "enterprise_ma", label: "事業会社(M&A室)" },
494        { value: "any", label: "こだわらない" },
495      ],
496      tenshoku_engineer: [
497        { value: "mega_venture", label: "メガベンチャー" },
498        { value: "startup", label: "スタートアップ" },
499        { value: "foreign_it_big", label: "外資IT(GAFA系)" },
500        { value: "japan_large", label: "日系大手" },
501        { value: "sier", label: "SIer" },
502        { value: "any", label: "こだわらない" },
503      ],
504      tenshoku_sales: [
505        { value: "foreign", label: "外資系" },
506        { value: "japan_large", label: "日系大手" },
507        { value: "startup", label: "スタートアップ・ベンチャー" },
508        { value: "any", label: "こだわらない" },
509      ],
510      tenshoku_marketing: [
511        { value: "foreign", label: "外資系" },
512        { value: "japan_large", label: "日系大手" },
513        { value: "startup", label: "スタートアップ・ベンチャー" },
514        { value: "any", label: "こだわらない" },
515      ],
516      tenshoku_accounting: [
517        { value: "big4_audit", label: "Big4・監査法人" },
518        { value: "fas", label: "FAS" },
519        { value: "enterprise_finance", label: "事業会社(経理財務)" },
520        { value: "tax_firm", label: "税理士法人" },
521        { value: "any", label: "こだわらない" },
522      ],
523      tenshoku_other: [
524        { value: "foreign", label: "外資系" },
525        { value: "japan_large", label: "日系大手" },
526        { value: "startup", label: "スタートアップ" },
527        { value: "any", label: "こだわらない" },
528      ],
529    };
530
531    // Q3ブースト: キーは「q2val:q3val」の複合キー(Q2カテゴリごとに独立して設定可能)
532    // 管理画面で上書き可能
533    const Q3_AGENT_BOOSTS = _savedQuestions?._Q3_AGENT_BOOSTS || {
534      // 戦略コンサル
535      "tenshoku_strategy_consul:mbb": ["myvision", "concord", "axis"],
536      "tenshoku_strategy_consul:big4": ["myvision", "axis", "assign"],
537      "tenshoku_strategy_consul:boutique": ["laughlogic", "kotora"],
538      "tenshoku_strategy_consul:enterprise_strategy": ["kotora", "axis", "samurai"],
539      // IT/DXコンサル
540      "tenshoku_it_consul:accenture_big4_dx": ["myvision", "axis", "assign"],
541      "tenshoku_it_consul:foreign_it": ["concord", "samurai", "myvision"],
542      "tenshoku_it_consul:japan_sier": ["kotora"],
543      "tenshoku_it_consul:enterprise_dx": ["kotora", "samurai", "axis"],
544      // M&A
545      "tenshoku_ma:foreign_ibd": ["yhc", "concord", "newma"],
546      "tenshoku_ma:japan_ibd": ["yhc", "newma", "kotora"],
547      "tenshoku_ma:fas": ["myvision", "yhc", "ma_beginners"],
548      "tenshoku_ma:pe_fund": ["yhc", "concord", "kotora"],
549      "tenshoku_ma:enterprise_ma": ["kotora", "newma", "ma_beginners"],
550      // エンジニア
551      "tenshoku_engineer:mega_venture": ["techgo", "techgate_expert"],
552      "tenshoku_engineer:startup": ["techgo", "miracareer", "owen_career", "neocareer"],
553      "tenshoku_engineer:foreign_it_big": ["techgate_expert", "s
553amurai"],
554      "tenshoku_engineer:japan_large": ["meiko_career", "kotora"],
555      "tenshoku_engineer:sier": ["meiko_career"],
556      // 営業
557      "tenshoku_sales:foreign": ["samurai", "concord", "myvision"],
558      "tenshoku_sales:japan_large": ["meiko_career", "kotora"],
559      "tenshoku_sales:startup": ["assign", "miracareer", "myvision", "owen_career", "neocareer"],
560      // マーケティング
561      "tenshoku_marketing:foreign": ["samurai", "concord", "myvision"],
562      "tenshoku_marketing:japan_large": ["meiko_career", "kotora"],
563      "tenshoku_marketing:startup": ["assign", "miracareer", "myvision", "owen_career", "neocareer"],
564      // 会計
565      "tenshoku_accounting:big4_audit": ["twinpro", "myvision", "kotora"],
566      "tenshoku_accounting:fas": ["twinpro", "yhc", "ma_beginners"],
567      "tenshoku_accounting:enterprise_finance": ["twinpro", "kotora"],
568      "tenshoku_accounting:tax_firm": ["twinpro", "kotora"],
569      // その他
570      "tenshoku_other:foreign": ["samurai", "myvision"],
571      "tenshoku_other:japan_large": ["meiko_career", "kotora"],
572      "tenshoku_other:startup": ["miracareer", "owen_career", "neocareer"],
573    };
574
575    const Q4_TENSHOKU = _savedQuestions?.Q4_TENSHOKU || {
576      id: "q4", title: "転職で何を一番重視する?",
577      subtitle: "1つだけお選びください",
578      type: "single",
579      options: [
580        { value: "high_rate", label: "年収アップ(800万〜)" },
581        { value: "stability", label: "安定した環境・福利厚生" },
582        { value: "speed", label: "すぐに転職できるスピード" },
583        { value: "career_up", label: "キャリアアップ・スキルアップ" },
584      ]
585    };
586
587    // mode判定: URLパラメータ or パスで事前指定があればそれを使う、なければnull(選択画面を表示)
588    function getInitialMode() {
589      const params = new URLSearchParams(window.location.search);
590      if (params.get('mode') === 'tenshoku') return 'tenshoku';
591      if (params.get('mode') === 'freelance') return 'freelance';
592      if (params.get('mode') === 'tekisei') return 'tekisei';
593      if (window.location.pathname.includes('tenshoku')) return 'tenshoku';
594      if (window.location.pathname.includes('tekisei')) return 'tekisei';
595      return null; // 未選択 → 選択画面を表示
596    }
597    // 後方互換: getMode()は廃止しないがApp内でstateを使う
598    function getMode() {
599      return getInitialMode() || 'freelance';
600    }
601
602    // modeに応じてQUESTIONSを生成(3問構成)
603    function getQuestions(mode) {
604      const isTenshoku = mode === 'tenshoku';
605      if (isTenshoku) {
606        return [Q1_TENSHOKU, Q2_TENSHOKU, Q3_TENSHOKU, Q4_TENSHOKU];
607      }
608      return [Q1_FREELANCE, Q2_FREELANCE, Q3_FREELANCE];
609    }
610
611    // ============================================================
612    // DATA: ロジック設定(localStorageから読み込み、フォールバックはハードコード)
613    // ============================================================
614    const _savedLogic = (() => {
615      try { return JSON.parse(localStorage.getItem('af_logic')); } catch(e) { return null; }
616    })();
617
618    const Q2_REQUIRED_AGENTS = _savedLogic?.requiredAgents || {
619      strategy: ["senjin", "highperfo", "lancers_pro", "freeconsultant_jp", "proconnect", "freeconsul_biz", "strategy_bank", "forpro", "pro_hub"],
620      biz_consul: ["senjin", "highperfo", "lancers_pro", "freeconsultant_jp", "pmo_jobs", "proconnect", "freeconsul_biz", "strategy_bank", "forpro", "pro_hub"],
621      it_consul: ["senjin_it", "highperfo", "freeconsultant_jp", "lancers_pro", "pmo_jobs", "proconnect", "pro_hub"],
622      engineering: ["levatech", "midworks", "techstock", "techfree", "techadapt", "itpro_partners", "bigdata_navi"],
623      sap: ["senjin_it", "highperfo", "levatech", "lancers_pro", "freeconsultant_jp", "proconnect", "techstock", "forpro"],
624      security: ["senjin_security", "levatech", "highperfo", "freeconsultant_jp", "lancers_pro", "proconnect", "techstock", "bigdata_navi"],
625      tenshoku_strategy_consul: ["myvision", "yhc", "axis", "concord", "assign", "samurai", "kotora", "laughlogic"],
626      tenshoku_it_consul: ["myvision", "axis", "yhc", "assign", "kotora", "samurai", "sap_tenshoku"],
627      tenshoku_engineer: ["techgo", "techgate_expert", "meiko_career", "shanai_se", "techclips", "sap_tenshoku", "raku_partners"],
628      tenshoku_ma: ["newma", "myvision", "yhc", "concord", "ma_beginners", "kotora"],
629      tenshoku_accounting: ["twinpro", "myvision", "kotora", "yhc"],
630      tenshoku_20s: ["miracareer", "owen_career", "neocareer", "assign", "myvision", "raku_partners"],
631      young_mid: ["myvision", "yhc", "axis", "assign", "samurai", "kotora", "techgo", "techgate_expert"],
632      senior: ["myvision", "yhc", "axis", "concord", "kotora", "samurai", "hr_career_agent"],
633      career_change: ["miracareer", "owen_career", "neocareer", "myvision", "assign", "raku_partners"],
634      tenshoku_sales: ["myvision", "assign", "yhc", "samurai", "newma", "miracareer", "owen_career", "neocareer"],
635      tenshoku_marketing: ["myvision", "axis", "assign", "yhc", "samurai", "kotora"],
636      tenshoku_other: ["myvision", "axis", "assign", "yhc", "samurai", "kotora", "miracareer", "owen_career", "neocareer"],
637      tenshoku_consul_firm: ["myvision", "yhc", "axis", "concord", "assign", "samurai", "kotora", "laughlogic"],
638      tenshoku_ibd_pe: ["yhc", "newma", "myvision", "concord", "kotora", "ma_beginners"],
639      tenshoku_startup: ["myvision", "yhc", "axis", "assign", "samurai", "miracareer", "owen_career", "neocareer"],
640      tenshoku_enterprise: ["myvision", "yhc", "axis", "assign", "samurai", "kotora", "hr_career_agent", "newma", "twinpro", "miracareer", "owen_career", "neocareer"],
641      tenshoku_foreign: ["myvision", "yhc", "axis", "concord", "assign", "samurai", "kotora"],
642      tenshoku_any: ["myvision", "assign", "yhc", "samurai"],
643    };
644
645    const _domainMap = _savedLogic?.domainMap || {
646      strategy: ["strategy", "bizdev", "consulting"],
647      biz_consul: ["bpr", "consulting", "pmo"],
648      it_consul: ["it", "dx", "pmo", "consulting"],
649      engineering: ["it", "engineer", "dx"],
650      sap: ["sap", "erp"],
651      security: ["security", "it"],
652      tenshoku_strategy_consul: ["tenshoku", "consulting", "strategy"],
653      tenshoku_it_consul: ["tenshoku", "consulting", "it", "dx"],
654      tenshoku_engineer: ["tenshoku", "engineer", "it"],
655      tenshoku_ma: ["tenshoku", "finance", "consulting"],
656      tenshoku_accounting: ["tenshoku", "finance", "accounting"],
657      tenshoku_20s: ["tenshoku", "office", "career_change"],
658      young_mid: ["tenshoku", "consulting", "it"],
659      senior: ["tenshoku", "consulting", "strategy"],
660      career_change: ["tenshoku", "office", "career_change"],
661      tenshoku_sales: ["tenshoku", "sales", "bizdev"],
662      tenshoku_marketing: ["tenshoku", "marketing", "digital"],
663      tenshoku_consul_firm: ["tenshoku", "consulting", "strategy"],
664      tenshoku_ibd_pe: ["tenshoku", "finance", "consulting"],
665      tenshoku_startup: ["tenshoku", "consulting", "it"],
666      tenshoku_enterprise: ["tenshoku", "consulting", "finance"],
667      tenshoku_foreign: ["tenshoku", "consulting", "strategy"],
668      tenshoku_any: ["tenshoku"],
669      tenshoku_other: ["tenshoku"],
670      // フリーランスQ1(スコアリングには影響しないがマップの整合性のため)
671      searching: ["consulting", "it", "engineer"],
672      considering: ["consulting", "it"],
673      ending_soon: ["consulting", "it", "engineer"],
674      want_better: ["consulting", "it", "engineer"],
675    };
676
677    const _goalMap = _savedLogic?.goalMap || {
678      high_rate: "high_rate",
679      stability: "stability",
680      speed: "speed",
681      career_up: "career_up",
682    };
683
684    const _weights = _savedLogic?.weights || { domain: 50, goal: 35, base: 15 };
685
686    // ============================================================
687    // UTILS: スコアリングロジック
688    // ============================================================
689    // Q1: 状況 → スコアリングには直接影響しない(全員同じ扱い)
690    // ただし将来の拡張用にマッピングは残す
691    function mapSituation(val) {
692      return val || "searching";
693    }
694
695    // Q2: やりたい案件 → ドメインマッチ
696    function mapDomain(val) {
697      return _domainMap[val] || [];
698    }
699
700    // Q3: 重視ポイント → ゴールマッチ
701    function mapGoal(val) {
702      return _goalMap[val] || "stability";
703    }
704
705    function calculateScore(agent, answers) {
706      let score = 0;
707
708      // Q2: 案件領域マッチ(複数選択対応)- 最重要
709      if (answers.q2 && answers.q2.length > 0) {
710        let domainMatchCount = 0;
711        answers.q2.forEach(selectedDomain => {
712          const mappedDomains = mapDomain(selectedDomain);
713          const hasMatch = mappedDomains.some(d =>
714            agent.targetDomain.includes(d) || agent.categories.includes(d)
715          );
716          if (hasMatch) domainMatchCount++;
717        });
718        score += _weights.domain * (domainMatchCount / answers.q2.length);
719      }
720
721      // Q3/Q4: 重視ポイントマッチ(転職モードではQ4がgoal、フリーランスではQ3がgoal)
722      const goalAnswer = answers.q4 || (typeof answers.q3 === 'string' ? answers.q3 : null);
723      if (goalAnswer) {
724        const goal = mapGoal(goalAnswer);
725        if (agent.targetGoal.includes(goal)) {
726          score += _weights.goal;
727        } else {
728          score += _weights.goal * 0.2;
729        }
730      }
731
732      // ベーススコア(優先度による)
733      score += _weights.base * ((10 - agent.priority) / 10);
734
735      return Math.min(100, Math.round(score));
736    }
737
738    // ============================================================
739    // UTILS: 設定解決
740    // ============================================================
741    function getConfig() {
742      const params = new URLSearchParams(window.location.search);
743      const mode = getMode(); // getMode()を使うことでパス名判定も含む
744      const configKey = params.get('config') || (mode === 'tenshoku' ? 'tenshoku_strategy_consul' : 'strategy');
745      return ARTICLE_CONFIGS[configKey] || ARTICLE_CONFIGS.strategy;
746    }
747
748    function resolveAgents(config) {
749      return config.agents.map(cfgAgent => {
750        const master = AGENTS_MASTER.find(a => a.id === cfgAgent.id);
751        if (!master) {
752          // マスターに未登録のエージェント → configデータだけで生成(管理画面で追加されたケース)
753          return {
754            id: cfgAgent.id,
755            name: cfgAgent.nameOverride || cfgAgent.name || cfgAgent.id,
756            shortName: cfgAgent.shortName || cfgAgent.nameOverride || cfgAgent.name || cfgAgent.id,
757            rank: cfgAgent.rank || 'B',
758            rating: cfgAgent.rating || 4.0,
759            affiliateUrl: cfgAgent.affiliateUrl || '',
760            tagline: cfgAgent.tagline || '',
761            monthlyRate: cfgAgent.monthlyRate || '',
762            strengths: cfgAgent.strengths || [],
763            categories: [],
764            targetExperience: ['senior', 'mid'],
765            targetWorkStyle: ['onsite', 'hybrid'],
766            targetDomain: [],
767            targetGoal: ['career_up'],
768            priority: cfgAgent.priority ?? 99,
769            badge: cfgAgent.badge || '',
770            ctaText: cfgAgent.ctaText || '無料登録',
771          };
772        }
773        return {
774          ...master,
775          // マスターデータが常に優先(rank, rating, strengths, targetDomain等)
776          rank: master.rank,
777          rating: master.rating,
778          targetDomain: master.targetDomain,
779          targetGoal: master.targetGoal,
780          categories: master.categories,
781          strengths: master.strengths,
782          monthlyRate: master.monthlyRate,
783          tagline: master.tagline,
784          // マスターデータのURLを優先(領域別設定のURLは使わない)
785          affiliateUrl: master.affiliateUrl,
786          // 領域別設定で上書き可能な項目(順番のみ)
787          priority: cfgAgent.priority ?? master.priority,
788          badge: master.badge || '',
789          ctaText: master.ctaText || '無料登録',
790        };
791      }).filter(Boolean).sort((a, b) => a.priority - b.priority);
792    }
793
794    // ============================================================
795    // UTILS: iframe高さ自動通知
796    // ============================================================
797    function notifyHeight() {
798      try {
799        const height = document.getElementById('root').scrollHeight;
800        window.parent.postMessage({ type: 'resize', height: height + 40 }, '*');
801      } catch (e) { /* ignore */ }
802    }
803
804    // ============================================================
805    // COMPONENT: 進捗ドット
806    // ============================================================
807    function ProgressDots({ total, current, completedCount }) {
808      return (
809        <div className="flex items-center justify-center gap-2 my-4">
810          {Array.from({ length: total }, (_, i) => (
811            <div
812              key={i}
813              className={`w-3 h-3 rounded-full transition-all duration-300 ${
814                i < completedCount ? 'bg-green-500' :
815                i === current ? 'bg-brand-gold w-4 h-4' :
816                'bg-gray-300'
817              }`}
818            />
819          ))}
820        </div>
821      );
822    }
823
824    // ============================================================
825    // COMPONENT: 質問カード
826    // ============================================================
827    function QuestionCard({ question, questionIndex, totalQuestions, answer, onAnswer, onSkip }) {
828      const [selectedValues, setSelectedValues] = useState(
829        question.type === 'multi' ? (answer || []) : []
830      );
831      const [animating, setAnimating] = useState(null);
832
833      const handleSingleSelect = (value) => {
834        setAnimating(value);
835        setTimeout(() => {
836          onAnswer(question.id, value);
837        }, 300);
838      };
839
840      const handleMultiToggle = (value) => {
841        setSelectedValues(prev => {
842          if (prev.includes(value)) {
843            return prev.filter(v => v !== value);
844          }
845          if (prev.length >= (question.maxSelect || 2)) {
846            return prev;
847          }
848          return [...prev, value];
849        });
850      };
851
852      const handleMultiSubmit = () => {
853        if (selectedValues.length > 0) {
854          onAnswer(question.id, selectedValues);
855        }
856      };
857
858      return (
859        <div className="animate-fade-in-up">
860          <div className="text-center mb-4">
861            <div className="text-[11px] text-gray-500 mb-1">
862              質問 {questionIndex + 1} / {totalQuestions}
863              {questionIndex < totalQuestions - 1 && ` — あと${totalQuestions - questionIndex - 1}問`}
864            </div>
865            <h2 className="text-base font-bold text-gray-800 mb-1 leading-snug">{question.title}</h2>
866            <p className="text-[11px] text-gray-500">{question.subtitle}</p>
867          </div>
868
869          <div className="space-y-2">
870            {question.options.map((opt, optIdx) => (
871              <React.Fragment key={opt.value}>
872                {opt.sectionLabel && (
873                  <div className="flex items-center gap-2 py-0.5">
874                    <div className="flex-1 border-t border-gray-200"></div>
875                    <span className="text-[10px] text-gray-400 whitespace-nowrap">{opt.sectionLabel}</span>
876                    <div className="flex-1 border-t border-gray-200"></div>
877                  </div>
878                )}
879                <button
880                  onClick={() => question.type === 'single' ? handleSingleSelect(opt.value) : handleMultiToggle(opt.value)}
881                  className={`w-full text-left px-3.5 py-3 rounded-xl border-2 transition-all duration-200 ${
882                    question.type === 'single' && animating === opt.value
883                      ? 'border-brand-gold bg-yellow-50 animate-bounce-light'
884                      : question.type === 'multi' && selectedValues.includes(opt.value)
885                      ? 'border-brand-gold bg-yellow-50'
886                      : 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
887                  }`}
888                >
889                  <div className="flex items-center gap-2.5">
890                    {question.type === 'multi' && (
891                      <div className={`w-4 h-4 rounded border-2 flex items-center justify-center transition-all shrink-0 ${
892                        selectedValues.includes(opt.value)
893                          ? 'border-brand-gold bg-brand-gold'
894                          : 'border-gray-300'
895                      }`}>
896                        {selectedValues.includes(opt.value) && (
897                          <svg className="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
898                            <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
899                          </svg>
900                        )}
901                      </div>
902                    )}
903                    <span className="text-gray-700 font-medium text-[13px] leading-snug">{opt.label}</span>
904                  </div>
905                </button>
906              </React.Fragment>
907            ))}
908          </div>
909
910          {question.type === 'multi' && (
911            <button
912              onClick={handleMultiSubmit}
913              disabled={selectedValues.length === 0}
914              className={`w-full mt-3 py-3 rounded-xl font-bold text-white text-sm transition-all ${
915                selectedValues.length > 0
916                  ? 'bg-brand-gold hover:bg-brand-gold-dark'
917                  : 'bg-gray-300 cursor-not-allowed'
918              }`}
919            >
920              {selectedValues.length > 0 ? `${selectedValues.length}つ選択して次へ` : '1つ以上選択してください'}
921            </button>
922          )}
923
924          {question.optional && (
925            <button
926              onClick={() => onSkip(question.id)}
927              className="w-full mt-2 py-1.5 text-[11px] text-gray-500 hover:text-gray-700 transition-colors"
928            >
929              スキップして次へ →
930            </button>
931          )}
932        </div>
933      );
934    }
935
936    // ============================================================
937    // COMPONENT: ランクバッジ
938    // ============================================================
939    function RankBadge({ rank }) {
940      const colors = {
941        S: 'bg-gradient-to-r from-yellow-400 to-yellow-600 text-white',
942        A: 'bg-gradient-to-r from-blue-400 to-blue-600 text-white',
943        B: 'bg-gradient-to-r from-gray-400 to-gray-600 text-white',
944      };
945      return (
946        <span className={`inline-flex items-center justify-center w-6 h-6 rounded-md text-xs font-bold shrink-0 whitespace-nowrap shadow-sm ${colors[rank] || colors.B}`}>
947          {rank}
948        </span>
949      );
950    }
951
952    // ============================================================
953    // COMPONENT: エージェントカード(1社ずつ提案)
954    // ============================================================
955    function AgentCard({ agent, matchScore, status, order, totalCount, onCtaClick, onRegistered, onSkip }) {
956      const [ctaAnimating, setCtaAnimating] = useState(false);
957
958      const handleCtaClick = () => {
959        setCtaAnimating(true);
960        setTimeout(() => {
961          setCtaAnimating(false);
962          onCtaClick(agent);
963        }, 200);
964      };
965
966      return (
967        <div className="animate-fade-in-up">
968          <div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
969            {/* ヘッダー */}
970            <div className="bg-gradient-to-r from-gray-50 to-gray-100 px-5 py-3 flex items-center justify-between">
971              <div className="flex items-center gap-2">
972                <span className="text-lg font-bold text-gray-700">#{order}</span>
973                <RankBadge rank={agent.rank} />
974              </div>
975              <div className="text-right">
976                <span className="text-sm text-gray-500">おすすめ度</span>
977                <span className={`ml-1 text-lg font-bold ${
978                  matchScore >= 80 ? 'text-green-600' : matchScore >= 60 ? 'text-brand-gold' : 'text-gray-600'
979                }`}>{matchScore}%</span>
980              </div>
981            </div>
982
983            {/* バッジ */}
984            {agent.badge && (
985              <div className="px-5 pt-3">
986                <span className="inline-block bg-yellow-50 border border-yellow-200 text-yellow-800 text-xs font-bold px-3 py-1 rounded-full">
987                  {agent.badge}
988                </span>
989              </div>
990            )}
991
992            {/* コンテンツ */}
993            <div className="p-5">
994              <h3 className="text-lg font-bold text-gray-800 mb-1">{agent.name}</h3>
995              <p className="text-sm text-gray-500 mb-3">{agent.tagline}</p>
996
997              <div className="flex items-center gap-2 mb-4">
998                <span className="text-sm text-gray-500">💰</span>
999                <span className="text-sm font-bold text-gray-700">{agent.monthlyRate}</span>
1000              </div>
1001
1002              {/* 強み */}
1003              <div className="flex flex-wrap gap-2 mb-5">
1004                {agent.strengths.map((s, i) => (
1005                  <span key={i} className="inline-flex items-center text-xs bg-green-50 text-green-700 px-2 py-1 rounded-lg">
1006                    ✅ {s}
1007                  </span>
1008                ))}
1009              </div>
1010
1011              {/* CTAボタン */}
1012              {status === 'registered' ? (
1013                <div className="w-full py-3 rounded-xl bg-green-100 text-green-700 font-bold text-center animate-pop-in">
1014                  ✅ 登録済み
1015                </div>
1016              ) : (
1017                <>
1018                  <button
1019                    onClick={handleCtaClick}
1020                    className={`w-full py-3.5 rounded-xl font-bold text-white transition-all text-base ${
1021                      ctaAnimating
1022                        ? 'bg-brand-orange-dark animate-pulse-once'
1023                        : 'bg-brand-orange hover:bg-brand-orange-dark hover:shadow-lg'
1024                    }`}
1025                  >
1026                    ▶ {agent.ctaText}
1027                  </button>
1028
1029                  <div className="flex gap-3 mt-3">
1030                    <button
1031                      onClick={() => onRegistered(agent.id)}
1032                      className={`flex-1 py-2 rounded-lg text-sm font-medium transition-all ${
1033                        status === 'clicked'
1034                          ? 'bg-green-50 text-green-700 border-2 border-green-300 hover:bg-green-100'
1035                          : 'bg-gray-50 text-gray-500 border border-gray-200 hover:bg-gray-100'
1036                      }`}
1037                    >
1038                      ✓ 登録した
1039                    </button>
1040                    <button
1041                      onClick={() => onSkip(agent.id)}
1042                      className="flex-1 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-600 hover:bg-gray-50 border border-gray-200 transition-all"
1043                    >
1044                      スキップ → 次へ
1045                    </button>
1046                  </div>
1047                </>
1048              )}
1049            </div>
1050          </div>
1051
1052          {/* 進捗 */}
1053          <ProgressDots
1054            total={totalCount}
1055            current={order - 1}
1056            completedCount={order - 1}
1057          />
1058
1059          <p className="text-center text-xs text-gray-400 mt-1">
1060            💡 複数社に登録すると案件の選択肢が広がります
1061          </p>
1062        </div>
1063      );
1064    }
1065
1066    // ============================================================
1067    // COMPONENT: ステータスサマリー
1068    // ============================================================
1069    function StatusSummary({ agents, statuses, onBulkOpen }) {
1070      const registeredCount = Object.values(statuses).filter(s => s.status === 'registered').length;
1071      const clickedNotRegistered = agents.filter(a => {
1072        const s = statuses[a.id];
1073        return s && s.status === 'clicked';
1074      });
1075      const pending = agents.filter(a => {
1076        const s = statuses[a.id];
1077        return !s || s.status === 'pending';
1078      });
1079      const notDone = [...clickedNotRegistered, ...pending];
1080
1081      return (
1082        <div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-5 animate-fade-in">
1083          <h3 className="text-base font-bold text-gray-800 mb-4">📊 あなたの登録ステータス</h3>
1084
1085          <div className="space-y-2 mb-5">
1086            {agents.map(agent => {
1087              const s = statuses[agent.id];
1088              const status = s ? s.status : 'pending';
1089              const icon = status === 'registered' ? '✅' : status === 'clicked' ? '🔵' : status === 'skipped' ? '⏭️' : '⬜';
1090              const label = status === 'registered' ? '登録済み' : status === 'clicked' ? '確認中' : status === 'skipped' ? 'スキップ' : 'まだ';
1091              const textColor = status === 'registered' ? 'text-green-600' : status === 'clicked' ? 'text-blue-600' : 'text-gray-400';
1092              return (
1093                <div key={agent.id} className="flex items-center justify-between py-1.5">
1094                  <span className="text-sm text-gray-700">{icon} {agent.name}</span>
1095                  <span className={`text-xs font-medium ${textColor}`}>{label}</span>
1096                </div>
1097              );
1098            })}
1099          </div>
1100
1101          {notDone.length > 0 && (
1102            <button
1103              onClick={() => onBulkOpen(notDone)}
1104              className="w-full py-3 rounded-xl bg-brand-gold hover:bg-brand-gold-dark text-white font-bold transition-all text-sm"
1105            >
1106              ☑ 未登録の{notDone.length}社を一気に開く
1107            </button>
1108          )}
1109
1110          {registeredCount > 0 && registeredCount < agents.length && (
1111            <p className="text-center text-xs text-gray-500 mt-3">
1112              💡 {registeredCount}社登録完了!あと{agents.length - registeredCount}社登録すると、最適な案件に出会う確率がさらにUP
1113            </p>
1114          )}
1115
1116          {registeredCount >= 3 && (
1117            <p className="text-center text-xs text-green-600 font-medium mt-3">
1118              🎉 {registeredCount}社登録完了!案件の比較検討が可能です
1119            </p>
1120          )}
1121        </div>
1122      );
1123    }
1124
1125    // ============================================================
1126    // COMPONENT: 一括登録パネル(Module B)
1127    // ============================================================
1128    function BulkRegistrationPanel({ agents, statuses, onBulkOpen }) {
1129      const handleClick = (agent) => {
1130        if (agent.affiliateUrl) {
1131          window.open(agent.affiliateUrl, '_blank');
1132          onBulkOpen([agent]);
1133        }
1134      };
1135
1136      return (
1137        <div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
1138          <div className="bg-gradient-to-r from-brand-gold/10 to-brand-orange/10 px-5 py-4">
1139            <h3 className="text-lg font-bold text-gray-800">📋 その他のおすすめエージェント</h3>
1140            <p className="text-xs text-gray-500 mt-1">比較検討のためにこちらも合わせて登録がおすすめです</p>
1141          </div>
1142
1143          <div className="p-4 space-y-2">
1144            {agents.map((agent) => {
1145              const s = statuses[agent.id];
1146              const isClicked = s && (s.status === 'clicked' || s.status === 'registered');
1147              const hasUrl = agent.affiliateUrl && agent.affiliateUrl.length > 0;
1148              return (
1149                <div
1150                  key={agent.id}
1151                  className={`flex items-center gap-3 p-3 rounded-xl border transition-all ${
1152                    isClicked ? 'border-green-200 bg-green-50' : 'border-gray-200 bg-white'
1153                  }`}
1154                >
1155                  <div className="flex-1 min-w-0">
1156                    <div className="flex items-center gap-2">
1157                      <span className="text-sm font-bold text-gray-700">{agent.name}</span>
1158                      <RankBadge rank={agent.rank} />
1159                      {isClicked && <span className="text-xs text-green-600">✅</span>}
1160                    </div>
1161                    <div className="text-xs text-gray-500">💰 {agent.monthlyRate}</div>
1162                  </div>
1163                  {hasUrl ? (
1164                    <button
1165                      onClick={() => handleClick(agent)}
1166                      className={`shrink-0 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
1167                        isClicked
1168                          ? 'bg-green-100 text-green-600 border border-green-200'
1169                          : 'bg-brand-orange text-white hover:bg-brand-orange-dark shadow-sm'
1170                      }`}
1171                    >
1172                      {isClicked ? '登録済' : '登録 →'}
1173                    </button>
1174                  ) : (
1175                    <span className="shrink-0 px-3 py-1.5 rounded-lg text-xs text-gray-400 bg-gray-100">準備中</span>
1176                  )}
1177                </div>
1178              );
1179            })}
1180          </div>
1181        </div>
1182      );
1183    }
1184
1185    // ============================================================
1186    // COMPONENT: 結果カード(一覧用コンパクト版)
1187    // ============================================================
1188    function AgentResultCard({ agent, matchScore, order, status, onCtaClick }) {
1189      return (
1190        <div className={`bg-white rounded-2xl shadow-md border border-gray-100 overflow-hidden animate-fade-in-up stagger-${Math.min(order, 5)}`}>
1191          {/* ヘッダー */}
1192          <div className="bg-gradient-to-r from-gray-50 to-gray-100 px-4 py-2.5 flex items-center justify-between gap-2">
1193            <div className="flex items-center gap-2 min-w-0 flex-1">
1194              <span className={`text-lg font-bold shrink-0 ${order <= 3 ? 'text-brand-gold' : 'text-gray-500'}`}>#{order}</span>
1195              <RankBadge rank={agent.rank} />
1196              {agent.badge && (
1197                <span className="text-[11px] bg-yellow-50 border border-yellow-200 text-yellow-800 font-bold px-2 py-0.5 rounded-full truncate">
1198                  {agent.badge}
1199                </span>
1200              )}
1201            </div>
1202            <div className="text-right shrink-0">
1203              <span className="text-xs text-gray-500">おすすめ度</span>
1204              <span className={`ml-1 text-base font-bold ${
1205                matchScore >= 80 ? 'text-green-600' : matchScore >= 60 ? 'text-brand-gold' : 'text-gray-600'
1206              }`}>{matchScore}%</span>
1207            </div>
1208          </div>
1209
1210          {/* コンテンツ */}
1211          <div className="p-4">
1212            <h3 className="text-base font-bold text-gray-800 mb-0.5">{agent.name}</h3>
1213            <p className="text-xs text-gray-500 mb-2">{agent.tagline}</p>
1214
1215            <div className="flex items-center gap-2 mb-3">
1216              <span className="text-xs text-gray-500">💰</span>
1217              <span className="text-xs font-bold text-gray-700">{agent.monthlyRate}</span>
1218            </div>
1219
1220            {/* 強み */}
1221            <div className="flex flex-wrap gap-1.5 mb-4">
1222              {agent.strengths.map((s, i) => (
1223                <span key={i} className="inline-flex items-center text-xs bg-green-50 text-green-700 px-2 py-0.5 rounded-lg">
1224                  ✅ {s}
1225                </span>
1226              ))}
1227            </div>
1228
1229            {/* CTAボタン */}
1230            {status === 'registered' ? (
1231              <div className="w-full py-2.5 rounded-xl bg-green-100 text-green-700 font-bold text-center text-sm">
1232                ✅ 登録済み
1233              </div>
1234            ) : (
1235              <button
1236                onClick={() => onCtaClick(agent)}
1237                className="w-full py-2.5 rounded-xl bg-brand-orange hover:bg-brand-orange-dark text-white font-bold text-sm transition-all hover:shadow-lg"
1238              >
1239                ▶ {agent.ctaText}
1240              </button>
1241            )}
1242          </div>
1243        </div>
1244      );
1245    }
1246
1247    // ============================================================
1248    // COMPONENT: 一括登録画面(チェック付き)
1249    // ============================================================
1250    function BulkOnlyView({ categories, statuses, updateStatus, onBack }) {
1251      // カテゴリごとのチェック状態: { catKey: { agentId: boolean } }
1252      const [checked, setChecked] = useState(() => {
1253        const init = {};
1254        categories.forEach(cat => {
1255          init[cat.key] = {};
1256          cat.agents.forEach((a, i) => { init[cat.key][a.id] = i < 3 && !!a.affiliateUrl; });
1257        });
1258        return init;
1259      });
1260      const [fallbacks, setFallbacks] = useState([]);
1261
1262      const toggleAgent = (catKey, agentId) => {
1263        setChecked(prev => ({
1264          ...prev,
1265          [catKey]: { ...prev[catKey], [agentId]: !prev[catKey]?.[agentId] }
1266        }));
1267      };
1268
1269      const selectAllCat = (catKey, agents) => {
1270        setChecked(prev => {
1271          const next = { ...prev, [catKey]: {} };
1272          agents.forEach(a => { next[catKey][a.id] = !!a.affiliateUrl; });
1273          return next;
1274        });
1275      };
1276
1277      const deselectAllCat = (catKey, agents) => {
1278        setChecked(prev => {
1279          const next = { ...prev, [catKey]: {} };
1280          agents.forEach(a => { next[catKey][a.id] = false; });
1281          return next;
1282        });
1283      };
1284
1285      const getCheckedAgents = (catKey, agents) => agents.filter(a => checked[catKey]?.[a.id] && a.affiliateUrl);
1286
1287      const bulkOpenCat = (catKey, agents) => {
1288        const selected = getCheckedAgents(catKey, agents);
1289        if (selected.length === 0) return;
1290        selected.forEach(agent => {
1291          const a = document.createElement('a');
1292          a.href = agent.affiliateUrl;
1293          a.target = '_blank';
1294          a.rel = 'noopener noreferrer';
1295          a.style.display = 'none';
1296          document.body.appendChild(a);
1297          try { a.click(); } catch(e) {}
1298          document.body.removeChild(a);
1299          updateStatus(agent.id, 'clicked');
1300        });
1301      };
1302
1303      return (
1304        <div className="max-w-lg mx-auto p-4 space-y-4">
1305          <div className="text-right"><span className="text-xs text-gray-400">PR</span></div>
1306          <div className="flex items-center justify-between">
1307            <button onClick={onBack} className="text-sm text-gray-400 hover:text-gray-700 transition-colors">
1308              ← 診断に戻る
1309            </button>
1310            <h2 className="text-base font-bold text-gray-800">領域別おすすめエージェント</h2>
1311          </div>
1312
1313          {/* 登録リンクリスト */}
1314          {fallbacks.length > 0 && (
1315            <div className="bg-yellow-50 border border-yellow-200 rounded-xl p-4">
1316              <p className="text-sm font-bold text-yellow-800 mb-1">📋 {fallbacks.length}社の登録リンク</p>
1317              <p className="text-xs text-yellow-700 mb-3">ポップアップがブロックされました。以下をタップして開いてください:</p>
1318              <div className="space-y-2">
1319                {fallbacks.map((a, idx) => (
1320                  <a key={a.id} href={a.affiliateUrl} target="_blank" rel="noopener noreferrer"
1321                    onClick={() => updateStatus(a.id, 'clicked')}
1322                    className="flex items-center gap-2 p-3 bg-white border border-yellow-300 rounded-xl text-sm font-bold text-gray-700 hover:bg-yellow-100 transition-all shadow-sm">
1323                    <span className={`shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-[11px] font-bold ${
1324                      idx === 0 ? 'bg-brand-gold text-white' : 'bg-gray-100 text-gray-500'
1325                    }`}>{idx + 1}</span>
1326                    <span className="flex-1">{a.name}</span>
1327                    <span className="text-xs text-green-600 font-bold whitespace-nowrap">無料登録 →</span>
1328                  </a>
1329                ))}
1330              </div>
1331              <button onClick={() => setFallbacks([])} className="w-full mt-3 text-xs text-gray-400 hover:text-gray-600 hover:underline">閉じる</button>
1332            </div>
1333          )}
1334
1335          {categories.map(cat => {
1336            const checkedCount = getCheckedAgents(cat.key, cat.agents).length;
1337            return (
1338              <div key={cat.key} className="bg-white rounded-2xl shadow border border-gray-100 overflow-hidden">
1339                <div className="px-5 py-3 bg-gray-50 border-b border-gray-100 flex items-center justify-between">
1340                  <div>
1341                    <div className="flex items-center gap-2">
1342                      <span className="text-lg">{cat.icon}</span>
1343                      <h3 className="text-sm font-bold text-gray-800">{cat.label}</h3>
1344                    </div>
1345                    <p className="text-[11px] text-gray-400 mt-0.5">{cat.desc}</p>
1346                  </div>
1347                  <span className="text-[11px] text-gray-400 bg-gray-100 px-2 py-0.5 rounded">{cat.agents.length}社</span>
1348                </div>
1349                <div className="divide-y divide-gray-50">
1350                  {cat.agents.map((agent, idx) => {
1351                    const st = statuses[agent.id];
1352                    const isClicked = st?.status === 'clicked';
1353                    const isChecked = !!checked[cat.key]?.[agent.id];
1354                    return (
1355                      <div key={agent.id} className="px-4 py-3 flex items-center gap-2.5">
1356                        <label className="shrink-0 flex items-center cursor-pointer">
1357                          <input
1358                            type="checkbox"
1359                            checked={isChecked}
1360                            onChange={() => toggleAgent(cat.key, agent.id)}
1361                            disabled={!agent.affiliateUrl}
1362                            className="w-4 h-4 rounded border-gray-300 text-brand-orange focus:ring-brand-orange/50 cursor-pointer"
1363                          />
1364                        </label>
1365                        <span className={`shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-[11px] font-bold ${
1366                          idx === 0 ? 'bg-brand-gold text-white' : 'bg-gray-100 text-gray-500'
1367                        }`}>{idx + 1}</span>
1368                        <div className="flex-1 min-w-0" onClick={() => agent.affiliateUrl && toggleAgent(cat.key, agent.id)} style={{cursor:'pointer'}}>
1369                          <div className="flex items-center gap-2">
1370                            <span className="text-[13px] font-semibold text-gray-800">{agent.name}</span>
1371                            {agent.badge && <span className="text-[9px] bg-brand-gold/10 text-brand-gold-dark px-1.5 py-0.5 rounded-full whitespace-nowrap">{agent.badge}</span>}
1372                          </div>
1373                          <div className="text-[11px] text-gray-400 mt-0.5">{agent.monthlyRate || agent.tagline}</div>
1374                        </div>
1375                        {isClicked && <span className="shrink-0 text-[10px] text-green-500 font-bold">済</span>}
1376                      </div>
1377                    );
1378                  })}
1379                </div>
1380                {/* カテゴリ別 一括ボタン */}
1381                <div className="px-4 py-3 bg-gray-50 border-t border-gray-100 flex items-center justify-between gap-2">
1382                  <div className="flex gap-2 items-center">
1383                    <button onClick={() => selectAllCat(cat.key, cat.agents)} className="text-[11px] text-brand-gold hover:underline">全選択</button>
1384                    <span className="text-[11px] text-gray-300">|</span>
1385                    <button onClick={() => deselectAllCat(cat.key, cat.agents)} className="text-[11px] text-gray-400 hover:underline">全解除</button>
1386                  </div>
1387                  <button
1388                    onClick={() => bulkOpenCat(cat.key, cat.agents)}
1389                    disabled={checkedCount === 0}
1390                    className={`px-4 py-2 rounded-xl text-[12px] font-bold transition-all ${
1391                      checkedCount > 0
1392                        ? 'bg-brand-orange text-white hover:bg-brand-orange-dark shadow-sm hover:shadow'
1393                        : 'bg-gray-200 text-gray-400 cursor-not-allowed'
1394                    }`}
1395                  >
1396                    {checkedCount > 0 ? `${checkedCount}社まとめて無料登録` : '選択してください'}
1397                  </button>
1398                </div>
1399              </div>
1400            );
1401          })}
1402
1403          <p className="text-[10px] text-gray-400 text-center pt-2">
1404            複数社に登録すると、案件の選択肢が3倍以上に広がります
1405          </p>
1406        </div>
1407      );
1408    }
1409
1410    // ============================================================
1411    // COMPONENT: 診断完了 → 結果画面(5社一覧表示)
1412    // ============================================================
1413    function ResultView({ rankedAgents, statuses, updateStatus, onBulkOpen, onRestart }) {
1414
1415      const handleCtaClick = (agent) => {
1416        if (agent.affiliateUrl) {
1417          window.open(agent.affiliateUrl, '_blank');
1418          updateStatus(agent.id, 'clicked');
1419        }
1420      };
1421
1422      return (
1423        <div className="space-y-5">
1424          <div className="text-center animate-fade-in">
1425            <h2 className="text-xl font-bold text-gray-800 mb-2">
1426              🎯 あなたにマッチするエージェントTOP5
1427            </h2>
1428            <p className="text-sm text-gray-500">
1429              複数社に登録すると、案件の選択肢が3倍以上に広がります💡
1430            </p>
1431          </div>
1432
1433          {/* TOP5エージェント一覧表示 */}
1434          <div className="space-y-4">
1435            {rankedAgents.slice(0, 5).map((r, i) => (
1436              <AgentResultCard
1437                key={r.agent.id}
1438                agent={r.agent}
1439                matchScore={r.score}
1440                order={i + 1}
1441                status={statuses[r.agent.id]?.status || 'pending'}
1442                onCtaClick={handleCtaClick}
1443              />
1444            ))}
1445          </div>
1446
1447          {/* もう一度診断するボタン */}
1448          <div className="text-center pt-2">
1449            <button
1450              onClick={onRestart}
1451              className="text-sm text-brand-gold hover:text-brand-gold-dark hover:underline transition-colors"
1452            >
1453              🔄 もう一度診断する
1454            </button>
1455          </div>
1456        </div>
1457      );
1458    }
1459
1460    // ============================================================
1461    // COMPONENT: 診断フロー(Module A)
1462    // ============================================================
1463    function DiagnosticFlow({ agents, mode, onComplete }) {
1464      const [currentQ, setCurrentQ] = useState(0);
1465      const [answers, setAnswers] = useState({});
1466      const [isComplete, setIsComplete] = useState(false);
1467
1468      // modeに応じて質問リストを生成(Q2の回答に応じてQ3を動的に変更)
1469      const questions = useMemo(() => {
1470        const base = getQuestions(mode);
1471        if (mode !== 'tenshoku') return base;
1472        // 転職モード: Q2の選択に基づきQ3の選択肢を動的生成
1473        const q2vals = answers.q2 || [];
1474        if (q2vals.length === 0) return base;
1475        // Q2で選ばれた各カテゴリのQ3オプションを統合(重複除去)
1476        const seen = new Set();
1477        const mergedOptions = [];
1478        q2vals.forEach(q2val => {
1479          const opts = Q3_OPTIONS_BY_Q2[q2val] || [];
1480          opts.forEach(opt => {
1481            if (!seen.has(opt.value)) {
1482              seen.add(opt.value);
1483              mergedOptions.push(opt);
1484            }
1485          });
1486        });
1487        // anyを末尾に移動
1488        const anyIdx = mergedOptions.findIndex(o => o.value === 'any');
1489        if (anyIdx >= 0) {
1490          const [anyOpt] = mergedOptions.splice(anyIdx, 1);
1491          mergedOptions.push(anyOpt);
1492        } else {
1493          mergedOptions.push({ value: 'any', label: 'こだわらない' });
1494        }
1495        // 新構造を強制(古い保存データが type=multi 等を持っていても上書き)
1496        const dynamicQ3 = {
1497          id: 'q3',
1498          title: Q3_TENSHOKU.title || '希望の企業タイプは?',
1499          subtitle: Q3_TENSHOKU.subtitle || '1つお選びください',
1500          type: 'single',
1501          options: mergedOptions,
1502        };
1503        return [base[0], base[1], dynamicQ3, base[3]];
1504      }, [mode, answers.q2]);
1505
1506      const handleAnswer = (questionId, value) => {
1507        const newAnswers = { ...answers, [questionId]: value };
1508        setAnswers(newAnswers);
1509
1510        if (currentQ < questions.length - 1) {
1511          setTimeout(() => setCurrentQ(prev => prev + 1), 100);
1512        } else {
1513          finishDiagnostic(newAnswers);
1514        }
1515      };
1516
1517      const handleBack = () => {
1518        if (currentQ > 0) {
1519          setCurrentQ(prev => prev - 1);
1520        }
1521      };
1522
1523      const handleSkip = (questionId) => {
1524        if (currentQ < questions.length - 1) {
1525          setCurrentQ(prev => prev + 1);
1526        } else {
1527          finishDiagnostic(answers);
1528        }
1529      };
1530
1531      const finishDiagnostic = (finalAnswers) => {
1532        // 共通関数 computeRankedAgents に委譲(SwipeDiagnosticと共有)
1533        const results = computeRankedAgents(finalAnswers, mode, agents);
1534        setIsComplete(true);
1535        onComplete(results);
1536      };
1537
1538      if (isComplete) return null;
1539
1540      return (
1541        <div className="max-w-lg mx-auto">
1542          <div className="text-right mb-0.5"><span className="text-[10px] text-gray-400">PR</span></div>
1543          {/* 戻るボタン + 進捗バー */}
1544          <div className="flex items-center gap-3 mb-4">
1545            {currentQ > 0 ? (
1546              <button
1547                onClick={handleBack}
1548                className="text-xs text-gray-400 hover:text-gray-600 transition-colors flex items-center gap-1 shrink-0"
1549              >
1550                ← 戻る
1551              </button>
1552            ) : <div className="w-10" />}
1553            <div className="flex-1 bg-gray-200 rounded-full h-1.5">
1554              <div
1555                className="bg-brand-gold h-1.5 rounded-full transition-all duration-500"
1556                style={{ width: `${((currentQ + 1) / questions.length) * 100}%` }}
1557              />
1558            </div>
1559          </div>
1560
1561          <QuestionCard
1562            key={questions[currentQ].id + '-' + currentQ}
1563            question={questions[currentQ]}
1564            questionIndex={currentQ}
1565            totalQuestions={questions.length}
1566            answer={answers[questions[currentQ].id]}
1567            onAnswer={handleAnswer}
1568            onSkip={handleSkip}
1569          />
1570        </div>
1571      );
1572    }
1573
1574    // ============================================================
1575    // SHARED: 診断結果計算(DiagnosticFlow/SwipeDiagnostic 共通)
1576    // ============================================================
1577    function computeRankedAgents(finalAnswers, mode, fallbackAgents) {
1578      const q2vals = finalAnswers.q2 || [];
1579      const configAgentsMap = new Map();
1580      const configPriorityMap = new Map();
1581
1582      q2vals.forEach(q2val => {
1583        const cfg = ARTICLE_CONFIGS[q2val];
1584        if (cfg && cfg.agents) {
1585          const resolved = resolveAgents(cfg);
1586          resolved.forEach(a => {
1587            if (!configAgentsMap.has(a.id)) configAgentsMap.set(a.id, a);
1588          });
1589          cfg.agents.forEach(ca => {
1590            if (!configPriorityMap.has(ca.id)) configPriorityMap.set(ca.id, ca.priority);
1591          });
1592        }
1593      });
1594
1595      if (configAgentsMap.size === 0 && fallbackAgents) {
1596        fallbackAgents.forEach(a => configAgentsMap.set(a.id, a));
1597      }
1598
1599      let allAgents = [...configAgentsMap.values()];
1600      const results = [];
1601      const seen = new Set();
1602
1603      const q3val = typeof finalAnswers.q3 === 'string' ? finalAnswers.q3 : (Array.isArray(finalAnswers.q3) ? finalAnswers.q3[0] : null);
1604      // Q3ブーストを「複数Q2」対応にする:q2valごとに複合キーで合成してブースト位置を保持
1605      // { agent_id: boost_rank (0=1位, 1=2位, ...) }
1606      const q3BoostRank = {};
1607      if (q3val && q3val !== 'any') {
1608        q2vals.forEach(q2val => {
1609          const compositeKey = `${q2val}:${q3val}`;
1610          const list = Q3_AGENT_BOOSTS[compositeKey] || [];
1611          list.forEach((aid, idx) => {
1612            // 同じエージェントが複数Q2でブーストされたら最小のrank(上位)を採用
1613            if (q3BoostRank[aid] === undefined || q3BoostRank[aid] > idx) {
1614              q3BoostRank[aid] = idx;
1615            }
1616          });
1617        });
1618        // フォールバック: 複合キーが1つも無ければq3val単独キー
1619        if (Object.keys(q3BoostRank).length === 0) {
1620          const list = Q3_AGENT_BOOSTS[q3val] || [];
1621          list.forEach((aid, idx) => { q3BoostRank[aid] = idx; });
1622        }
1623      }
1624
1625      // ★ハードフィルタはやめる(1社しか出ない問題の原因)
1626      // 代わりにスコアボーナスで上位に押し上げる:1位=+20, 2位=+15, 3位=+10, 4位=+7, 5位以降=+5
1627      const boostBonusByRank = [20, 15, 10, 7, 5, 4, 3, 2, 1];
1628
1629      const rankBonus = { S: 5, A: 0, B: -5 };
1630      const scored = allAgents.map(agent => {
1631        const a = agent;
1632        const priority = configPriorityMap.get(a.id) ?? 999;
1633        const score = calculateScore(a, finalAnswers);
1634        const rb = rankBonus[a.rank] || 0;
1635        // Q3ブースト: boost listに入ってれば順位に応じてボーナス加算
1636        let q3Bonus = 0;
1637        if (q3BoostRank[a.id] !== undefined) {
1638          const rank = q3BoostRank[a.id];
1639          q3Bonus = boostBonusByRank[rank] !== undefined ? boostBonusByRank[rank] : 1;
1640        }
1641        const finalScore = (1 - priority / 20) * 40 + rb + score * 0.05 + q3Bonus;
1642        const recScore = Math.min(100, Math.max(0, Math.round(finalScore / 68 * 100)));
1643        return { agent: a, score: recScore, priority, finalScore };
1644      });
1645
1646      scored.sort((a, b) => b.finalScore - a.finalScore);
1647
1648      for (const item of scored) {
1649        if (seen.has(item.agent.id)) continue;
1650        seen.add(item.agent.id);
1651        results.push({ agent: item.agent, score: item.score });
1652      }
1653      return results;
1654    }
1655
1656    // ============================================================
1657    // COMPONENT: 回答モード切替トグル(簡単 / しっかり)
1658    // ============================================================
1659    // COMPONENT: Tinder風スワイプ診断(簡単回答モード)
1660    // ============================================================
1661    const SWIPE_CARD_META = {
1662      // === Freelance Q2 ===
1663      strategy: { emoji: '🎯', short: '戦略コンサル', desc: '新規事業・M&A・戦略立案' },
1664      biz_consul: { emoji: '🏢', short: '業務コンサル', desc: '業務改善・BPR' },
1665      it_consul: { emoji: '💻', short: 'ITコンサル', desc: 'システム導入・PMO' },
1666      engineering: { emoji: '⚙️', short: 'エンジニアリング', desc: '開発・インフラ' },
1667      sap: { emoji: '📊', short: 'SAP案件', desc: 'FICO・SD・MM・ABAP' },
1668      security: { emoji: '🔒', short: 'セキュリティ', desc: 'SOC・脆弱性診断' },
1669      // === Freelance Q1 (今の状況) ===
1670      searching: { emoji: '🔍', short: '案件探し中', desc: 'フリーランスで探してる' },
1671      considering: { emoji: '🤔', short: '独立検討中', desc: 'まだ会社員で検討段階' },
1672      ending_soon: { emoji: '⏰', short: '案件終了予定', desc: '次の案件を探したい' },
1673      want_better: { emoji: '💰', short: '単価アップ狙い', desc: 'エージェントを変えたい' },
1674      // === Freelance Q3 / Tenshoku Q4 (重視するもの・同一value) ===
1675      high_rate: { emoji: '💰', short: '高単価・年収', desc: 'とにかく稼ぎたい' },
1676      stability: { emoji: '🛡️', short: '安定感', desc: '途切れない・福利厚生' },
1677      speed: { emoji: '⚡', short: 'スピード', desc: 'すぐ参画・すぐ転職' },
1678      career_up: { emoji: '📈', short: 'キャリアアップ', desc: 'スキル・成長' },
1679      // === Tenshoku Q1 (経験レベル) ===
1680      tenshoku_20s: { emoji: '🌱', short: '20代・第二新卒', desc: '経験3年未満' },
1681      young_mid: { emoji: '🌿', short: '若手〜中堅', desc: '経験3〜10年' },
1682      senior: { emoji: '🌳', short: 'ベテラン・管理職', desc: '経験10年以上' },
1683      career_change: { emoji: '🔄', short: 'キャリアチェンジ', desc: '異業種からの転職' },
1684      // === Tenshoku Q2 ===
1685      tenshoku_strategy_consul: { emoji: '🎯', short: '戦略・経営コンサル', desc: 'MBB・Big4・ブティック' },
1686      tenshoku_it_consul: { emoji: '💻', short: 'IT/DXコンサル', desc: 'Accenture・Big4 DX' },
1687      tenshoku_ma: { emoji: '💼', short: 'M&A・FAS', desc: 'IBD・FAS・PEファンド' },
1688      tenshoku_engineer: { emoji: '⚙️', short: 'エンジニア', desc: 'メガベンチャー・外資IT' },
1689      tenshoku_sales: { emoji: '🤝', short: '営業・セールス', desc: '外資・日系・ベンチャー' },
1690      tenshoku_marketing: { emoji: '📣', short: 'マーケティング', desc: 'ブランド・デジマ' },
1691      tenshoku_accounting: { emoji: '📑', short: '会計・税務・経理', desc: 'Big4・FAS・事業会社' },
1692      tenshoku_other: { emoji: '✨', short: 'その他・幅広く', desc: 'これから相談したい' },
1693      // === Tenshoku Q3 dynamic (企業タイプ) ===
1694      mbb: { emoji: '🏆', short: 'MBB・戦略ファーム', desc: '最上位の戦略ファーム' },
1695      big4: { emoji: '🏛️', short: 'Big4・総合ファーム', desc: '幅広い総合ファーム' },
1696      boutique: { emoji: '💎', short: 'ブティック系', desc: '専門特化ファーム' },
1697      enterprise_strategy: { emoji: '🏬', short: '事業会社(戦略)', desc: '戦略ポジション' },
1698      accenture_big4_dx: { emoji: '⚡', short: 'Accenture・Big4 DX', desc: 'DX大規模プロジェクト' },
1699      foreign_it: { emoji: '🌍', short: '外資IT系コンサル', desc: 'グローバル展開' },
1700      japan_sier: { emoji: '🏯', short: '日系SIer上流', desc: '大規模システム' },
1701      enterprise_dx: { emoji: '🏢', short: '事業会社(DX)', desc: 'DX推進ポジション' },
1702      foreign_ibd: { emoji: '💵', short: '外資IBD', desc: '投資銀行' },
1703      japan_ibd: { emoji: '🏦', short: '日系証券IBD', desc: '国
1703内投資銀行' },
1704      fas: { emoji: '📊', short: 'FAS', desc: 'Big4アドバイザリー' },
1705      pe_fund: { emoji: '💼', short: 'PEファンド', desc: 'プライベートエクイティ' },
1706      enterprise_ma: { emoji: '🤝', short: '事業会社M&A室', desc: '買い手側M&A' },
1707      mega_venture: { emoji: '🚀', short: 'メガベンチャー', desc: '成長企業' },
1708      startup: { emoji: '🌱', short: 'スタートアップ', desc: 'アーリー/ミドル' },
1709      foreign_it_big: { emoji: '🌐', short: '外資IT(GAFA系)', desc: 'ビッグテック' },
1710      japan_large: { emoji: '🏢', short: '日系大手', desc: '安定・大企業' },
1711      sier: { emoji: '💻', short: 'SIer', desc: 'システム開発' },
1712      foreign: { emoji: '🌍', short: '外資系', desc: 'グローバル企業' },
1713      big4_audit: { emoji: '📑', short: 'Big4監査法人', desc: '監査・アドバイザリー' },
1714      enterprise_finance: { emoji: '💴', short: '事業会社(経理財務)', desc: 'インハウス' },
1715      tax_firm: { emoji: '🧾', short: '税理士法人', desc: '税務専門' },
1716      any: { emoji: '🌈', short: 'こだわらない', desc: '幅広く見たい' },
1717    };
1718
1719    // ============================================================
1720    // 猫の反応セリフ(選択肢をキャッチした時の一言)
1721    // ============================================================
1722    const CAT_REACTIONS = {
1723      // Q1 経験レベル
1724      tenshoku_20s: 'おぉー若い!ポテンシャル枠で攻められるね✨',
1725      young_mid: '一番転職しやすい時期じゃん!チャンス🎯',
1726      senior: '大ベテラン様🫡 年収上げどきだね',
1727      career_change: '勇者だね〜!意外と道あるよ😺',
1728      // Q2 職種(転職)
1729      tenshoku_strategy_consul: '戦略コンサル!華あるねぇ〜',
1730      tenshoku_it_consul: 'DX案件めっちゃ多いよ、狙い目',
1731      tenshoku_ma: 'M&A系は年収の上振れがでかい',
1732      tenshoku_engineer: 'エンジニア需要、まだまだ強いよ',
1733      tenshoku_sales: '営業は結果出せば跳ねる!',
1734      tenshoku_marketing: 'マーケ、最近アツいよね〜',
1735      tenshoku_accounting: '会計・税務は安定の王道👍',
1736      tenshoku_other: '幅広く、いいね〜',
1737      // Q3 企業タイプ
1738      mbb: 'MBB狙いか!気合入ってるねぇ🔥',
1739      big4: 'Big4、安定感あっていいよ',
1740      boutique: 'ブティック、専門性で勝負!',
1741      enterprise_strategy: '事業会社の戦略ポジ、いいね',
1742      accenture_big4_dx: 'DX案件の本丸だね',
1743      foreign_it: '外資IT、グローバル志向👀',
1744      japan_sier: '日系SIer上流、堅実!',
1745      enterprise_dx: '事業会社DX、今アツい',
1746      foreign_ibd: '外資IBD、ガチ勢じゃん💰',
1747      japan_ibd: '日系証券IBD、安定強い',
1748      fas: 'FAS、財務のプロフェッショナル',
1749      pe_fund: 'PEファンド!上位層狙いだね',
1750      enterprise_ma: '事業会社M&A室、通じゃん',
1751      mega_venture: 'メガベン、勢いあるよ〜',
1752      startup: 'スタートアップ!冒険だね🚀',
1753      foreign_it_big: 'GAFA系!夢あるね〜',
1754      japan_large: '日系大手、王道の安定',
1755      sier: 'SIer、手堅い選択',
1756      foreign: '外資系、グローバルに羽ばたく',
1757      big4_audit: 'Big4監査、ゴールドスタンダード',
1758      enterprise_finance: '事業会社経理、腰を据えて',
1759      tax_firm: '税理士法人、専門職として',
1760      any: 'じゃあ幅広く見てあげるね',
1761      // Q4 重視するもの
1762      high_rate: 'そうそう、お金大事!💰',
1763      stability: '安定、大事にしたいよね🛡',
1764      speed: 'スピード勝負ね、了解🏃',
1765      career_up: '成長志向、いいねぇ📈',
1766    };
1767    const CAT_REACTIONS_FALLBACK = [
1768      '了解!',
1769      'なるほどね〜',
1770      'オッケー、把握した',
1771      'いいじゃん',
1772      'ふむふむ',
1773    ];
1774
1775    function SwipeDiagnostic({ mode, agents, onComplete }) {
1776      const isTenshoku = mode === 'tenshoku';
1777
1778      // 質問シーケンス(フリーランス: Q1→Q2→Q3、転職: Q1→Q2→Q3動的→Q4)
1779      const questionKeys = useMemo(() => {
1780        return isTenshoku ? ['q1', 'q2', 'q3_dyn', 'q4'] : ['q1', 'q2', 'q3'];
1781      }, [isTenshoku]);
1782
1783      const [qIdx, setQIdx] = useState(0);
1784      const [answers, setAnswers] = useState({});
1785      const [liked, setLiked] = useState([]);
1786      const [catching, setCatching] = useState(null); // value of fish currently being caught
1787      const [catReaction, setCatReaction] = useState(null); // cat's speech bubble text
1788      const [tick, setTick] = useState(0); // drives fish motion
1789      const [finished, setFinished] = useState(false);
1790      const [showTutorial, setShowTutorial] = useState(true);
1791      const dismissTutorial = useCallback(() => setShowTutorial(false), []);
1792
1793      // 魚を動かすためのtick (50ms = 20fps, 十分滑らか)
1794      useEffect(() => {
1795        if (finished) return;
1796        const id = setInterval(() => setTick(t => t + 1), 50);
1797        return () => clearInterval(id);
1798      }, [finished]);
1799
1800      // 現在の質問を解決(Q3転職は動的)
1801      const currentQuestion = useMemo(() => {
1802        const key = questionKeys[qIdx];
1803        if (!key) return null;
1804        if (key === 'q1') return isTenshoku ? Q1_TENSHOKU : Q1_FREELANCE;
1805        if (key === 'q2') return isTenshoku ? Q2_TENSHOKU : Q2_FREELANCE;
1806        if (key === 'q3') return Q3_FREELANCE;
1807        if (key === 'q4') return Q4_TENSHOKU;
1808        if (key === 'q3_dyn') {
1809          const q2vals = answers.q2 || [];
1810          const seen = new Set();
1811          const opts = [];
1812          q2vals.forEach(v => {
1813            (Q3_OPTIONS_BY_Q2[v] || []).forEach(o => {
1814              if (!seen.has(o.value)) {
1815                seen.add(o.value);
1816                opts.push(o);
1817              }
1818            });
1819          });
1820          if (opts.length === 0) opts.push({ value: 'any', label: 'こだわらない' });
1821          return {
1822            id: 'q3',
1823            title: Q3_TENSHOKU.title || '希望の企業タイプは?',
1824            type: 'single',
1825            options: opts,
1826          };
1827        }
1828        return null;
1829      }, [qIdx, questionKeys, isTenshoku, answers]);
1830
1831      const currentCards = useMemo(() => {
1832        if (!currentQuestion) return [];
1833        return currentQuestion.options.map(o => ({
1834          value: o.value,
1835          label: o.label,
1836          ...(SWIPE_CARD_META[o.value] || { emoji: '💡', short: o.label, desc: '' }),
1837        }));
1838      }, [currentQuestion]);
1839
1840      const isMulti = currentQuestion?.type === 'multi';
1841      const maxLikes = isMulti ? (currentQuestion?.maxSelect || 2) : 1;
1842
1843      // 次の質問へ or 診断完了
1844      const advanceQuestion = useCallback((newAnswersObj) => {
1845        const nextQIdx = qIdx + 1;
1846        if (nextQIdx >= questionKeys.length) {
1847          setFinished(true);
1848          const finalAns = { q1: null, q2: [], q3: null, q4: null, ...newAnswersObj };
1849          const results = computeRankedAgents(finalAns, mode, agents);
1850          setTimeout(() => onComplete(results), 520);
1851          return;
1852        }
1853        setAnswers(newAnswersObj);
1854        setQIdx(nextQIdx);
1855        setLiked([]);
1856        setCatching(null);
1857        setCatReaction(null);
1858      }, [qIdx, questionKeys, mode, agents, onComplete]);
1859
1860      // 魚をタップで捕まえる(飛び上がって猫の口へ)
1861      const catchFish = useCallback((cardValue) => {
1862        if (catching) return;
1863        if (liked.includes(cardValue)) return;
1864        if (!currentQuestion) return;
1865        if (showTutorial) setShowTutorial(false);
1866        setCatching(cardValue);
1867        // 猫の一言リアクション
1868        const reaction = CAT_REACTIONS[cardValue] || CAT_REACTIONS_FALLBACK[Math.floor(Math.random() * CAT_REACTIONS_FALLBACK.length)];
1869        setTimeout(() => setCatReaction(reaction), 420);
1870        setTimeout(() => {
1871          const qid = currentQuestion.id;
1872          if (isMulti) {
1873            const nextLiked = [...liked, cardValue];
1874            if (nextLiked.length >= maxLikes) {
1875              const newAnswers = { ...answers, [qid]: nextLiked };
1876              setTimeout(() => {
1877                setCatReaction(null);
1878                advanceQuestion(newAnswers);
1879              }, 900);
1880            } else {
1881              setLiked(nextLiked);
1882              setCatching(null);
1883              setTimeout(() => setCatReaction(null), 900);
1884            }
1885          } else {
1886            const newAnswers = { ...answers, [qid]: cardValue };
1887            setTimeout(() => {
1888              setCatReaction(null);
1889              advanceQuestion(newAnswers);
1890            }, 900);
1891          }
1892        }, 620);
1893      }, [catching, liked, currentQuestion, showTutorial, isMulti, maxLikes, answers, advanceQuestion]);
1894
1895      // 各魚の「家の位置」と泳ぎパラメータ(決定論的にseed)
1896      // ⚠️ Rules of Hooks: 早期 return より前に必ず呼ぶ
1897      const fishLayout = useMemo(() => {
1898        const n = currentCards.length;
1899        return currentCards.map((c, i) => {
1900          const col = n <= 4 ? 2 : 3;
1901          const row = Math.floor(i / col);
1902          const colIdx = i % col;
1903          const rows = Math.ceil(n / col);
1904          const homeX = 10 + ((colIdx + 0.5) / col) * 80;
1905          const homeY = 32 + ((row + 0.5) / Math.max(rows, 1)) * 50;
1906          return {
1907            homeX,
1908            homeY,
1909            phaseX: (i * 1.7) % (Math.PI * 2),
1910            phaseY: (i * 2.3) % (Math.PI * 2),
1911            speedX: 0.7 + (i % 3) * 0.15,
1912            speedY: 0.9 + (i % 4) * 0.12,
1913            flipPhase: (i * 0.9) % (Math.PI * 2),
1914          };
1915        });
1916      }, [currentCards]);
1917
1918      if (finished) {
1919        return (
1920          <div className="max-w-lg mx-auto">
1921            <div className="text-center py-16">
1922              <div className="text-6xl mb-4 animate-bounce">🎉</div>
1923              <div className="inline-block w-10 h-10 border-4 border-brand-gold border-t-transparent roun
1923ded-full animate-spin mb-4"></div>
1924              <p className="text-sm text-gray-500">診断結果を計算中...</p>
1925            </div>
1926          </div>
1927        );
1928      }
1929
1930      if (!currentQuestion || currentCards.length === 0) {
1931        return (
1932          <div className="max-w-lg mx-auto text-center py-16">
1933            <p className="text-sm text-gray-500">読み込み中...</p>
1934          </div>
1935        );
1936      }
1937
1938      // 進捗パーセンテージ
1939      const innerProgress = isMulti ? (liked.length / maxLikes) : 0;
1940      const progressPct = ((qIdx + innerProgress) / questionKeys.length) * 100;
1941
1942      // チュートリアルは最初の質問でのみ表示
1943      const showTutorialNow = showTutorial && qIdx === 0;
1944
1945      // 池の寸法(% ベースの座標系)
1946      // 上部 ~28% は猫のいる「岸」、中央 ~72% は水面
1947      const PondHeight = 440; // px
1948      const WaterTop = 0.28;  // pond 28% 以下が水
1949
1950      const time = tick * 0.05; // 秒
1951
1952      return (
1953        <div className="max-w-lg mx-auto">
1954          {/* ========== 進捗ヘッダー ========== */}
1955          <div className="mb-3 px-3">
1956            <div className="flex items-center gap-2 mb-3">
1957              <span className="text-[11px] font-black text-white bg-gradient-to-r from-amber-500 to-orange-500 rounded-full px-3 py-1 shadow-sm whitespace-nowrap">
1958                Q{qIdx + 1} / {questionKeys.length}
1959              </span>
1960              <div className="flex-1 h-2.5 bg-gray-100 rounded-full overflow-hidden shadow-inner">
1961                <div
1962                  className="h-full bg-gradient-to-r from-amber-400 via-orange-400 to-orange-500 rounded-full transition-all duration-500 shadow-sm"
1963                  style={{ width: `${Math.min(100, progressPct)}%` }}
1964                ></div>
1965              </div>
1966              <span className="text-[10px] font-bold text-gray-500 whitespace-nowrap">
1967                {Math.round(progressPct)}%
1968              </span>
1969            </div>
1970          </div>
1971
1972          {/* ========== 質問タイトル ========== */}
1973          <div key={`q-${qIdx}`} className="animate-fade-in-up">
1974            <div className="mb-3 px-3 text-center">
1975              <h3 className="text-2xl font-black text-gray-900 leading-tight">
1976                {currentQuestion.title}
1977              </h3>
1978              <p className="text-[11px] text-sky-600/80 font-bold mt-1">
1979                {isMulti
1980                  ? `🎣 気になる魚を${maxLikes}匹タップして猫にあげて`
1981                  : `🎣 気になる魚をタップして猫にあげよう`}
1982              </p>
1983              {isMulti && (
1984                <div className="flex items-center justify-center gap-1 mt-1 text-orange-500 font-black text-[11px]">
1985                  {Array.from({ length: maxLikes }).map((_, i) => (
1986                    <span key={i} className={i < liked.length ? 'opacity-100' : 'opacity-25'}>🐟</span>
1987                  ))}
1988                  <span className="ml-1">{liked.length}/{maxLikes}</span>
1989                </div>
1990              )}
1991            </div>
1992
1993            {/* ========== 池(水族館) ========== */}
1994            <div
1995              className="relative rounded-3xl border-2 border-sky-300 shadow-inner overflow-hidden select-none"
1996              style={{ height: `${PondHeight}px` }}
1997            >
1998              {/* 空(岸の上) */}
1999              <div className="absolute inset-x-0 top-0 bg-gradient-to-b from-amber-50 via-orange-50 to-amber-100" style={{ height: `${WaterTop * 100}%` }}></div>
2000              {/* 水面 */}
2001              <div className="absolute inset-x-0 bottom-0 bg-gradient-to-b from-sky-200 via-sky-300 to-blue-400" style={{ top: `${WaterTop * 100}%` }}>
2002                {/* 水面のキラキラ */}
2003                <div className="absolute inset-x-0 top-0 h-3 bg-gradient-to-b from-white/60 to-transparent"></div>
2004                {/* 水中の光の揺らぎ */}
2005                <div
2006                  className="absolute inset-x-0 top-0 h-6 opacity-30"
2007                  style={{
2008                    background: 'repeating-linear-gradient(90deg, transparent 0, transparent 15px, rgba(255,255,255,0.5) 15px, rgba(255,255,255,0.5) 18px)',
2009                    transform: `translateX(${Math.sin(time * 0.8) * 8}px)`,
2010                  }}
2011                ></div>
2012              </div>
2013
2014              {/* 猫(岸の上で待機) */}
2015              <div className="absolute left-1/2 -translate-x-1/2 z-10 flex flex-col items-center pointer-events-none" style={{ top: '4px' }}>
2016                <div
2017                  className="text-[72px] leading-none drop-shadow-lg transition-transform duration-150"
2018                  style={{
2019                    transform: catching
2020                      ? 'scale(1.3) translateY(-6px)'
2021                      : `scale(1) translateY(${Math.sin(time * 1.4) * 2}px)`,
2022                  }}
2023                >
2024                  {catching ? '😋' : (liked.length > 0 ? '😺' : '🐱')}
2025                </div>
2026                {/* 猫の吹き出し(リアクション) */}
2027                {catReaction && (
2028                  <div className="absolute top-full left-1/2 -translate-x-1/2 mt-1 bg-white border-2 border-amber-300 rounded-2xl px-3 py-1.5 shadow-lg whitespace-nowrap animate-pop-in">
2029                    <div className="text-[11px] font-black text-gray-800">{catReaction}</div>
2030                    <div className="absolute -top-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-white border-t-2 border-l-2 border-amber-300 rotate-45"></div>
2031                  </div>
2032                )}
2033              </div>
2034
2035              {/* 魚たち(水中で泳ぐ) */}
2036              {currentCards.map((c, i) => {
2037                const caught = liked.includes(c.value);
2038                const isCatching = catching === c.value;
2039                if (caught && !isCatching) return null; // 食べられて消えた
2040
2041                const p = fishLayout[i];
2042                // 泳ぎのふらふら(% ベース)
2043                const swimX = Math.sin(time * p.speedX + p.phaseX) * 10;
2044                const swimY = Math.cos(time * p.speedY + p.phaseY) * 5;
2045
2046                const posX = p.homeX + swimX;
2047                const posY = p.homeY + swimY;
2048
2049                // キャッチ中は猫の口(top center)に向かって一直線に飛ぶ
2050                const flyStyle = isCatching ? {
2051                  left: '50%',
2052                  top: '8%',
2053                  transform: 'translate(-50%, -50%) scale(0.2) rotate(20deg)',
2054                  opacity: 0,
2055                  transition: 'left 0.6s cubic-bezier(0.5, 0, 0.75, 0.5), top 0.6s cubic-bezier(0.5, 0, 0.75, 0.5), transform 0.6s ease-in, opacity 0.6s ease-in 0.2s',
2056                } : {
2057                  left: `${posX}%`,
2058                  top: `${posY}%`,
2059                  transform: `translate(-50%, -50%) rotate(${Math.sin(time * 0.7 + p.flipPhase) * 4}deg)`,
2060                  transition: 'left 0.1s linear, top 0.1s linear',
2061                };
2062
2063                return (
2064                  <button
2065                    key={c.value}
2066                    type="button"
2067                    onClick={() => catchFish(c.value)}
2068                    disabled={!!catching}
2069                    className="absolute flex flex-col items-center group focus:outline-none"
2070                    style={{
2071                      ...flyStyle,
2072                      cursor: catching ? 'default' : 'grab',
2073                    }}
2074                  >
2075                    {/* 魚の本体(カード風) */}
2076                    <div className="relative bg-gradient-to-br from-cyan-100 via-white to-sky-100 rounded-[22px] border-[3px] border-sky-400 px-2.5 py-1.5 shadow-lg group-hover:shadow-2xl group-hover:scale-110 group-active:scale-95 transition-all">
2077                      {/* キラキラマーカー */}
2078                      <div className="absolute -top-1 -right-1 w-3 h-3 bg-yellow-300 rounded-full shadow animate-pulse"></div>
2079                      <div className="flex items-center gap-1.5">
2080                        <span className="text-2xl leading-none">{c.emoji || '🐟'}</span>
2081                        <div className="text-[10px] font-black text-gray-800 leading-tight whitespace-nowrap">
2082                          {c.short || c.label}
2083                        </div>
2084                      </div>
2085                      {/* 尾びれ */}
2086                      <div className="absolute top-1/2 -left-1.5 -translate-y-1/2 w-0 h-0 border-t-[6px] border-b-[6px] border-r-[8px] border-t-transparent border-b-transparent border-r-sky-400"></div>
2087                    </div>
2088                  </button>
2089                );
2090              })}
2091
2092              {/* 気泡エフェクト(装飾) */}
2093              {!catching && Array.from({ length: 6 }).map((_, i) => {
2094                const bubblePhase = (time * 0.4 + i * 1.1) % 3;
2095                const left = 10 + (i * 17) % 80;
2096                const bottom = (bubblePhase / 3) * 70; // 0〜70%
2097                const size = 3 + (i % 3) * 2;
2098                const opacity = Math.max(0, 1 - bubblePhase / 3) * 0.6;
2099                return (
2100                  <div
2101                    key={i}
2102                    className="absolute rounded-full bg-white pointer-events-none"
2103                    style={{
2104                      left: `${left}%`,
2105                      bottom: `${bottom}%`,
2106                      width: `${size}px`,
2107                      height: `${size}px`,
2108                      opacity,
2109                    }}
2110                  ></div>
2111                );
2112              })}
2113
2114              {/* チュートリアル: 最初の質問のみ */}
2115              {showTutorialNow && (
2116                <div className="absolute inset-0 bg-black/50 backdrop-blur-[2px] flex items-center justify-center z-20 rounded-3xl animate-fade-in" onClick={dismissTutorial}>
2117                  <div className="bg-white rounded-3xl p-5 max-w-[280px] mx-4 text-center shadow-2xl animate-pop-in">
2118                    <div className="text-5xl mb-2">🎣🐱</div>
2119                    <h4 className="text-sm font-black text-gray-900 mb-2">猫に魚をあげよう!</h4>
2120                    <p className="text-[11px] text-gray-600 mb-3 leading-relaxed">
2121                      池を泳いでる魚が<br/>
2122                      <b className="text-orange-500">選択肢</b>だよ 🐟<br/>
2123                      気になる魚を<b className="text-sky-600">タップ</b>すると<br/>
2124                      猫がキャッチして食べるよ🐾
2125                    </p>
2126                    <button
2127                      onClick={(e) => { e.stopPropagation(); dismissTutorial(); }}
2128                      className="w-full py-2.5 bg-gradient-to-r from-sky-500 to-blue-500 text-white text-xs font-black rounded-full shadow-md hover:shadow-lg transition-all"
2129                    >
2130                      わかった! 釣り開始 🎣
2131                    </button>
2132                  </div>
2133                </div>
2134              )}
2135            </div>
2136          </div>
2137        </div>
2138      );
2139    }
2140
2141    // ============================================================
2142    // COMPONENT: コンサル適性診断(tekiseiモード・自己完結)
2143    // ============================================================
2144    const TEKISEI_QUESTIONS = [
2145      { key: 'age', title: 'あなたの年齢は?', sub: '想定ポジション・年収レンジの算出に使います', options: [
2146        { value: 'a1', label: '24歳以下' },
2147        { value: 'a2', label: '25〜29歳' },
2148        { value: 'a3', label: '30〜34歳' },
2149        { value: 'a4', label: '35〜39歳' },
2150        { value: 'a5', label: '40歳以上' },
2151      ]},
2152      { key: 'job', title: '現在の職種に一番近いのは?', sub: '', options: [
2153        { value: 'kikaku', label: '企画・マーケティング' },
2154        { value: 'sales', label: '営業・セールス' },
2155        { value: 'engineer', label: 'エンジニア・IT' },
2156        { value: 'keiri', label: '経理・財務・会計' },
2157        { value: 'consul', label: 'コンサル業界(現職)' },
2158        { value: 'other', label: '公務員・その他' },
2159      ]},
2160      { key: 'salary', title: '現在の年収は?', sub: '想定アップ幅の算出に使います(匿名・保存さã
2160‚Œã¾ã›ã‚“)', options: [
2161        { value: 's1', label: '〜400万円' },
2162        { value: 's2', label: '400〜600万円' },
2163        { value: 's3', label: '600〜800万円' },
2164        { value: 's4', label: '800〜1,000万円' },
2165        { value: 's5', label: '1,000万円〜' },
2166      ]},
2167      { key: 'solve', title: '複雑な問題を、分解して構造的に整理するのは', sub: 'ファームが最も重視する要素です', options: [
2168        { value: 'high', label: '得意(仕事でよくやっている)' },
2169        { value: 'mid', label: 'ある程度できる' },
2170        { value: 'low', label: '苦手なほう' },
2171      ]},
2172      { key: 'domain', title: '「この業界・領域なら詳しい」と言える専門分野がある', sub: '例: 金融・製造・医療・IT・人事・会計など', options: [
2173        { value: 'high', label: 'ある(実務経験3年以上)' },
2174        { value: 'mid', label: '少しある' },
2175        { value: 'low', label: '特にない' },
2176      ]},
2177      { key: 'pm', title: '複数人を巻き込んだプロジェクトを推進した経験は', sub: '社内プロジェクト・委員会・イベント運営ã
2177ªã©ã‚‚含みます', options: [
2178        { value: 'high', label: 'リーダーとして経験あり' },
2179        { value: 'mid', label: 'メンバーとして経験あり' },
2180        { value: 'low', label: 'ほとんどない' },
2181      ]},
2182      { key: 'logic', title: '話すとき、結論から入るほうだ', sub: '直感で選んでください', options: [
2183        { value: 'high', label: 'かなり当てはまる' },
2184        { value: 'mid', label: 'どちらかといえば当てはまる' },
2185        { value: 'low', label: 'あまり当てはまらない' },
2186      ]},
2187      { key: 'stamina', title: '納期前の集中的なハードワークは苦にならない', sub: '', options: [
2188        { value: 'high', label: 'かなり当てはまる' },
2189        { value: 'mid', label: 'どちらかといえば当てはまる' },
2190        { value: 'low', label: 'あまり当てはまらない' },
2191      ]},
2192      { key: 'client', title: '初対面の役職者と議論するのは', sub: '', options: [
2193        { value: 'high', label: '得意なほう' },
2194        { value: 'mid', label: '普通' },
2195        { value: 'low', label: '正直、苦手' },
2196      ]},
2197      { key: 'learn', title: '未知の業界を短期間でキャッチアップするのは', sub: '', options: [
2198        { value: 'high', label: '得意なほう' },
2199        { value: 'mid', label: '普通' },
2200        { value: 'low', label: '正直、苦手' },
2201      ]},
2202    ];
2203
2204    // 年齢帯 → 想定ポジション・年収レンジ(万円)。一般的な入社時目安
2205    const TEKISEI_AGE_SALARY = {
2206      a1: { pos: 'アナリスト', low: 550, high: 900 },
2207      a2: { pos: 'アナリスト〜コンサルタント', low: 600, high: 1000 },
2208      a3: { pos: 'コンサルタント〜シニアコンサルタント', low: 700, high: 1200 },
2209      a4: { pos: 'シニアコンサルタント〜マネージャー', low: 850, high: 1400 },
2210      a5: { pos: 'マネージャー以上', low: 1000, high: 1600 },
2211    };
2212    const TEKISEI_CUR_SALARY_MID = { s1: 350, s2: 500, s3: 700, s4: 900, s5: 1150 };
2213
2214    function computeTekisei(answers) {
2215      const traitPt = { high: 25, mid: 15, low: 6 };
2216      // スキル3問(問題解決・業界知見・PM)は採用要件として重み1.4倍
2217      const skills = ['solve', 'domain', 'pm'].reduce((s, k) => s + (traitPt[answers[k]] || 6) * 1.4, 0); // 25.2〜105
2218      const traits = ['logic', 'stamina', 'client', 'learn'].reduce((s, k) => s + (traitPt[answers[k]] || 6), 0); // 24〜100
2219      const ageAdj = { a1: 4, a2: 6, a3: 2, a4: -4, a5: -8 }[answers.age] || 0;
2220      const jobAdj = { consul: 8, kikaku: 6, engineer: 5, keiri: 5, sales: 3, other: 0 }[answers.job] || 0;
2221      let score = Math.round((skills + traits) * 0.39 + ageAdj + jobAdj + 8);
2222      score = Math.max(10, Math.min(97, score));
2223      const competencies = { solve: answers.solve || 'low', domain: answers.domain || 'low', pm: answers.pm || 'low' };
2224      const band = score >= 72 ? 'high' : score >= 48 ? 'mid' : 'low';
2225      const sal = TEKISEI_AGE_SALARY[answers.age] || TEKISEI_AGE_SALARY.a3;
2226      const curMid = TEKISEI_CUR_SALARY_MID[answers.salary] || 500;
2227      const diffLow = sal.low - curMid;
2228      const diffHigh = sal.high - curMid;
2229      return { score, band, sal, curMid, diffLow, diffHigh, competencies };
2230    }
2231
2232    function TekiseiFlow({ trackEvent, updateStatus, statuses, onExit }) {
2233      const [stage, setStage] = useState('start'); // 'start' | 'q' | 'result'
2234      const [qIndex, setQIndex] = useState(0);
2235      const [answers, setAnswers] = useState({});
2236      // stage依存: 結果表示時に解決(Supabaseリモート設定の読込完了後の値を使うため)
2237      const consulAgents = useMemo(() => resolveAgents(ARTICLE_CONFIGS['tenshoku_strategy_consul'] || ARTICLE_CONFIGS.strategy).filter(a => a.affiliateUrl).slice(0, 3), [stage]);
2238      const result = useMemo(() => (stage === 'result' ? computeTekisei(answers) : null), [stage, answers]);
2239
2240      useEffect(() => { if (stage === 'result') trackEvent('result_view'); }, [stage]);
2241
2242      const answer = (key, value) => {
2243        const next = { ...answers, [key]: value };
2244        setAnswers(next);
2245        if (qIndex + 1 < TEKISEI_QUESTIONS.length) {
2246          setQIndex(qIndex + 1);
2247        } else {
2248          setStage('result');
2249        }
2250      };
2251
2252      const handleCtaClick = (agent) => {
2253        if (agent.affiliateUrl) {
2254          window.open(agent.affiliateUrl, '_blank');
2255          updateStatus(agent.id, 'clicked');
2256        }
2257      };
2258
2259      // ---- スタート画面 ----
2260      if (stage === 'start') {
2261        return (
2262          <div className="max-w-lg mx-auto px-4 pt-3 pb-4">
2263            <div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
2264              <div className="bg-gradient-to-br from-indigo-50 to-blue-50 px-5 py-6 text-center">
2265                <div className="text-3xl mb-2">🧭</div>
2266                <h1 className="text-xl font-bold text-gray-800 mb
2266-2 leading-snug">コンサル適性診断</h1>
2267                <p className="text-xs text-gray-500 mb-1">ファームが採用で重視する要素を含む10の質問で、</p>
2268                <p className="text-xs text-gray-500 mb-5">あなたのコンサル適性度と<b className="text-indigo-600">想定年収レンジ</b>がわかります</p>
2269                <button
2270                  onClick={() => { setStage('q'); trackEvent('start_diagnosis'); }}
2271                  className="w-full max-w-xs mx-auto py-3.5 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-base transition-all hover:shadow-lg"
2272                >
2273                  1分で診断スタート →
2274                </button>
2275                <p className="text-[11px] text-gray-400 mt-3">無料・登録不要・すぐに結果がわかります</p>
2276              </div>
2277              <div className="px-4 py-2.5 bg-gray-50 border-t border-gray-100">
2278                <div className="flex items-center justify-center gap-4 text-[11px] text-gray-500">
2279                  <span>✅ 完全無料</span>
2280                  <span>✅ 約1分で完了</span>
2281                  <span>✅ 適性度+想定年収</span>
2282                </div>
2283              </div>
2284            </div>
2285            <div className="flex items-center justify-between gap-2 mt-3 px-1">
2286              <button onClick={onExit} className="text-[11px] text-gray-400 hover:text-gray-600">← 他の診断を見る</button>
2287              <span className="text-[10px] text-gray-400">PR</span>
2288            </div>
2289          </div>
2290        );
2291      }
2292
2293      // ---- 質問画面 ----
2294      if (stage === 'q') {
2295        const q = TEKISEI_QUESTIONS[qIndex];
2296        const progress = ((qIndex + 1) / TEKISEI_QUESTIONS.length) * 100;
2297        return (
2298          <div className="max-w-lg mx-auto px-4 pt-3 pb-4">
2299            <div className="text-right mb-1"><span className="text-[10px] text-gray-400">PR</span></div>
2300            <div className="flex items-center gap-2 mb-4">
2301              {qIndex > 0 && (
2302                <button onClick={() => setQIndex(qIndex - 1)} className="text-xs text-gray-400 hover:text-gray-600 shrink-0">← 戻る</button>
2303              )}
2304              <div className="flex-1 h-1.5 bg-gray-100 rounded-full overflow-hidden">
2305                <div className="h-full bg-indigo-500 rounded-full transition-all duration-300" style={{ width: progress + '%' }} />
2306              </div>
2307            </div>
2308            <div className="text-center mb-4">
2309              <p className="text-[11px] text-gray-400 mb-1">質問 {qIndex + 1} / {TEKISEI_QUESTIONS.length}</p>
2310              <h2 className="text-lg font-bold text-gray-800 leading-snug">{q.title}</h2>
2311              {q.sub && <p className="text-[11px] text-gray-400 mt-1">{q.sub}</p>}
2312            </div>
2313            <div className="space-y-2.5 max-w-sm mx-auto">
2314              {q.options.map(opt => (
2315                <button
2316                  key={opt.value}
2317                  onClick={() => answer(q.key, opt.value)}
2318                  className="w-full py-3 px-4 rounded-xl border border-gray-200 bg-white hover:border-indigo-400 hover:bg-indigo-50 text-left text-sm text-gray-700 font-medium transition-all"
2319                >
2320                  {opt.label}
2321                </button>
2322              ))}
2323            </div>
2324          </div>
2325        );
2326      }
2327
2328      // ---- 結果画面 ----
2329      if (stage === 'result' && result) {
2330        const bandInfo = {
2331          high: { label: 'コンサル適性:高い', color: 'text-emerald-600', bar: 'bg-emerald-500', msg: '思考様式・耐性ともにコンサルタントの働き方と噛み合っています。あとは選考対策の質で決まります。', cta: 'コンサル転職のプロに相談する(無料)' },
2332          mid:  { label: 'コンサル適性:素養あり', color: 'text-indigo-600', bar: 'bg-indigo-500', msg: '土台はあります。足りない部分はケース面接対策と職務経歴の言語化で埋まる範囲です。', cta: 'プロに強み・弱みを聞いてみる(無料)' },
2333          low:  { label: 'コンサル適性:現時点では未知数', 
2333color: 'text-amber-600', bar: 'bg-amber-500', msg: '自己回答ベースでは慎重な結果です。ただし自己認識と市場評価はズレることが多く、プロの面談で覆るケースは珍しくありません。', cta: 'まずはプロに適性を聞いてみる(無料)' },
2334        }[result.band];
2335        const showUp = result.diffHigh > 0;
2336        return (
2337          <div className="max-w-lg mx-auto px-4 pt-3 pb-6 space-y-5">
2338            <div className="text-right"><span className="text-[10px] text-gray-400">PR</span></div>
2339
2340            {/* 適性スコア */}
2341            <div className="bg-white rounded-2xl shadow-lg border border-gray-100 px-5 py-6 text-center">
2342              <p className="text-xs text-gray-400 mb-2">診断結果</p>
2343              <p className={`text-sm font-bold mb-3 ${bandInfo.color}`}>{bandInfo.label}</p>
2344              <div className="flex items-end justify-center gap-1 mb-3">
2345                <span className={`text-5xl font-black ${bandInfo.color}`}>{result.score}</span>
2346                <span className="text-lg text-gray-400 font-bold mb-1">/100</span>
2347              </div>
2348              <div className="h-2.5 bg-gray-100 rounded-full overflow-hidden max-w-xs mx-auto mb-4">
2349                <div className={`h-full rounded-full ${bandInfo.bar} transition-all duration-700`} style={{ width: result.score + '%' }} />
2350              </div>
2351              <p className="text-xs text-gray-600 leading-relaxed max-w-sm mx-auto">{bandInfo.msg}</p>
2352              {/* ファームが重視する3要素の評価 */}
2353              <div className="mt-4 pt-3 border-t border-gray-100 max-w-sm mx-auto">
2354                <p className="text-[10px] text-gray-400 mb-2">ファームが採用で重視する3要素</p>
2355                <div className="grid grid-cols-3 gap-1.5">
2356                  {[
2357                    { label: '問題解決思考', key: 'solve' },
2358                    { label: '業界・領域知見', key: 'domain' },
2359                    { label: 'PJ推進経験', key: 'pm' },
2360                  ].map(c => {
2361                    const lv = result.competencies[c.key];
2362                    const style = lv === 'high' ? 'bg-emerald-50 text-emerald-700 border-emerald-200' : lv === 'mid' ? 'bg-indigo-50 text-indigo-600 border-indigo-200' : 'bg-gray-50 text-gray-500 border-gray-200';
2363                    const mark = lv === 'high' ? '◎ 強み' : lv === 'mid' ? '○ 標準' : '△ 伸びしろ';
2364                    return (
2365                      <div key={c.key} className={`rounded-lg border px-1.5 py-2 text-center ${style}`}>
2366                        <div className="text-[10px] font-bold leading-tight">{c.label}</div>
2367                        <div className="text-[11px] font-black mt-1">{mark}</div>
2368                      </div>
2369                    );
2370                  })}
2371                </div>
2372              </div>
2373            </div>
2374
2375            {/* 想定年収 */}
2376            <div className="bg-white rounded-2xl shadow-lg border border-gray-100 px-5 py-5">
2377              <p className="text-xs font-bold text-gray-500 mb-3">{result.band === 'low' ? '💰 仮にコンサル転職が実現した場合の想定年収(目安)' : '💰 コンサル転職した場合の想定年収(目安)'}</p>
2378              <div className="bg-gradient-to-br from-indigo-50 to-blue-50 rounded-xl px-4 py-4 text-center mb-2">
2379                <p className="text-[11px] text-gray-500 mb-1">想定ポジション:{result.sal.pos}</p>
2380                <p className="text-2xl font-black text-indigo-700">{result.sal.low.toLocaleString()}〜{result.sal.high.toLocaleString()}<span className="text-sm font-bold text-indigo-500">万円</span></p>
2381                {showUp && (
2382                  <p className="text-xs font-bold text-emerald-600 mt-1.5">
2383                    現在の年収帯から {result.diffLow > 0 ? '+' + result.diffLow : '±0'}〜+{result.diffHigh}万円
2384                  </p>
2385                )}
2386                {!showUp && (
2387                  <p className="text-[11px] text-gray-500 mt-1.5">現年収帯が高いため、年収よりも裁量・市場価値の観点での検討がおすすめです</p>
2388                )}
2389              </div>
2390              <p className="text-[10px] text-gray-400 leading-relaxed">※一般的な入社時レンジの目安です。経験・ファーム・選考評価により大きく異なり、本診断は採用可否や年収を保証するものではありません。</p>
2391            </div>
2392
2393            {/* エージェント紹介 */}
2394            <div>
2395              <div className="text-center mb-3">
2396                <h2 className="text-base font-bold text-gray-800">{bandInfo.cta}</h2>
2397                <p className="text-[11px] text-gray-500 mt-1">
2398                  {result.band === 'low'
2399                    ? '無料のキャリア面談で、客観的な適性と市場価値を確認できます'
2400                    : 'コンサル特化エージェントなら、非公開求人とã
2400‚±ãƒ¼ã‚¹é¢æŽ¥å¯¾ç­–まで無料です'}
2401                </p>
2402              </div>
2403              <div className="space-y-4">
2404                {consulAgents.map((agent, i) => (
2405                  <AgentResultCard
2406                    key={agent.id}
2407                    agent={agent}
2408                    matchScore={[93, 88, 84][i] || 80}
2409                    order={i + 1}
2410                    status={statuses[agent.id]?.status || 'pending'}
2411                    onCtaClick={handleCtaClick}
2412                  />
2413                ))}
2414              </div>
2415            </div>
2416
2417            <div className="text-center pt-1">
2418              <button
2419                onClick={() => { setStage('start'); setQIndex(0); setAnswers({}); }}
2420                className="text-sm text-indigo-500 hover:text-indigo-700 hover:underline transition-colors"
2421              >
2422                🔄 もう一度診断する
2423              </button>
2424            </div>
2425          </div>
2426        );
2427      }
2428
2429      return null;
2430    }
2431
2432    // ============================================================
2433    // MAIN APP
2434    // ============================================================
2435    function AgentFinderApp() {
2436      const initialMode = useMemo(() => getInitialMode(), []);
2437      const [mode, setMode] = useState(initialMode || 'freelance');
2438      const [modeSelected, setModeSelected] = useState(initialMode !== null);
2439      const config = useMemo(() => {
2440        const params = new URLSearchParams(window.location.search);
2441        const configKey = params.get('config') || (mode === 'tenshoku' ? 'tenshoku_strategy_consul' : 'strategy');
2442        return ARTICLE_CONFIGS[configKey] || ARTICLE_CONFIGS.strategy;
2443      }, [mode]);
2444      const agents = useMemo(() => resolveAgents(config), [config]);
2445      const isTenshoku = mode === 'tenshoku';
2446
2447      const [phase, setPhase] = useState(initialMode !== null ? 'start' : 'mode_select'); // 'mode_select' | 'start' | 'diagnostic' | 'diagnostic-simple' | 'result' | 'bulk-only'
2448      const [rankedAgents, setRankedAgents] = useState([]);
2449      const [statuses, setStatuses] = useState({});
2450
2451      // iframe高さ通知
2452      useEffect(() => {
2453        const observer = new ResizeObserver(() => notifyHeight());
2454        observer.observe(document.getElementById('root'));
2455        return () => observer.disconnect();
2456      }, []);
2457
2458      useEffect(() => { notifyHeight(); }, [phase, rankedAgents, statuses]);
2459
2460      const updateStatus = useCallback((agentId, status) => {
2461        setStatuses(prev => ({
2462          ...prev,
2463          [agentId]: { ...prev[agentId], status, clickedAt: status === 'clicked' ? new Date() : prev[agentId]?.clickedAt }
2464        }));
2465        // クリックログをlocalStorage + Supabaseに記録
2466        if (status === 'clicked') {
2467          const config = new URLSearchParams(window.location.search).get('config') || 'direct';
2468          const currentMode = mode;
2469          // localStorage(後方互換)
2470          try {
2471            const logs = JSON.parse(localStorage.getItem('af_click_log') || '[]');
2472            logs.push({ agentId, timestamp: new Date().toISOString(), config });
2473            localStorage.setItem('af_click_log', JSON.stringify(logs));
2474          } catch(e) {}
2475          // 管理者・開発環境はSupabase送信しない
2476          let skipSupabase = false;
2477          try {
2478            if (localStorage.getItem('af_admin_auth') === 'true') skipSupabase = true;
2479          } catch(e) {}
2480          const host = (typeof window !== 'undefined' && window.location && window.location.hostname) || '';
2481          if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host.endsWith('.local')) skipSupabase = true;
2482          // Supabase送信
2483          if (!skipSupabase) {
2484            try {
2485              _supabase.from('click_logs').insert({
2486                agent_id: agentId,
2487                agent_name: rankedAgents.find(r => r.agent.id === agentId)?.agent.shortName || agentId,
2488                config: config,
2489                mode: currentMode,
2490                user_agent: navigator.userAgent
2491              }).then(() => {});
2492            } catch(e) {}
2493          }
2494        }
2495      }, [mode, rankedAgents]);
2496
2497      // ファネルイベント追跡
2498      const sessionId = useMemo(() => crypto.randomUUID(), []);
2499      const utmSource = useMemo(() => new URLSearchParams(window.location.search).get('utm_source') || 'direct', []);
2500      const trackEvent = useCallback((eventType) => {
2501        // 管理者・開発環境は計測しない(コール時に毎回チェックしてクロージャ古化を防ぐ)
2502        try {
2503          if (localStorage.getItem('af_admin_auth') === 'true') return;
2504        } catch(e) {}
2505        const host = (typeof window !== 'undefined' && window.location && window.location.hostname) || '';
2506        if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host.endsWith('.local')) return;
2507        try {
2508          _supabase.from('page_events').insert({
2509            event_type: eventType,
2510            mode: mode,
2511            session_id: sessionId,
2512            user_agent: navigator.userAgent,
2513            utm_source: utmSource
2514          }).then(() => {});
2515        } catch(e) {}
2516      }, [mode, sessionId, utmSource]);
2517
2518      // 初回ランディング計測
2519      useEffect(() => { trackEvent('landing'); }, []);
2520
2521      const handleDiagnosticComplete = useCallback((results) => {
2522        setRankedAgents(results);
2523        setPhase('result');
2524        trackEvent('result_view');
2525      }, [trackEvent]);
2526
2527      const handleRestart = useCallback(() => {
2528        setPhase(initialMode !== null ? 'start' : 'mode_select');
2529        setRankedAgents([]);
2530        setStatuses({});
2531      }, []);
2532
2533      const handleBulkOpen = useCallback((selectedAgents) => {
2534        selectedAgents.forEach(agent => {
2535          updateStatus(agent.id, 'clicked');
2536        });
2537      }, [updateStatus]);
2538
2539      // モード選択画面(独立 or 転職)
2540      if (phase === 'mode_select') {
2541        return (
2542          <div className="max-w-lg mx-auto px-4 pt-3 pb-4">
2543            <div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
2544              <div className="bg-gradient-to-br from-brand-gold/5 to-brand-orange/5 px-5 py-6 text-center">
2545                <div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-gradient-to-br from-brand-gold to-brand-orange mb-3 shadow-lg shadow-brand-gold/20">
2546                  <svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
2547                    <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
2548                  </svg>
2549                </div>
2550                <h1 className="text-xl font-bold text-gray-800 mb-1">
2551                  エージェント診断
2552                </h1>
2553                <p className="text-xs text-gray-400 mb-5">
2554                  あなたの目的に合ったエージェントを厳選します
2555                </p>
2556
2557                <div className="space-y-3 max-w-sm mx-auto">
2558                  {[
2559                    {
2560                      mode: 'tenshoku',
2561                      emoji: '\ud83c\udf1f',
2562                      title: '転職エージェント診断',
2563                      sub: 'コンサル・事業会社・スタートアップ',
2564                      tags: ['戦略コンサル', 'IT/DXコンサル', 'M&A/FAS', 'エンジニア', '営業', 'マーケ', '会計/経理'],
2565                      gradient: 'from-emerald-500 to-teal-600',
2566                      hoverGradient: 'hover:from-emerald-600 hover:to-teal-700',
2567                      tagStyle: 'bg-white/20 text-white',
2568                    },
2569                    {
2570                      mode: 'freelance',
2571                      emoji: '\ud83d\ude80',
2572                      title: 'フリーランスエージェント診断',
2573                      sub: '案件探し・単価UP・エージェント比較',
2574                      tags: ['戦略コンサル', 'IT/PMO', 'エンジニア', 'SAP', 'セキュリティ'],
2575                      gradient: 'from-brand-orange to-orange-600',
2576                      hoverGradient: 'hover:from-brand-orange-dark hover:to-orange-700',
2577                      tagStyle: 'bg-white/20 text-white',
2578                    },
2579                    {
2580                      mode: 'tekisei',
2581                      emoji: '🧭',
2582                      title: 'コンサル適性診断',
2583                      sub: '適性スコアと想定年収レンジがわかる',
2584                      tags: ['適性度スコア', '想定年収', '約1分・10問'],
2585                      gradient: 'from-indigo-500 to-blue-600',
2586                      hoverGradient: 'hover:from-indigo-600 hover:to-blue-700',
2587                      tagStyle: 'bg-white/20 text-white',
2588                    },
2589                  ].map(item => (
2590                    <button
2591                      key={item.mode}
2592                      onClick={() => { setMode(item.mode); setModeSelected(true); setPhase('start'); trackEvent('mode_select_' + item.mode); }}
2593                      className={`w-full rounded-2xl bg-gradient-to-r ${item.gradient} ${item.hoverGradient} transition-all duration-200 hover:scale-[1.02] hover:shadow-lg shadow-md px-4 py-3.5 text-left group`}
2594                    >
2595                      <div className="flex items-start gap-3">
2596                        <div className="text-2xl shrink-0 mt-0.5">{item.emoji}</div>
2597                        <div className="flex-1 min-w-0">
2598                          <div className="flex items-center gap-2">
2599                            <span className="text-[15px] font-bold text-white leading-tight">{item.title}</span>
2600                          </div>
2601                          <div className="text-[11px] mt-0.5 text-white/80">{item.sub}</div>
2602                          <div className="flex flex-wrap gap-1 mt-2">
2603                            {item.tags.map(t => (
2604                              <span key={t} className={`text-[10px] rounded-full px-1.5 py-0.5 font-medium ${item.tagStyle}`}>{t}</span>
2605                            ))}
2606                          </div>
2607                        </div>
2608                        <svg className="w-4 h-4 mt-1 text-white/60 group-hover:text-white group-hover:translate-x-1 transition-all shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
2609                          <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
2610                        </svg>
2611                      </div>
2612                    </button>
2613                  ))}
2614                </div>
2615
2616                <div className="flex items-center justify-center gap-3 mt-4 text-[10px] text-gray-400">
2617                  <span>完全無料</span>
2618                  <span className="w-1 h-1 bg-gray-300 rounded-full" />
2619                  <span>10秒で完了</span>
2620                  <span className="w-1 h-1 bg-gray-300 rounded-full" />
2621                  <span>今すぐ診断できる</span>
2622                </div>
2623              </div>
2624            </div>
2625
2626            <div className="flex items-center justify-end gap-2 mt-3 px-1">
2627              <span className="text-[10px] text-gray-400">PR</span>
2628            </div>
2629          </div>
2630        );
2631      }
2632
2633      // コンサル適性診断(自己完結フロー)
2634      if (mode === 'tekisei') {
2635        return (
2636          <TekiseiFlow
2637            trackEvent={trackEvent}
2638            updateStatus={updateStatus}
2639            statuses={statuses}
2640            onExit={() => { setMode('freelance'); setModeSelected(false); setPhase('mode_select'); }}
2641          />
2642        );
2643      }
2644
2645      // スタート画面
2646      if (phase === 'start') {
2647        return (
2648          <div className="max-w-lg mx-auto px-4 pt-3 pb-4">
2649            <div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
2650              <div className="bg-gradient-to-br from-brand-gold/5 to-brand-orange/5 px-5 py-6 text-center">
2651                <div className="text-3xl mb-2">{isTenshoku ? '🌟' : '🎯'}</div>
2652                <h1 className="text-xl font-bold text-gray-800 mb-2 leading-snug">
2653                  {isTenshoku ? (<>あなたに合う<br />転職エージェントを診断</>) : (<>あなたに合う<br />フリーランスエージェントを診断</>)}
2654                </h1>
2655                <p className="text-xs text-gray-500 mb-5">
2656                  {isTenshoku ? '4つの質問に答えるだけで、最適な転職エージェントが見つかります' : '3つの質問に答えるだけで、最適なエージェントが見つかります'}
2657                </p>
2658
2659                <button
2660                  onClick={() => {
2661                    setPhase('diagnostic');
2662                    trackEvent('start_diagnosis');
2663                  }}
2664                  className="w-full max-w-xs mx-auto py-3.5 rounded-xl bg-brand-orange hover:bg-brand-orange-dark text-white font-bold text-base transition-all hover:shadow-lg"
2665                >
2666                  10秒で診断スタート →
2667                </button>
2668
2669                <p className="text-[11px] text-gray-400 mt-3">
2670                  {'無料ですぐに結果がわかります'}
2671                </p>
2672              </div>
2673
2674              <div className="px-4 py-2.5 bg-gray-50 border-t border-gray-100">
2675                <div className="flex items-center justify-center gap-4 text-[11px] text-gray-500">
2676                  <span>✅ 完全無料</span>
2677                  <span>✅ 10秒で完了</span>
2678                  <span>✅ プロが徹底比較</span>
2679                </div>
2680              </div>
2681            </div>
2682
2683            <div className="flex items-center justify-end gap-2 mt-3 px-1">
2684              <span className="text-[10px] text-gray-400">PR</span>
2685            </div>
2686          </div>
2687        );
2688      }
2689
2690      // 診断中(しっかり回答: 既存の3-4問フロー)
2691      if (phase === 'diagnostic') {
2692        return (
2693          <div className="max-w-lg mx-auto px-4 pt-3 pb-4">
2694            <DiagnosticFlow
2695              agents={agents}
2696              mode={mode}
2697              onComplete={handleDiagnosticComplete}
2698            />
2699          </div>
2700        );
2701      }
2702
2703      // 結果表示
2704      if (phase === 'result') {
2705        return (
2706          <div className="max-w-lg mx-auto p-4 space-y-8">
2707            <div className="text-right"><span className="text-xs text-gray-400">PR</span></div>
2708            <ResultView
2709              rankedAgents={rankedAgents}
2710              statuses={statuses}
2711              updateStatus={updateStatus}
2712              onBulkOpen={handleBulkOpen}
2713              onRestart={handleRestart}
2714            />
2715
2716            {/* 一括登録パネル(6位以降のエージェント) */}
2717            {(() => {
2718              const extraAgents = rankedAgents.slice(5).map(r => r.agent).filter(a => a.affiliateUrl);
2719              return extraAgents.length > 0 ? (
2720                <div className="mt-8">
2721                  <BulkRegistrationPanel
2722                    agents={extraAgents}
2723                    statuses={statuses}
2724                    onBulkOpen={handleBulkOpen}
2725                  />
2726                </div>
2727              ) : null;
2728            })()}
2729          </div>
2730        );
2731      }
2732
2733      // 一括登録のみ(診断スキップ)→ 領域別に5社ずつ表示
2734      if (phase === 'bulk-only') {
2735        const categories = isTenshoku
2736          ? [
2737              { key: 'tenshoku_strategy_consul', label: '戦略・経営コンサルタント', icon: '🏢', desc: '戦略ファーム・経営コンサルへの転職' },
2738              { key: 'tenshoku_it_consul', label: 'IT・DXコンサルタント', icon: '💻', desc: 'ITコンサル・DX推進への転職' },
2739              { key: 'tenshoku_ma', label: 'M&A・財務アドバイザリー', icon: '💰', desc: 'FAS/IBD/PEへの転職' },
2740              { key: 'tenshoku_engineer', label: 'エンジニア・IT技術職', icon: '⚡', desc: 'ITエンジニア・技術職への転職' },
2741              { key: 'tenshoku_accounting', label: '会計・税務・経理', icon: '📊', desc: '経理・財務・会計への転職' },
2742              { key: 'tenshoku_sales', label: '営業・セールス', icon: '🤝', desc: '営業・セールス職への転職' },
2743              { key: 'tenshoku_marketing', label: 'マーケティング', icon: '📣', desc: 'マーケティング職への転職' },
2744              { key: 'tenshoku_other', label: 'その他', icon: '📋', desc: 'その他の職種への転職' },
2745            ]
2746          : [
2747              { key: 'strategy', label: '戦略コンサル', icon: '🎯', desc: '戦略策定・新規事業・M&A' },
2748              { key: 'biz_consul', label: '業務コンサル', icon: '📋', desc: '業務改善・BPR' },
2749              { key: 'it_consul', label: 'ITコンサル / PMO', icon: '💻', desc: 'システム導入・PMO・DX' },
2750              { key: 'engineering', label: 'エンジニアリング', icon: '⚡', desc: '開発・インフラ・SES' },
2751              { key: 'sap', label: 'SAP案件', icon: '🔧', desc: 'SAP導入・運用保守' },
2752              { key: 'security', label: 'セキュリティ', icon: '🛡️', desc: 'セキュリティコンサル・エンジニア' },
2753            ];
2754
2755        // カテゴリごとのエージェントとチェック状態を管理
2756        const allCatData = categories.map(cat => {
2757          const cfg = ARTICLE_CONFIGS[cat.key];
2758          if (!cfg) return null;
2759          const catAgents = resolveAgents(cfg).slice(0, 5);
2760          return { ...cat, agents: catAgents };
2761        }).filter(Boolean);
2762
2763        return (
2764          <BulkOnlyView
2765            categories={allCatData}
2766            statuses={statuses}
2767            updateStatus={updateStatus}
2768            onBack={() => setPhase('start')}
2769          />
2770        );
2771      }
2772
2773      return null;
2774    }
2775
2776    // ============================================================
2777    // Supabase API から設定を非同期読み込み → グローバル変数を上書きして再レンダー
2778    // ============================================================
2779    let _remoteSettingsResolve;
2780    const _remoteSettingsReady = new Promise(r => { _remoteSettingsResolve = r; });
2781    let _settingsVersion = 0; // インクリメントで再レンダーをトリガー
2782
2783    (async function _loadRemoteSettings() {
2784      try {
2785        const resp = await fetch('/api/settings');
2786        if (!resp.ok) { _remoteSettingsResolve(); return; }
2787        const data = await resp.json();
2788        if (!data || Object.keys(data).length === 0) { _remoteSettingsResolve(); return; }
2789
2790        // af_configs を適用
2791        if (data.af_configs) {
2792          const remoteConfigs = typeof data.af_configs === 'string' ? JSON.parse(data.af_configs) : data.af_configs;
2793          Object.keys(remoteConfigs).forEach(key => {
2794            if (ARTICLE_CONFIGS[key]) {
2795              ARTICLE_CONFIGS[key] = { ...ARTICLE_CONFIGS[key], ...remoteConfigs[key] };
2796            }
2797          });
2798        }
2799
2800        // af_questions を適用(グローバル変数を上書き)
2801        if (data.af_questions) {
2802          const rq = typeof data.af_questions === 'string' ? JSON.parse(data.af_questions) : data.af_questions;
2803          if (rq.Q1_FREELANCE) Object.assign(Q1_FREELANCE, rq.Q1_FREELANCE);
2804          if (rq.Q1_TENSHOKU) Object.assign(Q1_TENSHOKU, rq.Q1_TENSHOKU);
2805          if (rq.Q2_FREELANCE) Object.assign(Q2_FREELANCE, rq.Q2_FREELANCE);
2806          if (rq.Q2_TENSHOKU) Object.assign(Q2_TENSHOKU, rq.Q2_TENSHOKU);
2807          if (rq.Q3_FREELANCE) Object.assign(Q3_FREELANCE, rq.Q3_FREELANCE);
2808          if (rq.Q3_TENSHOKU) Object.assign(Q3_TENSHOKU, rq.Q3_TENSHOKU);
2809          if (rq.Q4_TENSHOKU) Object.assign(Q4_TENSHOKU, rq.Q4_TENSHOKU);
2810          // Q3動的選択肢: 既存キーを全削除してから上書き(admin側で削除されたキーも消す)
2811          if (rq._Q3_OPTIONS_BY_Q2) {
2812            Object.keys(Q3_OPTIONS_BY_Q2).forEach(k => delete Q3_OPTIONS_BY_Q2[k]);
2813            Object.assign(Q3_OPTIONS_BY_Q2, rq._Q3_OPTIONS_BY_Q2);
2814          }
2815          if (rq._Q3_AGENT_BOOSTS) {
2816            // 旧形式(q3val のみ)を新形式(q2val:q3val 複合キー)に移行
2817            const migrated = _migrateQ3Boosts({
2818              _Q3_OPTIONS_BY_Q2: rq._Q3_OPTIONS_BY_Q2 || Q3_OPTIONS_BY_Q2,
2819              _Q3_AGENT_BOOSTS: rq._Q3_AGENT_BOOSTS,
2820            });
2821            Object.keys(Q3_AGENT_BOOSTS).forEach(k => delete Q3_AGENT_BOOSTS[k]);
2822            Object.assign(Q3_AGENT_BOOSTS, migrated._Q3_AGENT_BOOSTS);
2823          }
2824        }
2825
2826        // af_master を適用(表示系フィールドのみ上書き。スコアリング用フィールドはagents-master.jsが常に正)
2827        if (data.af_master) {
2828          const remoteMaster = typeof data.af_master === 'string' ? JSON.parse(data.af_master) : data.af_master;
2829          // スコアリングに影響するフィールドは上書きしない(agents-master.jsが正)
2830          const PROTECTED_FIELDS = ['targetGoal', 'targetDomain', 'categories', 'targetExperience', 'targetWorkStyle'];
2831          if (Array.isArray(remoteMaster)) {
2832            remoteMaster.forEach(rm => {
2833              const idx = AGENTS_MASTER.findIndex(a =>
2833 a.id === rm.id);
2834              if (idx >= 0) {
2835                const safeOverride = { ...rm };
2836                PROTECTED_FIELDS.forEach(f => delete safeOverride[f]);
2837                AGENTS_MASTER[idx] = { ...AGENTS_MASTER[idx], ...safeOverride };
2838              }
2839            });
2840          }
2841        }
2842
2843        // af_logic を適用
2844        if (data.af_logic) {
2845          const rl = typeof data.af_logic === 'string' ? JSON.parse(data.af_logic) : data.af_logic;
2846          if (rl.requiredAgents) Object.assign(Q2_REQUIRED_AGENTS, rl.requiredAgents);
2847          if (rl.domainMap) Object.assign(_domainMap, rl.domainMap);
2848          if (rl.goalMap) Object.assign(_goalMap, rl.goalMap);
2849          if (rl.weights) Object.assign(_weights, rl.weights);
2850        }
2851
2852        _settingsVersion++;
2853        console.log('[AF] Remote settings loaded from Supabase API');
2854      } catch(e) {
2855        console.warn('[AF] Failed to load remote settings:', e.message);
2856      }
2857      _remoteSettingsResolve();
2858    })();
2859
2860    // ============================================================
2861    // RENDER
2862    // ============================================================
2863    const root = ReactDOM.createRoot(document.getElementById('root'));
2864    root.render(<AgentFinderApp />);
2865
2866    // リモート設定が到着したら再レンダー(診断開始前に適用されるよう)
2867    _remoteSettingsReady.then(() => {
2868      if (_settingsVersion > 0) {
2869        root.render(<AgentFinderApp key={_settingsVersion} />);
2870      }
2871    });
2872  </script>
2872
2873</body>
2874</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.