/* app.js (Final Clean Version) */

// =========================================================
// 1. グローバル変数・状態管理 (必ずファイルの一番上に記述)
// =========================================================
let lastPersonalData = null;
let currentPersonalKeyType = null;
let currentPersonalKeyValue = null;
let currentWikiContent = ''; // 再代入するため let 必須
let lastSearchState = null;
let lastRMState = null;
let lastBackState = null;

// 月間ランキング用
let monthlyOptionsLoaded = false;
let monthlyOffset = 0;

// 村タブ用
let village_loadedOnce = false;

// 名前チェイン用
let nameChainCurrentPage = 1;
let nameChainCurrentOrder = 'last_ts_desc';
let nameChainIsLoading = false;

// 掲示板用
let bbsOffset = 0;

// ログイン状態
let loginState = { logged_in: false, trip: null, name: null };
const LOGIN_IDENTITY_STORAGE_KEY = 'zinro_login_identity';

// タブ制御フラグ
let isProgrammaticTabChange = false;


// =========================================================
// 2. ユーティリティ関数
// =========================================================
function qs(id) { return document.getElementById(id); }
function ce(tag) { return document.createElement(tag); }
function qsAny(ids) {
  for (const id of ids) {
    const el = document.getElementById(id);
    if (el) return el;
  }
  return null;
}


// HTMLエスケープ
function escapeHtml(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

// 共通API呼び出し
async function apiGet(params) {
  const p = new URLSearchParams(params);
  const res = await fetch('api.php?' + p.toString(), { credentials: 'include' });
  const json = await res.json();
  if (!json.ok) throw new Error(json.error || 'API error');
  return json;
}

// タブ切り替え
function switchTab(tabId) {
  const btn = document.querySelector(`.tab-btn[data-tab="${tabId}"]`);
  if (!btn) return;
  isProgrammaticTabChange = true;
  btn.click();
  isProgrammaticTabChange = false;
}

// トリップ正規化（◆などを除去して整形）
function normalizeTripForPersonal(trip) {
  return String(trip || '').trim(); // ★付けない・剥がさない
}


// 日付フォーマット (YYYY-MM-DD)
function nh_formatDate(ts) {
  if (!ts) return '';
  const d = new Date(ts * 1000);
  const y = d.getFullYear();
  const m = ('0' + (d.getMonth() + 1)).slice(-2);
  const day = ('0' + d.getDate()).slice(-2);
  return `${y}-${m}-${day}`;
}


// =========================================================
// 3. UI操作・ロジック関数定義
// =========================================================
async function saveWiki(e) {
  if (e?.preventDefault) e.preventDefault();

  if (!loginState.logged_in) {
    alert('ログインしてください。');
    return;
  }

const wikiBox = qs('wikiBox');

const keyType =
  (String(currentPersonalKeyType ?? '').trim() || String(wikiBox?.dataset.keyType ?? '').trim() || String(qs('key_type')?.value ?? '').trim()) || null;

const keyValue =
  (String(currentPersonalKeyValue ?? '').trim() || String(wikiBox?.dataset.keyValue ?? '').trim() || String(qs('key_value')?.value ?? '').trim()) || null;

if (!keyType || !keyValue) {
  alert('先に検索をしてください。');
  return;
}




  const body = qs('wikiBody')?.value ?? '';

  try {
    const res = await fetch('api.php?mode=wiki_save', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ key_type: keyType, key_value: keyValue, body })
    });
    const data = await res.json().catch(() => ({}));

    if (!res.ok || !data.ok) {
      alert(data.error || 'Wiki保存に失敗しました');
      return;
    }

    // 表示更新
    currentWikiContent = body;
const wikiViewBody = qs('wikiViewBody') || qs('wikiView'); // wikiViewBodyが無くても動く
if (wikiViewBody) {
  wikiViewBody.innerHTML = currentWikiContent
    ? `<div style="white-space:pre-wrap;">${escapeHtml(currentWikiContent)}</div>`
    : '<span style="color:var(--muted);">まだWikiは登録されていません。</span>';
}


    setWikiEditing(false);
    const action = qs("wikiEditToggleArea"); // ★ここ！

if (action) action.style.display = loginState.logged_in ? 'block' : 'none';

  } catch (err) {
    console.error(err);
    alert('通信エラー');
  }
}


function setWikiEditing(flag) {
  const wikiView = qs("wikiView");
  const wikiEdit = qs("wikiEdit");
  const editBtn  = qs("wikiEditToggleBtn");
  const saveBtn  = qs("wikiSaveBtn");
  const cancelBtn= qs("wikiEditCancelBtn");

  if (!wikiView || !wikiEdit) return;

  if (flag) {
    wikiView.style.display = "none";
    wikiEdit.style.display = "block";

    // ボタン表示
    if (editBtn) editBtn.style.display = "none";
    if (saveBtn) saveBtn.style.display = "inline-block";
    if (cancelBtn) cancelBtn.style.display = "inline-block";

    const wikiBody = qs("wikiBody");
    if (wikiBody) wikiBody.value = currentWikiContent || '';
  } else {
    wikiView.style.display = "block";
    wikiEdit.style.display = "none";

    // ボタン表示（ログイン中のみ編集を出す）
    if (editBtn) editBtn.style.display = loginState.logged_in ? "inline-block" : "none";
    if (saveBtn) saveBtn.style.display = "none";
    if (cancelBtn) cancelBtn.style.display = "none";
  }
}



function initWikiEditUI() {
  const toggleBtn = qs('wikiEditToggleBtn');
  const cancelBtn = qs('wikiEditCancelBtn');

  if (toggleBtn) {
    toggleBtn.addEventListener('click', () => {
      const edit = qs('wikiEdit');
      const isEditing = edit && edit.style.display !== 'none';
      setWikiEditing(!isEditing);
    });
  }
  if (cancelBtn) cancelBtn.addEventListener('click', () => setWikiEditing(false));
  
  // 初期状態をセット
  setWikiEditing(false);
}

// --- 履歴管理（名前/トリップ別、各5件） ---
const HISTORY_MAX = 5;

function historyKey(type) {
  return (type === 'trip') ? 'personalHistory_trip' : 'personalHistory_name';
}

function saveSearchHistory(type, q) {
  const t = (type === 'trip') ? 'trip' : 'name';
  const v = String(q || '').trim();
  if (!v) return;

  let list = JSON.parse(localStorage.getItem(historyKey(t)) || '[]');
  list = list.filter(x => x !== v);
  list.unshift(v);
  list = list.slice(0, HISTORY_MAX);
  localStorage.setItem(historyKey(t), JSON.stringify(list));
}

function getSearchHistory(type) {
  const t = (type === 'trip') ? 'trip' : 'name';
  return JSON.parse(localStorage.getItem(historyKey(t)) || '[]');
}

function ensureHistoryBoxFor(btnId, boxId) {
  const btn = qs(btnId);
  if (!btn) return null;

  let box = qs(boxId);
  if (!box) {
    box = ce('span');
    box.id = boxId;
    box.style.marginLeft = '10px';
    box.style.fontSize = '.85rem';
    box.style.color = 'var(--muted)';
    btn.insertAdjacentElement('afterend', box);
  }
  return box;
}

function renderSearchHistoryButtons() {
  // 名前
  const nameBox = ensureHistoryBoxFor('personalNameSearchBtn', 'historyNameBox');
  if (nameBox) {
    const list = getSearchHistory('name');
    nameBox.innerHTML = list.length
      ? `最近の検索：` + list.map(v =>
          `<button type="button" class="btn btn-ghost hist-mini" data-hist-type="name" data-q="${escapeHtml(v)}">${escapeHtml(v)}</button>`
        ).join(' ')
      : '';
  }

  // トリップ
  const tripBox = ensureHistoryBoxFor('personalTripSearchBtn', 'historyTripBox');
  if (tripBox) {
    const list = getSearchHistory('trip');
    tripBox.innerHTML = list.length
      ? `最近の検索：` + list.map(v =>
          `<button type="button" class="btn btn-ghost hist-mini" data-hist-type="trip" data-q="${escapeHtml(v)}">${escapeHtml(v)}</button>`
        ).join(' ')
      : '';
  }
}


// --- 画面遷移ヘルパー ---
function jumpToPersonalByTrip(rawTrip) {
  const trip = normalizeTripForPersonal(rawTrip);
  if (!trip) return;
  const activeTab = document.querySelector('.tab-btn.active')?.dataset.tab || 'tab-personal';
  lastBackState = { tab: activeTab, scrollY: window.scrollY || 0 };
  switchTab('tab-personal');
  if (qs('personalTripQuery')) qs('personalTripQuery').value = trip;
  if (qs('personalNameQuery')) qs('personalNameQuery').value = '';
  runPersonalSearch('trip', trip);
}

function jumpToPersonalByName(name) {
  const v = String(name || '').trim();
  if (!v) return;
  const activeTab = document.querySelector('.tab-btn.active')?.dataset.tab || 'tab-personal';
  lastBackState = { tab: activeTab, scrollY: window.scrollY || 0 };
  switchTab('tab-personal');
  if (qs('personalNameQuery')) qs('personalNameQuery').value = v;
  if (qs('personalTripQuery')) qs('personalTripQuery').value = '';
  runPersonalSearch('name', v);
}

function jumpToPersonalByLabel(label) {
  const s = String(label || '').trim();
  if (!s) return;
  if (s.startsWith('◆')) jumpToPersonalByTrip(s);
  else jumpToPersonalByName(s);
}

// --- 戻るボタン表示 ---
function showBackGeneral() {
  const box = qs('personalBackGeneral');
  if (!box) return;
  if (!lastSearchState) {
    box.style.display = 'none';
    box.innerHTML = '';
    return;
  }
  box.style.display = 'block';
  box.innerHTML = `<button type="button" class="btn btn-primary" id="backGeneralBtn">直前に戻る</button>`;
  qs('backGeneralBtn').addEventListener('click', () => {
    const state = lastSearchState;
    lastSearchState = null;
    if (!state) return;
    switchTab(state.tab);
    setTimeout(() => {
      if (state.type === 'personal') {
        if (qs('personalQuery')) qs('personalQuery').value = state.params.q || '';
        loadPersonal({ skipHistory: true });
      } else if (state.type === 'monthly') {
        // パラメータ復元
        const p = state.params || {};
        if (qs('monthlyScope')) qs('monthlyScope').value = p.scope || 'all';
        updateMonthlyScopeUI();
        if (qs('monthlyYm') && p.ym) qs('monthlyYm').value = p.ym;
        if (qs('monthlyYear') && p.year) qs('monthlyYear').value = p.year;
        if (qs('monthlyKind')) qs('monthlyKind').value = p.kind || 'games';
        if (qs('monthlyKeyType')) qs('monthlyKeyType').value = p.keyType || 'trip';
        if (qs('monthlyMinCount')) qs('monthlyMinCount').value = String(p.minCount || 0);
        updateMonthlyKeyTypeUI();
        refreshPillGroups();
        monthlyOffset = 0;
        loadMonthly({ skipHistory: true });
      } else if (state.type === 'rm') {
        if (qs('multiTripInput')) qs('multiTripInput').value = state.params.raw || '';
        loadMultiTrip({ skipHistory: true });
      }
      window.scrollTo(0, state.scrollY || 0);
    }, 0);
  });
}

function showBackToRM() {
  const box = qs('personalBackFromRm');
  if (!box) return;
  if (!lastRMState || !lastRMState.fromRM) {
    box.style.display = 'none';
    box.innerHTML = '';
    return;
  }
  box.style.display = 'block';
  box.innerHTML = '<button type="button" class="btn btn-primary" id="backToRmBtn">一斉検索結果に戻る</button>';
  const backBtn = qs('backToRmBtn');
  if (backBtn) {
    backBtn.addEventListener('click', () => {
      switchTab('tab-rm');
      setTimeout(() => window.scrollTo(0, lastRMState.scrollY || 0), 0);
    });
  }
}

// --- 個人戦績検索実行 ---
function runPersonalSearch(type, rawValue) {
  const t = (type === 'trip') ? 'trip' : 'name';
  let v = (rawValue != null)
    ? String(rawValue).trim()
    : (
      t === 'trip'
        ? (qs('personalTripQuery')?.value ?? qs('personalQuery')?.value ?? '')
        : (qs('personalNameQuery')?.value ?? qs('personalQuery')?.value ?? '')
    ).trim();

  if (!v) {
    alert(t === 'trip' ? 'トリップを入力してください。' : '名前を入力してください。');
    return;
  }

  if (t === 'trip') {
    v = normalizeTripForPersonal(v);
    if (!v) { alert('トリップ形式が不正です。'); return; }
    if (qs('personalTripQuery')) qs('personalTripQuery').value = v;
  } else {
    if (qs('personalNameQuery')) qs('personalNameQuery').value = v;
  }

  // hidden設定
  if (qs('key_type')) qs('key_type').value = t;
  if (qs('key_value')) qs('key_value').value = v;
  // 互換用
  if (qs('personalQuery')) qs('personalQuery').value = v;

  loadPersonal({ keyType: t, keyValue: v });
}

// --- 個人戦績データロード ---
async function loadPersonal(opts = {}) {
  if (qs('personalHaiyakuTable')) qs('personalHaiyakuTable').querySelector('tbody').innerHTML = '';
  if (qs('personalRoleTable')) qs('personalRoleTable').querySelector('tbody').innerHTML = '';
  if (qs('personalVillageTable')) qs('personalVillageTable').querySelector('tbody').innerHTML = '';
  if (qs('personalPartnerTable')) qs('personalPartnerTable').querySelector('tbody').innerHTML = '';  
  // ★ ここが重要！「逆引き」や「最近のログ」もクリアする
  if (qs('personalRecent')) qs('personalRecent').innerHTML = '';
  if (qs('personalReverseInline')) qs('personalReverseInline').innerHTML = ''; 
  if (qs('personalReverseHeadline')) qs('personalReverseHeadline').innerHTML = '';
  if (qs('personalCalendar')) qs('personalCalendar').innerHTML = ''; // カレンダーも重ねないように

  const forcedType = opts.keyType || qs('key_type')?.value || '';
  const forcedValue = opts.keyValue || qs('key_value')?.value || '';
  const q = String(forcedValue || (qs('personalQuery')?.value.trim() ?? '')).trim();

  const resultMain = qs('personalResultMain');
  const resultSide = qs('personalResultSide');
  const loadingEl = qs('personalLoading');
  const summaryEl = qs('personalSummary');
  const queryLabelEl = qs('personalQueryLabel');

  if (resultMain) resultMain.style.display = 'none';
  if (resultSide) resultSide.style.display = 'none';
  if (loadingEl) loadingEl.style.display = 'none';

  // テーブルクリア
  if (qs('personalHaiyakuTable')) qs('personalHaiyakuTable').querySelector('tbody').innerHTML = '';
  if (qs('personalRoleTable')) qs('personalRoleTable').querySelector('tbody').innerHTML = '';
  if (qs('personalVillageTable')) qs('personalVillageTable').querySelector('tbody').innerHTML = '';
  if (qs('personalPartnerTable')) qs('personalPartnerTable').querySelector('tbody').innerHTML = '';
  if (qs('personalRecent')) qs('personalRecent').innerHTML = '';
  if (summaryEl) summaryEl.textContent = '';
  if (queryLabelEl) queryLabelEl.textContent = '';

  if (!q) {
    if (summaryEl) summaryEl.textContent = '検索語を入力してください。';
    return;
  }

  if (loadingEl) loadingEl.style.display = 'block';

  try {
    let url = `api.php?mode=personal&q=${encodeURIComponent(q)}`;
    if (forcedType) url += `&key_type=${encodeURIComponent(forcedType)}`;

    const res = await fetch(url, { credentials: 'include' });
    if (!res.ok) throw new Error('Server Error');
    const data = await res.json();

    if (!data.ok) {
      if (summaryEl) summaryEl.textContent = data.msg || data.error || 'エラー';
      return;
    }
    if (!data.found) {
      if (summaryEl) summaryEl.textContent = `「${data.query}」の戦績はありません。`;
      return;
    }

    // データ格納
currentPersonalKeyType  = data.key_type;
currentPersonalKeyValue = data.query; // ★ここが本体


if (queryLabelEl) {
  const kind = data.key_type === 'trip' ? 'トリップ' : '名前';
  const shareUrl = buildPersonalShareUrl(data.key_type, data.query);

  queryLabelEl.innerHTML = `
    <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
      <div style="flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">
        検索語：${escapeHtml(data.query)}（${kind}で集計）
      </div>
      <div id="personalShareWrap" style="display:flex; align-items:center; gap:8px;">
        <button type="button" class="btn-share" id="personalShareBtn">この戦績URLを共有</button> <button type="button" class="btn-share" id="personalCardBtn">戦績カードを保存</button>
      </div>
    </div>
  `;

  const b = qs('personalShareBtn');
  if (b) {
    b.addEventListener('click', async () => {
      const wrap = qs('personalShareWrap');
      if (!wrap) return;

      // 既存アラートを消す（連打対策）
      wrap.querySelectorAll('.share-alert').forEach(e => e.remove());

      try {
        await copyToClipboard(shareUrl);

const alert = document.createElement('span');
alert.className = 'share-alert';
alert.textContent = 'コピーしました';

// ★ボタンの左に出す（wrapの先頭へ）
wrap.prepend(alert);

setTimeout(() => alert.remove(), 1200);

      } catch (e) {
        prompt('共有URL（コピーしてね）', shareUrl);
      }
    });
  }
}


qs('personalShareBtn')?.addEventListener('click', async () => {
  const wrap = qs('personalShareWrap');
  if (!wrap) return;

  // 既存アラートを消す（連打対策）
  wrap.querySelectorAll('.share-alert').forEach(e => e.remove());

  try {
    await navigator.clipboard.writeText(shareUrl);

    const alert = document.createElement('span');
    alert.className = 'share-alert';
    alert.textContent = 'コピーしました';
    wrap.appendChild(alert);

    setTimeout(() => alert.remove(), 1200);
  } catch {
    prompt('共有URL', shareUrl);
  }
});



    // 概要表示
    window._lastPersonalData = data;
    const s = data.summary;
    let ratingHtml = '';
    if (data.rating && data.rating.games > 0) {
      ratingHtml = `<div>レート：<b>${data.rating.rating}</b> （対象戦数 <b>${data.rating.games}</b>）</div><br>`;
    } else if (data.key_type !== 'trip') {
      ratingHtml = `<span style="color:var(--muted);">※レートはトリップ検索で表示されます。</span><br>`;
    } else {
      ratingHtml = `<span style="color:var(--muted);">レート対象の対戦がまだありません。</span><br>`;
    }

    // 割れ酉（同一トリップを多数の名前で使用）に該当する場合の注記
    const sharedHtml = data.is_shared_trip
      ? `<div style="margin-top:6px;padding:6px 8px;border:1px solid rgba(255,159,159,.5);border-radius:6px;color:#ffb3b3;">⚠️ このトリップは「割れ酉」判定です（同じトリップが20人以上の異なる名前で使われた実績あり）。複数人での共有のほか、意図的に酉を公開・使い回ししている有名プレイヤーも該当します。不正の断定ではありません。</div>`
      : '';

    const statsUpdatedAt = Number(data.stats_updated_at || 0);
    const statsCheckedAt = Number(data.stats_checked_at || 0);
    const statsUpdatedEl = qs('personalStatsUpdatedAt');
    if (statsUpdatedEl) {
      const updatedText = statsUpdatedAt > 0
        ? new Date(statsUpdatedAt * 1000).toLocaleString('ja-JP')
        : '取得できません';
      const checkedText = statsCheckedAt > 0
        ? new Date(statsCheckedAt * 1000).toLocaleString('ja-JP')
        : '取得できません';
      statsUpdatedEl.textContent = `戦績データ更新：${updatedText} ／ 確認：${checkedText}`;
    }

    if (summaryEl) {
      summaryEl.innerHTML =
        `総戦数 <b>${s.total_games}</b>（勝ち <b>${s.wins}</b> / 勝率 <b>${s.win_rate}%</b>）<br>` +
        ratingHtml +
        `生存日あたり発言数：<b>${s.avg_talk_per_day}</b> 回/日` +
        sharedHtml;
    }

// 履歴保存（名前/トリップ別）
if (!opts.skipHistory) {
  saveSearchHistory(data.key_type, data.query); // data.key_type は 'name' or 'trip'
  renderSearchHistoryButtons();
}


    if (resultMain) resultMain.style.display = 'block';
    if (resultSide) resultSide.style.display = 'block';

    // 変名の系譜図（トリップ検索でチェーンデータがある場合のみ）
    renderChainFig(data.trip_chain);

    // AI解析（トリップ検索時・解析済みの場合のみ）
    await renderPersonalAiTrial(data.key_type, data.query);

    // カレンダー
    if (data.calendar) {
      initPlayCalendar(qs('personalCalendar'), data.calendar);
    }

    // テーブル描画
    const haiBody = qs('personalHaiyakuTable')?.querySelector('tbody');
    (data.haiyaku_stats || []).forEach(r => {
      const tr = ce('tr');
      tr.innerHTML = `<td>${escapeHtml(r.haiyaku)}</td><td class="num">${r.games}</td><td class="num">${r.wins}</td><td class="num">${r.games ? r.win_rate + '％' : '-'}</td>`;
      haiBody?.appendChild(tr);
    });

    const roleBody = qs('personalRoleTable')?.querySelector('tbody');
    (data.role_stats || []).forEach(r => {
      const tr = ce('tr');
      tr.innerHTML = `<td>${escapeHtml(r.role)}</td><td class="num">${r.games}</td><td class="num">${r.wins}</td><td class="num">${r.games ? r.win_rate + '％' : '-'}</td>`;
      roleBody?.appendChild(tr);
    });

    const vilBody = qs('personalVillageTable')?.querySelector('tbody');
    (data.village_top || []).forEach(r => {
      const tr = ce('tr');
      tr.innerHTML = `<td>${escapeHtml(r.village)}</td><td class="num">${r.games}</td>`;
      vilBody?.appendChild(tr);
    });

    const parBody = qs('personalPartnerTable')?.querySelector('tbody');
    (data.partners_top || []).forEach(r => {
      const tr = ce('tr');
      const td1 = ce('td');
      const b = ce('button');
      b.type = 'button';
      b.className = 'name-trip-link-button';
      b.dataset.label = r.key || '';
      b.textContent = r.key || '';
      td1.appendChild(b);
      const td2 = ce('td');
      td2.className = 'num';
      td2.textContent = String(r.games ?? 0);
      tr.appendChild(td1); tr.appendChild(td2);
      parBody?.appendChild(tr);
    });

// 最近ログ
const recentEl = qs('personalRecent');
const recentParts = (data.recent_logs || []).map(r => {
  const d = new Date((r.started_at + 9 * 3600) * 1000);
  const ym = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
  return `<div><a href="https://zinro.net/m/log.php?id=${r.log_id}" target="_blank" rel="noopener">${escapeHtml(r.village_name)}</a>（${ym} / id=${r.log_id}）</div>`;
});
if (recentEl) recentEl.innerHTML = recentParts.join('');


    renderProfileWikiComments(data);
    renderReverseLookup(data);


} catch (e) {
  console.error(e);
  if (summaryEl) summaryEl.textContent = 'エラー: ' + (e?.message || e);
  } finally {
    if (loadingEl) loadingEl.style.display = 'none';
  }

  showBackGeneral();
  showBackToRM();
}

function renderReverseLookup(data) {
  const box = qs('personalReverseInline') || qs('personalReverseBox');
  if (!box) return;

  const hbox = qs('personalReverseHeadline'); // 見出し差し込み先（任意）
  const clearAll = () => {
    box.innerHTML = '';
    if (hbox) hbox.innerHTML = '';
  };

  const rev = data.reverse || null;
  if (!rev) { clearAll(); return; }

  const hasN2T = Array.isArray(rev.names_to_trips) && rev.names_to_trips.length > 0;
  const hasT2N = Array.isArray(rev.trips_to_names) && rev.trips_to_names.length > 0;

  if (!hasN2T && !hasT2N) { clearAll(); return; }

  // 見出しモード決定（両方あるなら「名前→トリップ＆トリップ→名前」でもOK）
  let headlineLabel = '';
  if (hasN2T && !hasT2N) headlineLabel = '名前 → トリップ';
  else if (!hasN2T && hasT2N) headlineLabel = 'トリップ → 名前';
  else headlineLabel = '名前 → トリップ / トリップ → 名前';

  if (hbox) {
    hbox.innerHTML = `<h3 class="reverse-h3">逆引き（${headlineLabel}）</h3>`;
  }

  let html = '';

  // 名前→トリップ
  if (hasN2T) {
    rev.names_to_trips.slice(0, 10).forEach(row => {
      const trips = row.trips ?? [];
      const tripLinks = trips.map(t => {
        const tripDisp = String(t.trip ?? '').trim(); // ★DB尊重（◆も含めてそのまま）
        const g = (t.games != null) ? ` <span style="color:var(--muted);">(${t.games})</span>` : '';
        return `<button type="button" class="trip-link-button" data-trip="${escapeHtml(tripDisp)}">${escapeHtml(tripDisp)}</button>${g}`;
      }).join(' ');
      html += `<div style="margin:4px 0;">${tripLinks || '<span style="color:var(--muted);">なし</span>'}</div>`;
    });
  }

  // トリップ→名前
  if (hasT2N) {
    rev.trips_to_names.slice(0, 10).forEach(row => {
      const tripDisp = String(row.trip ?? '').trim(); // ★DB尊重（normalizeしない）
      const names = row.names ?? [];
      const nameLinks = names.map(n => {
        const g = (n.games != null) ? ` <span style="color:var(--muted);">(${n.games})</span>` : '';
        return `<button type="button" class="name-trip-link-button" data-label="${escapeHtml(n.name)}">${escapeHtml(n.name)}</button>${g}`;
      }).join(' ');
      html += `<div style="margin:4px 0;">${nameLinks || '<span style="color:var(--muted);">なし</span>'}</div>`;
    });
  }

  box.innerHTML = html;
}


function normTrip(t) {
  return String(t || '').trim().replace(/^◆+/, '');
}



// --- Wiki/プロフィール/コメント描画 ---
function renderProfileWikiComments(data) {
  lastPersonalData = data;
currentPersonalKeyType  = data.key_type;
currentPersonalKeyValue = data.query ?? data.key_value ?? qs('key_value')?.value ?? null;
if (qs('key_type'))  qs('key_type').value  = currentPersonalKeyType || '';
if (qs('key_value')) qs('key_value').value = currentPersonalKeyValue || '';


  const profileView = qs('profileView');
  const profileEdit = qs('profileEdit');
  const wikiView = qs('wikiView');
  const wikiBody = qs('wikiBody');
  const commentList = qs('commentList');
  const commentForm = qsAny(['commentForm', 'comment-form', 'commentFormBox']);
  const selfIntroEl = qs('profileSelfIntro');
  const xIdEl = qs('profileXId');
  const xDispEl = qs('profileXDisplay');


const wikiBox = qs('wikiBox'); // wiki全体の親要素
if (wikiBox) {
wikiBox.dataset.keyType  = data.key_type;
wikiBox.dataset.keyValue = (data.query ?? data.key_value ?? '').trim();
}



  // プロフィール
  if (data.key_type === 'trip' && data.profile) {
    const p = data.profile;
    let html = '';
    if (p.self_intro) {
      html += `<div style="white-space:pre-wrap;">${escapeHtml(p.self_intro)}</div>`;
    } else {
      html += `<div style="color:var(--muted);">自己紹介はまだ登録されていません。</div>`;
    }
    if (p.x_id) {
      const raw = p.x_id.trim();
      const id = raw.replace(/^@/, '');
      const label = p.x_display && p.x_display.trim() ? p.x_display.trim() : '@' + id;
      html += `<div style="margin-top:4px;">X: <a href="https://x.com/${encodeURIComponent(id)}" target="_blank" rel="noopener">${escapeHtml(label)}</a></div>`;
    }
    if (profileView) profileView.innerHTML = html;

    if (selfIntroEl) selfIntroEl.value = p.self_intro || '';
    if (xIdEl) xIdEl.value = p.x_id || '';
    if (xDispEl) xDispEl.value = p.x_display || '';

    if (profileEdit) {
      profileEdit.style.display = (loginState.logged_in && p.editable) ? 'block' : 'none';
    }
  } else {
    if (profileView) profileView.innerHTML = '<span style="color:var(--muted);">プロフィールはトリップ検索時のみ表示されます。</span>';
    if (profileEdit) profileEdit.style.display = 'none';
  }

  // Wiki
  const w = data.wiki || { body: '', updated_at: null, editable: false };
  currentWikiContent = w.body || ''; // ★再代入 (constだとエラーになる)
const wikiViewBody = qs('wikiViewBody') || qs('wikiView');

if (wikiViewBody) {
  wikiViewBody.innerHTML = currentWikiContent
    ? `<div style="white-space:pre-wrap;">${escapeHtml(currentWikiContent)}</div>`
    : '<span style="color:var(--muted);">まだWikiは登録されていません。</span>';
}

  if (wikiBody) wikiBody.value = currentWikiContent;
  
  // 編集モードリセット
  setWikiEditing(false);

const toggleArea = qs('wikiEditToggleArea');
if (toggleArea) {
  toggleArea.style.display = loginState.logged_in ? 'block' : 'none';
}



  // コメント
if (commentList) {
  const comments = data.comments || [];
    if (!comments.length) {
      commentList.innerHTML = '<span style="color:var(--muted);">コメントはまだありません。</span>';
    } else {
      const parts = comments.map(c => {
        const d = new Date(c.created_at * 1000);
        const ym = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
        const hh = String(d.getHours()).padStart(2, '0');
        const mm = String(d.getMinutes()).padStart(2, '0');
        const who = c.author_name || '(名無し)';
        const trip = c.author_trip ? ` ${c.author_trip}` : '';
        return `
          <div style="border-bottom:1px solid var(--line);padding:4px 0;">
            <div style="font-size:.75rem;color:var(--muted);">${ym} ${hh}:${mm} / ${escapeHtml(who)}${escapeHtml(trip)}
            ${escapeHtml(c.body)}</div>
          </div>
        `;
      });
      commentList.innerHTML = parts.join('');
    }
  }
  if (commentForm) commentForm.style.display = loginState.logged_in ? 'block' : 'none';
}

// --- カレンダー描画 ---
function renderCalendar(parent, calendar, viewYear = null) {
  const days = calendar.days || {};
  if (!calendar.start || !calendar.end) return;

  const allStart = new Date(calendar.start + 'T00:00:00+09:00');
  const allEnd = new Date(calendar.end + 'T00:00:00+09:00');
  let startDate = new Date(allStart);
  let endDate = new Date(allEnd);

  if (viewYear !== null) {
    startDate = new Date(`${viewYear}-01-01T00:00:00+09:00`);
    endDate = new Date(`${viewYear}-12-31T00:00:00+09:00`);
    if (startDate < allStart) startDate = new Date(allStart);
    if (endDate > allEnd) endDate = new Date(allEnd);
  }

  const card = ce('div'); card.className = 'calendar-card';
  const title = ce('div'); title.className = 'calendar-title';
  title.innerHTML = `<span>プレイ日カレンダー</span><small>${startDate.getFullYear()}年</small>`;
  card.appendChild(title);

  const grid = ce('div'); grid.className = 'calendar-grid';

  for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
    const y = d.getFullYear();
    const m = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');
    const key = `${y}-${m}-${day}`;
    const info = days[key];
    const cell = ce('div'); cell.className = 'calendar-day';

    if (info && info.games > 0) {
      cell.classList.add('has-games');
      const g = info.games;
      let level = 1;
      if (g >= 6) level = 4;
      else if (g >= 4) level = 3;
      else if (g >= 2) level = 2;
      cell.classList.add(`calendar-level-${level}`);
      cell.title = `${key}\n${g}戦 (${info.wins ?? 0}勝)`;
    } else {
      cell.title = `${key}\n対戦なし`;
    }
    grid.appendChild(cell);
  }
  card.appendChild(grid);
  parent.appendChild(card);
}

function initPlayCalendar(parent, calendar) {
  if (!calendar || !calendar.start || !calendar.end) return;
  parent.innerHTML = '';
  const wrapper = ce('div'); wrapper.className = 'calendar-wrapper';
  const firstYear = parseInt(calendar.start.slice(0, 4), 10);
  const lastYear = parseInt(calendar.end.slice(0, 4), 10);
  let currentYear = lastYear;

  const nav = ce('div'); nav.className = 'calendar-year-nav';
  nav.innerHTML = `
    <button type="button" class="calendar-year-btn" id="calPrev">前の年</button>
    <span class="calendar-year-label">${currentYear}年</span>
    <button type="button" class="calendar-year-btn" id="calNext">次の年</button>
  `;
  const container = ce('div'); container.className = 'calendar-container';

  wrapper.appendChild(nav);
  wrapper.appendChild(container);
  parent.appendChild(wrapper);

  const label = nav.querySelector('.calendar-year-label');
  const update = () => {
    label.textContent = currentYear + '年';
    container.innerHTML = '';
    renderCalendar(container, calendar, currentYear);
  };

  nav.querySelector('#calPrev').addEventListener('click', () => {
    if (currentYear > firstYear) { currentYear--; update(); }
  });
  nav.querySelector('#calNext').addEventListener('click', () => {
    if (currentYear < lastYear) { currentYear++; update(); }
  });
  update();
}

// =========================================================
// 4. データ保存・ログイン系
// =========================================================
async function doLogin() {
  const name = qs('loginNameInput')?.value.trim() ?? '';
  const tripKey = qs('loginTripKeyInput')?.value.trim() ?? '';
  if (!name) { alert('名前を入力してください。'); qs('loginNameInput')?.focus(); return; }
  if (!tripKey) { alert('トリップキーを入力してください。'); return; }

  try {
    const res = await fetch('api.php?mode=login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name, trip_key: tripKey }),
      credentials: 'include',
    });
    const data = await res.json();
    if (!res.ok || !data.ok) { alert(data.error || 'ログイン失敗'); return; }

    loginState.logged_in = true;
    loginState.trip = data.trip;
    loginState.name = data.name || name || '';
    saveLoginIdentity(name, tripKey);
    updateLoginUI();
    loadProfileForLoginTab();
    if (currentPersonalKeyType && currentPersonalKeyValue) loadPersonal();
  } catch (e) { alert('通信エラー'); }
}

function restoreLoginIdentity() {
  try {
    const saved = JSON.parse(localStorage.getItem(LOGIN_IDENTITY_STORAGE_KEY) || 'null');
    if (!saved || typeof saved !== 'object') return;
    if (qs('loginNameInput')) qs('loginNameInput').value = String(saved.name || '');
    if (qs('loginTripKeyInput')) qs('loginTripKeyInput').value = String(saved.tripKey || '');
    if (qs('loginRememberInput')) qs('loginRememberInput').checked = true;
  } catch {
    localStorage.removeItem(LOGIN_IDENTITY_STORAGE_KEY);
  }
}

function saveLoginIdentity(name, tripKey) {
  if (qs('loginRememberInput')?.checked) {
    localStorage.setItem(LOGIN_IDENTITY_STORAGE_KEY, JSON.stringify({ name, tripKey }));
  } else {
    localStorage.removeItem(LOGIN_IDENTITY_STORAGE_KEY);
  }
}

async function doLogout() {
  try {
    await fetch('api.php?mode=logout', { credentials: 'include' });
    loginState = { logged_in: false, trip: null, name: null };
    updateLoginUI();
    if (currentPersonalKeyType && currentPersonalKeyValue) loadPersonal();
  } catch (e) { }
}

function updateLoginUI() {
  const statusEl = qs('loginStatus');
  const logoutArea = qs('logoutArea');
  const loginForm = qs('loginForm');
  const personalInfo = qs('personalLoginInfo');

  if (statusEl) {
    if (loginState.logged_in) {
      statusEl.textContent = `ログイン中：${loginState.name || '(名前未設定)'} ／ ${loginState.trip}`;
    } else {
      statusEl.textContent = '（未ログイン）';
    }
  }
  if (logoutArea) logoutArea.style.display = loginState.logged_in ? 'block' : 'none';
  if (loginForm) loginForm.style.opacity = loginState.logged_in ? 0.5 : 1;

  if (personalInfo) {
    if (loginState.logged_in) {
      personalInfo.style.display = 'none';
    } else {
      personalInfo.style.display = 'block';
      personalInfo.innerHTML = `<button class="btn btn-primary" onclick="switchTab('tab-login')">ログインページへ</button>`;
    }
  }
  if (lastPersonalData) renderProfileWikiComments(lastPersonalData);
}

async function refreshLoginUI() {
  let j = null;
  try {
    const r = await fetch('api.php?mode=me', { credentials: 'include' });
    j = await r.json();
  } catch (e) {}

  const loggedIn = !!(j && (j.ok === true) && j.logged_in);
  const name = loggedIn ? (j.name || '(名無し)') : '(未ログイン)';
  const trip = loggedIn ? (j.trip || '') : '';

  // 1) 「ログイン状態を確認中…」の表示場所を総当りで潰す
  const infoEl = qsAny([
    'personalLoginInfo',
    'personal-login-info',
    'loginInfo',
    'login-info',
    'loginStateInfo'
  ]);
  if (infoEl) {
    infoEl.textContent = loggedIn ? `ログイン中：${name} / ${trip}` : '未ログイン（Wiki/コメントの編集はできません）';
  }

  // 2) コメント投稿者名
  const authorEl = qsAny([
    'comment-author-name',
    'commentAuthorName',
    'commentAuthor',
    'comment-author',
    'commentAuthorLabel'
  ]);
  if (authorEl) authorEl.textContent = name;
}




async function fetchMe() {
  try {
    const res = await fetch('api.php?mode=me', { credentials: 'include' });
    if (!res.ok) return;
    const data = await res.json();
    if (data.ok) {
      loginState.logged_in = !!data.logged_in;
      loginState.trip = data.trip || null;
      loginState.name = data.name || null;
      updateLoginUI();
    }
  } catch (e) { }
}

async function loadProfileForLoginTab() {
  if (!loginState.logged_in) return;
  try {
    const res = await fetch('api.php?mode=profile_get', { credentials: 'include' });
    if (!res.ok) return;
    const data = await res.json();
    if (data.ok && data.profile) {
      if (qs('profileSelfIntro')) qs('profileSelfIntro').value = data.profile.self_intro || '';
      if (qs('profileXId')) qs('profileXId').value = data.profile.x_id || '';
      if (qs('profileXDisplay')) qs('profileXDisplay').value = data.profile.x_display || '';
    }
  } catch (e) { }
}

async function saveProfile() {
  if (!loginState.logged_in) return;
  const selfIntro = qs('profileSelfIntro')?.value ?? '';
  const xId = qs('profileXId')?.value ?? '';
  const xDisp = qs('profileXDisplay')?.value ?? '';
  try {
    const res = await fetch('api.php?mode=profile_update', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ self_intro: selfIntro, x_id: xId, x_display: xDisp })
    });
    const data = await res.json();
    if (!data.ok) { alert(data.error); return; }
    alert('保存しました');
    if (currentPersonalKeyType && currentPersonalKeyValue) loadPersonal();
  } catch (e) { alert('通信エラー'); }
}


async function sendComment(e) {
  if (e?.preventDefault) e.preventDefault();

  if (!loginState.logged_in) {
    alert('ログインしてください。');
    return;
  }

 const wikiDs = qs('wikiBox')?.dataset || {};
 const keyType =
   (wikiDs.keyType && wikiDs.keyType.trim()) 
   || (qs('key_type')?.value || '').trim()
   || (currentPersonalKeyType || '').trim()
   || null;

 const keyValue =
   (wikiDs.keyValue && wikiDs.keyValue.trim())
   || (qs('key_value')?.value || '').trim()
   || (currentPersonalKeyValue || '').trim()
   || null;


  if (!keyType || !keyValue) {
    alert('先に個人検索をしてください。');
    return;
  }

  const bodyEl = qsAny(['commentBody', 'comment-body']);
  const bodyText = (bodyEl?.value ?? '').trim();
  if (!bodyText) { alert('コメント本文が空です。'); return; }

  try {
    const res = await fetch('api.php?mode=comment_add', {
      method: 'POST',
      credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        key_type: keyType,
        key_value: keyValue,
        body: bodyText,
      }),
    });

    const text = await res.text();

    let data = {};
    try { data = JSON.parse(text); } catch (_) {}

    if (!res.ok || !data.ok) {
if (!res.ok) {
  alert(`HTTP ${res.status}\n${text.slice(0, 500)}`);
  return;
}
if (!data.ok) {
  alert((data && data.error) ? data.error : `API ok=false\n${text.slice(0, 500)}`);
  return;
}

      //alert((data && data.error) ? data.error : `コメント投稿に失敗しました (HTTP ${res.status})\n${text.slice(0, 300)}`);
      return;
    }

    if (bodyEl) bodyEl.value = '';
    loadPersonal();
  } catch (err) {
    console.error(err);
    alert('通信エラー');
  }
}



// =========================================================
// 5. ランキング・その他タブ
// =========================================================

// --- 月間ランキング ---
function updateMonthlyScopeUI() {
  const scope = qs('monthlyScope')?.value || 'month';
  const mBox = qs('monthlyMonthBox');
  const yBox = qs('monthlyYearBox');
  if (mBox && yBox) {
    mBox.style.display = (scope === 'month') ? 'flex' : 'none';
    yBox.style.display = (scope === 'year') ? 'flex' : 'none';
  }
}

function updateMonthlyKeyTypeUI() {
  const kind = qs('monthlyKind')?.value || '';
  const box = qs('monthlyKeyTypeBox');
  const keySel = qs('monthlyKeyType');
  if (box && keySel) {
    // 検索種別は必須項目として常時表示（トリップ別が使えない種別は名前別に戻す）
    box.style.display = 'flex';
    if ((kind === 'talk' || kind === 'days') && keySel.value === 'trip') keySel.value = 'name';
  }
  // 配役選択は「配役別プレイヤー」のときだけ表示
  const hyBox = qs('monthlyHaiyakuBox');
  if (hyBox) hyBox.style.display = (kind === 'haiyaku_player') ? 'flex' : 'none';
  // レート・投票精度は全期間固定なので期間UIを隠す
  const isRating = (kind === 'rating' || kind === 'vote_acc');
  const scopeSel = qs('monthlyScope');
  if (scopeSel && scopeSel.parentElement) scopeSel.parentElement.style.display = isRating ? 'none' : 'flex';
  if (isRating) {
    const mBox = qs('monthlyMonthBox');
    const yBox = qs('monthlyYearBox');
    if (mBox) mBox.style.display = 'none';
    if (yBox) yBox.style.display = 'none';
  } else {
    updateMonthlyScopeUI();
  }
}

function setMonthlyLoading(isLoading) {
  const l = qs('monthlyLoading');
  if (l) l.style.display = isLoading ? 'block' : 'none';
}

function renderMonthlyPager(total, limit) {
  const el = qs('monthlyPager');
  if (!el) return;
  const start = monthlyOffset + 1;
  const end = Math.min(monthlyOffset + limit, total);
  const more = (monthlyOffset + limit < total);
  el.innerHTML = total ? `${start}〜${end} / ${total}件` + (more ? ` <button class="btn btn-ghost" id="monthlyMoreBtn">もっと見る</button>` : '') : '';
  if (more) {
    qs('monthlyMoreBtn').addEventListener('click', () => { monthlyOffset += limit; loadMonthly(); });
  }
}

async function loadMonthly(opts = {}) {
  const kind = qs('monthlyKind')?.value;
  if (!kind) return;
  setMonthlyLoading(true);
  const tbody = qs('monthlyTable')?.querySelector('tbody');
  if (monthlyOffset === 0) tbody.innerHTML = '';
  const wrap = qs('monthlyTableWrap');
  if (wrap) wrap.style.display = 'block';

  // パラメータ構築
  const scope = qs('monthlyScope')?.value || 'month';
  let ymParam = 'all';
  if (scope === 'month') ymParam = qs('monthlyYm')?.value;
  if (scope === 'year') ymParam = qs('monthlyYear')?.value;
  const keyType = qs('monthlyKeyType')?.value || 'name';

  let mode = '';
  // (mode決定ロジックは簡略化して記述)
  if (kind === 'games') mode = (keyType === 'trip') ? 'monthly_trip_rank' : 'monthly_name_rank';
  else if (kind === 'win_rate') mode = (keyType === 'trip') ? 'monthly_trip_win_rank' : 'monthly_name_win_rank';
  else if (kind === 'talk') mode = 'monthly_name_talk_rank';
  else if (kind === 'days') mode = 'monthly_name_days_rank';
  else if (kind === 'role_win') mode = 'monthly_role_win_rank';
  else if (kind === 'haiyaku_win') mode = 'monthly_haiyaku_win_rank';
  else if (kind === 'village') mode = 'monthly_village_rank';
  else if (kind === 'haiyaku') mode = 'monthly_haiyaku_rank';
  else if (kind === 'rm') mode = 'monthly_rm_rank';
  else if (kind === 'tsuiho') mode = 'monthly_tsuiho_rank';
  else if (kind === 'career') mode = 'monthly_career_rank';
  else if (kind === 'haiyaku_player') mode = 'haiyaku_player_rank';
  else if (kind === 'rating') mode = 'rating_rank';
  else if (kind === 'vote_acc') mode = 'vote_acc_rank';

  const minCount = Math.max(0, parseInt(qs('monthlyMinCount')?.value || '0', 10) || 0);
  let url = `api.php?mode=${mode}&limit=100&offset=${monthlyOffset}`;
  if (kind === 'vote_acc') {
    if (minCount > 0) url += `&min_count=${minCount}`;
  } else if (kind !== 'rating') {
    url += `&ym=${encodeURIComponent(ymParam)}&key_type=${keyType}`;
    if (minCount > 0) url += `&min_count=${minCount}`;
    if (kind === 'haiyaku_player') {
      const hy = qs('monthlyHaiyaku')?.value || '';
      if (!hy) { setMonthlyLoading(false); return; }
      url += `&haiyaku=${encodeURIComponent(hy)}`;
    }
    // トリップなし除外（名前別のプレイヤー集計のみ意味を持つ）
    if (qs('monthlyNoTrip')?.value === '1' && keyType === 'name' &&
        ['games', 'win_rate', 'talk', 'career', 'haiyaku_player'].includes(kind)) {
      url += '&exclude_no_trip=1';
    }
  } else if (minCount > 0) {
    url += `&min_games=${minCount}`;
  }

  // ヘッダー更新
  const labelTh = qs('monthlyLabelTh');
  const winsTh = qs('monthlyWinsTh');
  const rateTh = qs('monthlyRateTh');
  const valueTh = qs('monthlyValueTh');

  winsTh.textContent = (kind === 'rating' || kind === 'career') ? '戦数' : ((kind === 'vote_acc') ? '票数' : '勝ち');
  if (kind === 'rating' || kind === 'vote_acc') {
    labelTh.textContent = '名前 / トリップ';
    winsTh.style.display = '';
    rateTh.style.display = 'none';
  } else if (kind === 'career') {
    labelTh.textContent = ((keyType === 'trip') ? 'トリップ' : '名前') + '（初登場〜最終）';
    winsTh.style.display = '';
    rateTh.style.display = 'none';
  } else if (['games', 'win_rate', 'talk', 'days', 'haiyaku_player'].includes(kind)) {
    labelTh.textContent = (keyType === 'trip') ? 'トリップ' : '名前';
    const showWin = (kind === 'games' || kind === 'win_rate' || kind === 'haiyaku_player');
    winsTh.style.display = showWin ? '' : 'none';
    rateTh.style.display = showWin ? '' : 'none';
  } else {
    labelTh.textContent = '項目';
    winsTh.style.display = (kind.includes('win') && kind !== 'win_rate') ? '' : 'none';
    rateTh.style.display = (kind.includes('win') && kind !== 'win_rate') ? '' : 'none';
  }
  valueTh.textContent = (kind === 'talk') ? '発言密度' : ((kind === 'days') ? '日数' : ((kind === 'rating') ? 'レート' : ((kind === 'vote_acc') ? '対狼投票率' : ((kind === 'career') ? '日数' : '件数'))));

  try {
    const res = await fetch(url, { credentials: 'include' });
    const data = await res.json();
    if (data.ok) {
      // 値（ラベル）が空白の行は表示しない
      const noTripExcluded = qs('monthlyNoTrip')?.value === '1';
      const rowsData = (data.rows || []).filter(r => {
        // トリップ未設定はデータ上 trip='なし' で入っているため、除外指定時は落とす
        if (noTripExcluded && r.trip === 'なし') return false;
        if (kind === 'rating') return !!(r.trip || r.main_name);
        const lb = r.name || r.trip || r.village_name || r.village || r.haiyaku || r.role || r.rm_name || '';
        return String(lb).trim() !== '';
      });
      rowsData.forEach((r, i) => {
        const tr = ce('tr');
        const rank = monthlyOffset + i + 1;
        // 行生成ロジック
        let label = r.name || r.trip || r.village_name || r.village || r.haiyaku || r.role || r.rm_name || '';
        if (keyType === 'trip' && r.trip) label = '◆' + r.trip.replace(/^◆/, '');

        // リンクボタン化するかどうか
        let labelHtml = escapeHtml(label);
        if (kind === 'rating' || kind === 'vote_acc') {
          const trip = '◆' + (r.trip || '').replace(/^◆/, '');
          labelHtml = `<button class="name-trip-link-button" data-label="${escapeHtml(r.main_name || '')}">${escapeHtml(r.main_name || '')}</button> <button class="trip-link-button" data-trip="${escapeHtml(r.trip)}">${escapeHtml(trip)}</button>`;
        } else if (kind === 'career') {
          let btn;
          if (keyType === 'trip' && r.trip) {
            const trip = '◆' + r.trip.replace(/^◆/, '');
            btn = `<button class="trip-link-button" data-trip="${escapeHtml(r.trip)}">${escapeHtml(trip)}</button>`;
          } else {
            btn = `<button class="name-trip-link-button" data-label="${escapeHtml(r.name || '')}">${escapeHtml(r.name || '')}</button>`;
          }
          labelHtml = `${btn} <span style="font-size:.75em;color:var(--muted);">${escapeHtml(r.first_day || '')}〜${escapeHtml(r.last_day || '')}</span>`;
        } else if (['games', 'win_rate', 'talk', 'days', 'haiyaku_player'].includes(kind)) {
          const cls = (keyType === 'trip') ? 'trip-link-button' : 'name-trip-link-button';
          const dataAttr = (keyType === 'trip') ? `data-trip="${escapeHtml(r.trip)}"` : `data-label="${escapeHtml(label)}"`;
          labelHtml = `<button class="${cls}" ${dataAttr}>${escapeHtml(label)}</button>`;
        }

        const val = (kind === 'rating')
          ? Math.round(r.rating ?? 0)
          : ((kind === 'vote_acc')
            ? (r.accuracy ?? 0) + '%'
            : (r.span_days ?? r.times ?? r.unique_days ?? r.density ?? r.players ?? r.games ?? r.count ?? 0));
        const win = (kind === 'rating' || kind === 'career') ? (r.games ?? 0) : ((kind === 'vote_acc') ? (r.votes ?? 0) : (r.wins ?? 0));
        const rate = r.win_rate ?? 0;

        let html = `<td class="num">${rank}</td><td>${labelHtml}</td><td class="num">${val}</td>`;
        if (winsTh.style.display !== 'none') {
          html += `<td class="num">${win}</td>`;
          if (rateTh.style.display !== 'none') html += `<td class="num">${rate}%</td>`;
        }
        tr.innerHTML = html;
        tbody.appendChild(tr);
      });
      renderMonthlyPager(data.total, data.limit);
      
      if (monthlyOffset === 0 && !opts.skipHistory) {
        lastSearchState = {
          tab: 'tab-monthly', type: 'monthly',
          params: { scope, kind, keyType, ym: ymParam, minCount },
          scrollY: window.scrollY
        };
        saveMonthlyPrefs({
          scope, kind, keyType, minCount,
          ym: qs('monthlyYm')?.value || '',
          year: qs('monthlyYear')?.value || '',
          haiyaku: qs('monthlyHaiyaku')?.value || '',
          noTrip: qs('monthlyNoTrip')?.value || '0'
        });
      }
    }
  } catch (e) { console.error(e); }
  finally { setMonthlyLoading(false); }
}

function buildFallbackMonths() {
  const months = [];
  const now = new Date();
  for (let y = 2016; y <= now.getFullYear(); y++) {
    for (let m = 1; m <= 12; m++) {
      if (y === now.getFullYear() && m > now.getMonth() + 1) break;
      months.unshift(`${y}-${String(m).padStart(2, '0')}`);
    }
  }
  return months;
}

function fillMonthYearSelects(monthSelect, yearSelect, months) {
  if (monthSelect) monthSelect.innerHTML = '';
  if (yearSelect) yearSelect.innerHTML = '';
  const years = [...new Set(months.map(ym => ym.slice(0, 4)))];
  months.forEach(ym => {
    if (!monthSelect) return;
    const optM = ce('option');
    optM.value = ym;
    optM.textContent = ym;
    monthSelect.appendChild(optM);
  });
  years.forEach(y => {
    if (!yearSelect) return;
    const optY = ce('option');
    optY.value = y;
    optY.textContent = y + '年';
    yearSelect.appendChild(optY);
  });
  if (monthSelect) monthSelect.selectedIndex = 0;
  if (yearSelect) yearSelect.selectedIndex = 0;
}

async function loadMonths() {
  let months = [];
  try {
    const data = await apiGet({ mode: 'months' });
    months = Array.isArray(data.months) ? data.months : [];
  } catch (e) {
    console.error(e);
  }
  if (!months.length) months = buildFallbackMonths();
  fillMonthYearSelects(qs('monthlyYm'), qs('monthlyYear'), months);
  fillMonthYearSelects(qs('classStatsYm'), qs('classStatsYear'), months);
  // Cookieに保存された前回の対象月/年を復元
  ['monthlyYm', 'monthlyYear'].forEach(id => {
    const el = qs(id);
    if (el && el.dataset.pref && [...el.options].some(o => o.value === el.dataset.pref)) el.value = el.dataset.pref;
  });
}

// 配役別プレイヤー用の配役一覧（件数降順）を読み込んでピル化する
async function loadHaiyakuOptions() {
  const sel = qs('monthlyHaiyaku');
  if (!sel) return;
  try {
    const res = await fetch('api.php?mode=monthly_haiyaku_rank&ym=all&limit=100', { credentials: 'include' });
    const data = await res.json();
    if (!data.ok) throw new Error(data.error || 'API error');
    sel.innerHTML = '';
    (data.rows || []).forEach(r => {
      const hy = (r.haiyaku || '').trim();
      if (!hy) return;
      const opt = ce('option');
      opt.value = hy;
      opt.textContent = hy;
      sel.appendChild(opt);
    });
    setupPillGroup('monthlyHaiyaku');
    // Cookieに保存された前回の配役を復元
    if (sel.dataset.pref && [...sel.options].some(o => o.value === sel.dataset.pref)) {
      sel.value = sel.dataset.pref;
      refreshPillGroups();
    }
  } catch (e) {
    console.error(e);
  }
}

function resetMonthlyControls() {
  monthlyOffset = 0;
  qs('monthlyTable').querySelector('tbody').innerHTML = '';
  qs('monthlyPager').textContent = '';
  const wrap = qs('monthlyTableWrap');
  if (wrap) wrap.style.display = 'none';
  updateMonthlyScopeUI();
  updateMonthlyKeyTypeUI();
  updateClassStatsScopeUI();
}

function updateClassStatsScopeUI() {
  const scope = qs('classStatsScope')?.value || 'all';
  const mBox = qs('classStatsMonthBox');
  const yBox = qs('classStatsYearBox');
  if (mBox) mBox.style.display = (scope === 'month') ? 'block' : 'none';
  if (yBox) yBox.style.display = (scope === 'year') ? 'block' : 'none';
}

async function loadClassStats() {
  const box = qs('classStatsBox');
  if (!box) return;
  const scope = qs('classStatsScope')?.value || 'all';
  let ym = 'all';
  if (scope === 'month') ym = qs('classStatsYm')?.value || 'all';
  if (scope === 'year') ym = qs('classStatsYear')?.value || 'all';
  box.textContent = '読み込み中…';
  try {
    const data = await apiGet({ mode: 'stats_by_class', ym });
    const rows = [
      ['全体', data.all],
      ['通常', data.normal],
      ['カオス', data.chaos],
    ];
    let html = '<div class="table-wrap"><table><thead><tr><th>分類</th><th class="num">ゲーム数</th><th class="num">勝ち</th><th class="num">勝率</th></tr></thead><tbody>';
    rows.forEach(([label, r]) => {
      const games = r?.games ?? 0;
      const wins = r?.wins ?? 0;
      const rate = r?.win_rate ?? 0;
      html += `<tr><td>${label}</td><td class="num">${games}</td><td class="num">${wins}</td><td class="num">${rate}%</td></tr>`;
    });
    html += '</tbody></table></div>';
    box.innerHTML = html;
  } catch (e) {
    console.error(e);
    box.textContent = '統計の取得に失敗しました。';
  }
}




// --- 戦績カード画像（PNG保存） ---
async function savePlayerCard() {
  const d = window._lastPersonalData;
  if (!d || !d.summary) { alert('先に個人戦績を検索してください'); return; }
  const s = d.summary;
  const name = d.query || '';
  const isTrip = d.key_type === 'trip';
  const rating = (d.rating && d.rating.games > 0) ? Math.round(d.rating.rating) : null;
  const roles = Object.entries(d.role_stats || {}).map(([k, v]) => ({role: k, g: (v && v.games) || 0, w: (v && v.wins) || 0}))
    .sort((a, b) => b.g - a.g).slice(0, 3);
  const esc = t => String(t).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  const fy = d.first_ym || '';
  const W = 800, H = 420;
  const rolesSvg = roles.map((r, i) =>
    `<text x="60" y="${272 + i * 34}" font-size="19" fill="#b9c2e8">${esc(r.role)}</text>` +
    `<text x="240" y="${272 + i * 34}" font-size="19" fill="#eef1fb">${r.g}戦 ${r.g ? Math.round(r.w / r.g * 100) : 0}%勝</text>`).join('');
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}">
    <defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
      <stop offset="0" stop-color="#141a33"/><stop offset="1" stop-color="#25152e"/></linearGradient></defs>
    <rect width="${W}" height="${H}" rx="18" fill="url(#bg)"/>
    <rect x="6" y="6" width="${W-12}" height="${H-12}" rx="14" fill="none" stroke="#ff6680" stroke-opacity=".55" stroke-width="2"/>
    <text x="60" y="86" font-size="44" font-weight="bold" fill="#ffffff" font-family="sans-serif">${esc(name)}</text>
    <text x="60" y="120" font-size="17" fill="#8f9bc4" font-family="sans-serif">${isTrip ? 'トリップ戦績' : '名前戦績'}${fy ? ' / 初登場 ' + esc(fy) : ''}</text>
    <text x="60" y="190" font-size="30" fill="#ff8aa0" font-weight="bold" font-family="sans-serif">${s.total_games}戦 勝率${s.win_rate}%${rating ? '　レート ' + rating : ''}</text>
    <text x="60" y="240" font-size="15" fill="#8f9bc4" font-family="sans-serif">よく引く役職</text>
    ${rolesSvg.replace(/font-size/g, 'font-family="sans-serif" font-size')}
    <text x="${W-60}" y="${H-40}" font-size="15" text-anchor="end" fill="#8f9bc4" font-family="sans-serif">サプリの小道具＊' — mitdok.dokasen.com/zinro</text>
  </svg>`;
  const img = new Image();
  const blob = new Blob([svg], {type: 'image/svg+xml;charset=utf-8'});
  const url = URL.createObjectURL(blob);
  await new Promise((res, rej) => { img.onload = res; img.onerror = rej; img.src = url; });
  const cv = document.createElement('canvas');
  cv.width = W * 2; cv.height = H * 2;
  const ctx = cv.getContext('2d');
  ctx.scale(2, 2); ctx.drawImage(img, 0, 0);
  URL.revokeObjectURL(url);
  cv.toBlob(b => {
    const a = document.createElement('a');
    a.href = URL.createObjectURL(b);
    a.download = 'zinro-card.png';
    a.click();
    setTimeout(() => URL.revokeObjectURL(a.href), 5000);
  }, 'image/png');
}
document.addEventListener('click', e => {
  if (e.target && e.target.id === 'personalCardBtn') savePlayerCard();
});

// --- AI解析（個人戦績内） ---
let aiTrialDataPromise = null;
let aiTrialScoreStats = null;

function fetchAiTrialData() {
  if (!aiTrialDataPromise) {
    aiTrialDataPromise = fetch('ai-trial.json', { cache: 'no-store' }).then(response => {
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json();
    }).catch(error => {
      aiTrialDataPromise = null;
      throw error;
    });
  }
  return aiTrialDataPromise;
}

function comparableTrip(value) {
  return String(value || '').trim().replace(/^◆/, '');
}

let aiPerspectiveSerial = 0;
const AI_PERSPECTIVE_LABELS = [
  ['overall', '総合・勝利貢献'],
  ['influence', '場への影響'],
  ['reasoning', '推理・情報整理'],
  ['collaboration', '説得・協働'],
  ['decisions', '局面判断']
];

function aiPerspectiveTabs(perspectives, fallbacks = {}) {
  const source = {...fallbacks, ...(perspectives || {})};
  const group = `ai-perspective-${++aiPerspectiveSerial}`;
  const buttons = AI_PERSPECTIVE_LABELS.map(([key, label], index) =>
    `<button type="button" class="ai-perspective-tab${index === 0 ? ' is-active' : ''}" data-ai-perspective="${key}" aria-controls="${group}-${key}" aria-selected="${index === 0 ? 'true' : 'false'}">${label}</button>`
  ).join('');
  const panels = AI_PERSPECTIVE_LABELS.map(([key, label], index) =>
    `<section id="${group}-${key}" class="ai-perspective-panel" data-ai-perspective-panel="${key}"${index === 0 ? '' : ' hidden'}><h4>${label}</h4><p>${escapeHtml(source[key] || 'この視点の解析は次回更新時に追加されます。')}</p></section>`
  ).join('');
  return `<div class="ai-perspectives"><nav class="ai-perspective-tabs" aria-label="解析視点">${buttons}</nav>${panels}</div>`;
}

function aiTrialHistory(row) {
  const history = Array.isArray(row.history) ? row.history.slice().reverse() : [];
  if (!history.length) return '';
  const items = history.map(old => {
    const analyzed = old.analyzed_at ? new Date(old.analyzed_at).toLocaleString('ja-JP') : '初回解析';
    const score = Number.isFinite(Number(old.influence_score)) ? `${Number(old.influence_score)} / 1000` : '採点なし';
    const logs = (old.log_ids || []).map(id => `<a href="https://zinro.net/m/log.php?id=${Number(id)}" target="_blank" rel="noopener">${Number(id)}</a>`).join(' / ');
    const phase = [
      old.personality_analysis ? `<h5>人となり</h5><p>${escapeHtml(old.personality_analysis)}</p>` : '',
      old.postgame_analysis ? `<h5>感想戦</h5><p>${escapeHtml(old.postgame_analysis)}</p>` : '',
      old.rm_analysis ? `<h5>RM評価</h5><p>${escapeHtml(old.rm_analysis)}</p>` : ''
    ].join('');
    return `<div class="ai-trial-history-item"><b>${escapeHtml(analyzed)}</b><span>会話影響力 ${score}</span><h5>ゲーム中チャット</h5><p>${escapeHtml(old.analysis || '')}</p>${old.influence_reason ? `<p class="ai-trial-history-reason">${escapeHtml(old.influence_reason)}</p>` : ''}${phase}<small>確認ログ：${logs || '記録なし'}</small></div>`;
  }).join('');
  return `<details class="ai-trial-history"><summary>過去のAI解析（${history.length}件）</summary>${items}</details>`;
}

function aiPhaseAnalysis(row) {
  if (!row.personality_analysis && !row.postgame_analysis) return '';
  const games = Number(row.phase_games_sampled || 0);
  const pregameLines = Number(row.phase_pregame_lines || 0);
  const reviewLines = Number(row.phase_review_lines || 0);
  const rmGames = Number(row.phase_rm_games || 0);
  const rmLines = Number(row.phase_rm_lines || 0);
  const rmEvaluation = row.rm_analysis ? `<h4>RM評価 <small>村管理時の開始前チャット</small></h4>
    <p>${escapeHtml(row.rm_analysis)}</p>` : '';
  return `<section class="ai-phase-analysis" aria-label="ゲーム前後の評価">
    <h4>人となり <small>ゲーム開始前のチャット</small></h4>
    <p>${escapeHtml(row.personality_analysis || '判断材料がありません。')}</p>
    <h4>感想戦 <small>ゲーム終了後のチャット</small></h4>
    <p>${escapeHtml(row.postgame_analysis || '判断材料がありません。')}</p>
    ${rmEvaluation}
    <div class="ai-phase-notes">異なるゲームID ${games}件を参照（開始前の本人発言 ${pregameLines}件・終了後 ${reviewLines}件${rmGames ? `・うちRM ${rmGames}件／RM時発言 ${rmLines}件` : ''}）。待ち時間・立て直し・RMの管理発言を考慮した参考解析です。</div>
  </section>`;
}

async function renderPersonalAiTrial(keyType, keyValue) {
  const wrap = qs('personalAiEval');
  const body = qs('personalAiEvalBody');
  if (!wrap || !body) return;
  wrap.style.display = 'none';
  body.innerHTML = '';
  if (keyType !== 'trip') return;
  try {
    const data = await fetchAiTrialData();
    const target = comparableTrip(keyValue);
    const row = (data.results || []).find(item => comparableTrip(item.trip) === target);
    if (!row) return;
    const raw = row.objective?.raw || {};
    const rawLabels = [['発言数/戦',''],['盤面語率%','%'],['質問率%','%'],['被質問応答率%','%'],['他者名言及率%','%'],['根拠語率%','%']];
    const metrics = rawLabels.map(([key, unit]) => `<div><span>${escapeHtml(key)}</span><b>${Number(raw[key] || 0)}${unit}</b></div>`).join('');
    const logs = (row.log_ids || []).map(id => `<a href="https://zinro.net/m/log.php?id=${Number(id)}" target="_blank" rel="noopener">${Number(id)}</a>`).join(' / ');
    aiTrialScoreStats = aiTrialScoreStats || calculateAiScoreStats(data.results || []);
    const influenceValue = Number(row.influence_score);
    const deviationValue = calculateAiDeviation(influenceValue, aiTrialScoreStats);
    const influence = Number.isFinite(influenceValue) ? `<div class="ai-influence-score"><b>${influenceValue}</b><span>/ 1000　会話影響力</span><span class="ai-deviation-badge">偏差値 ${deviationValue.toFixed(1)}</span><p>${escapeHtml(row.influence_reason || '')}</p></div>` : '';
    body.innerHTML = `<div class="personal-ai-trial">${influence}
      <div class="ai-trial-visual">${aiTrialRadar(row.objective?.radar || {})}<div class="ai-trial-axes">${metrics}</div></div>
      ${aiPerspectiveTabs(row.perspectives, {
        overall: row.analysis,
        influence: row.influence_reason || row.analysis,
        reasoning: row.analysis,
        collaboration: row.personality_analysis || row.analysis,
        decisions: row.postgame_analysis || row.analysis
      })}
      ${aiPhaseAnalysis(row)}
      <div class="ai-trial-notes">ゲーム中チャットのみをQwen 3で参考解析。レーダーは機械集計値です。確認標本 ${Number(row.games_sampled || 0)}戦・本人発言${Number(row.target_lines || 0)}件。<br>${logs}</div>
      ${aiTrialHistory(row)}
    </div>`;
    wrap.style.display = 'block';
  } catch (error) {
    console.error('AI解析の読み込みに失敗しました。', error);
  }
}

// --- 変名の系譜図（タイムラインSVG） ---
function renderChainFig(tc) {
  const wrap = qs('personalChainFig');
  const body = qs('personalChainFigBody');
  if (!wrap || !body) return;
  const namesAll = (tc && Array.isArray(tc.names)) ? tc.names.filter(n => n.first_ts && n.last_ts) : [];
  if (namesAll.length === 0) { wrap.style.display = 'none'; body.innerHTML = ''; return; }

  // 図が縦に伸びすぎないよう、戦数上位40名までに絞って表示する
  const MAX_FIG_NAMES = 40;
  const names = [...namesAll].sort((a, b) => b.games_total - a.games_total).slice(0, MAX_FIG_NAMES);
  const omitted = namesAll.length - names.length;

  const sorted = [...names].sort((a, b) => a.first_ts - b.first_ts);
  const t0 = Math.min(...sorted.map(n => n.first_ts));
  const t1 = Math.max(...sorted.map(n => n.last_ts));
  const span = Math.max(t1 - t0, 86400);

  const labelW = 150, chartW = 640, rowH = 22, padTop = 20, padRight = 60;
  const W = labelW + chartW + padRight;
  const H = padTop + sorted.length * rowH + 8;
  const x = ts => labelW + (ts - t0) / span * chartW;
  const escSvg = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');

  let g = '';
  // 年目盛り
  const y0 = new Date(t0 * 1000).getFullYear(), y1 = new Date(t1 * 1000).getFullYear();
  for (let y = y0; y <= y1 + 1; y++) {
    const ts = Date.UTC(y, 0, 1) / 1000 - 9 * 3600;
    if (ts < t0 || ts > t1) continue;
    g += `<line x1="${x(ts)}" y1="${padTop - 6}" x2="${x(ts)}" y2="${H - 6}" stroke="rgba(255,255,255,.12)"/>`;
    g += `<text x="${x(ts) + 2}" y="${padTop - 8}" font-size="9" fill="rgba(255,255,255,.45)">${y}</text>`;
  }
  sorted.forEach((n, i) => {
    const y = padTop + i * rowH;
    const bx = x(n.first_ts), bw = Math.max(x(n.last_ts) - bx, 3);
    g += `<text x="${labelW - 6}" y="${y + 13}" font-size="11" text-anchor="end" fill="var(--fg,#eee)">${escSvg(n.name)}</text>`;
    g += `<rect x="${bx}" y="${y + 3}" width="${bw}" height="${rowH - 9}" rx="4" fill="rgba(79,116,217,.75)" stroke="rgba(255,255,255,.25)"><title>${escSvg(n.name)}: ${n.games_total}戦</title></rect>`;
    g += `<text x="${bx + bw + 4}" y="${y + 13}" font-size="9" fill="rgba(255,255,255,.55)">${n.games_total}戦</text>`;
  });
  body.innerHTML = `<svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" style="max-width:100%;height:auto;min-width:520px;">${g}</svg>` +
    `<div style="font-size:.72rem;color:var(--muted);margin-top:2px;">同一トリップ系譜で使われた名前（${tc.name_count}種 / 計${tc.games_total}戦 / 期間 ${tc.span_days}日）。棒は各名前の初出〜最終使用。${omitted > 0 ? `戦数上位${names.length}名のみ表示（他${omitted}名省略）。` : ''}</div>`;
  wrap.style.display = 'block';
}

// --- 変名図タブ（トリップ⇄名前の連鎖系譜） ---
let chainsOffset = 0;
let chainsLoadedOnce = false;

function _fmtDay(ts) {
  if (!ts) return '-';
  const d = new Date(ts * 1000);
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}

async function loadChainsList(opts = {}) {
  const tbody = qs('chainsTable')?.querySelector('tbody');
  if (!tbody) return;
  if (!opts.append) { chainsOffset = 0; tbody.innerHTML = ''; }
  const l = qs('chainsLoading');
  if (l) l.style.display = 'block';
  try {
    const res = await fetch(`api.php?mode=trip_lineage_index&limit=50&offset=${chainsOffset}`, { credentials: 'include' });
    const data = await res.json();
    if (!data.ok) throw new Error(data.error || 'API error');
    (data.rows || []).forEach((r, i) => {
      const tr = ce('tr');
      const rank = chainsOffset + i + 1;
      const disp = '◆' + String(r.trip || '').replace(/^◆/, '');
      tr.innerHTML = `
        <td class="num">${rank}</td>
        <td>${escapeHtml(r.main_name || '')}</td>
        <td><button class="trip-link-button" data-trip="${escapeHtml(r.trip)}" style="font-size:.75rem;">${escapeHtml(disp)}</button></td>
        <td class="num">${r.games}</td>
        <td style="font-size:.75rem;">${_fmtDay(r.last_ts)}</td>
        <td style="font-size:.75rem;">${_fmtDay(r.first_ts)}</td>
        <td><button class="btn btn-ghost lineage-btn" data-trip="${escapeHtml(r.trip)}" style="font-size:.7rem;padding:2px 8px;">系譜を見る</button></td>`;
      tr.querySelector('.lineage-btn').addEventListener('click', () => loadLineage(r.trip));
      tbody.appendChild(tr);
    });
    const got = (data.rows || []).length;
    const pager = qs('chainsPager');
    if (pager) {
      pager.innerHTML = `${chainsOffset + 1}〜${chainsOffset + got} / ${data.total}件` +
        (chainsOffset + got < data.total ? ` <button class="btn btn-ghost" id="chainsMoreBtn">もっと見る</button>` : '');
      qs('chainsMoreBtn')?.addEventListener('click', () => { chainsOffset += 50; loadChainsList({ append: true }); });
    }
  } catch (e) {
    console.error(e);
    const pager = qs('chainsPager');
    if (pager) pager.textContent = '読み込みに失敗しました。';
  } finally {
    if (l) l.style.display = 'none';
  }
}

async function searchChainsByName() {
  const nm = (qs('chainsNameQuery')?.value || '').trim();
  const box = qs('chainsNameResults');
  if (!box) return;
  if (!nm) { box.innerHTML = ''; return; }
  box.innerHTML = '<div style="font-size:.8rem;color:var(--muted);">検索中…</div>';
  try {
    const res = await fetch(`api.php?mode=trip_lineage_search&name=${encodeURIComponent(nm)}&limit=50`, { credentials: 'include' });
    const data = await res.json();
    if (!data.ok) throw new Error(data.error || 'API error');
    const rows = data.rows || [];
    if (rows.length === 0) { box.innerHTML = `<div style="font-size:.8rem;color:var(--muted);">「${escapeHtml(nm)}」を名乗ったトリップは見つかりませんでした。</div>`; return; }
    let html = `<div style="font-size:.75rem;color:var(--muted);margin-bottom:4px;">「${escapeHtml(nm)}」を名乗ったトリップ ${data.total}件（最新参戦順、最大50件表示）。系譜を選んでください。</div>`;
    html += '<div class="table-wrap"><table><thead><tr><th>トリップ</th><th>主な名前</th><th class="num">この名前での戦数</th><th class="num">総戦数</th><th>最終参戦</th><th></th></tr></thead><tbody>';
    rows.forEach(r => {
      const disp = '◆' + String(r.trip || '').replace(/^◆/, '');
      html += `<tr>
        <td><button class="trip-link-button" data-trip="${escapeHtml(r.trip)}" style="font-size:.75rem;">${escapeHtml(disp)}</button></td>
        <td>${escapeHtml(r.main_name || '')}</td>
        <td class="num">${r.name_games}</td>
        <td class="num">${r.total_games || '-'}</td>
        <td style="font-size:.75rem;">${_fmtDay(r.last_ts)}</td>
        <td><button class="btn btn-ghost lineage-btn" data-trip="${escapeHtml(r.trip)}" style="font-size:.7rem;padding:2px 8px;">系譜を見る</button></td></tr>`;
    });
    html += '</tbody></table></div>';
    box.innerHTML = html;
    box.querySelectorAll('.lineage-btn').forEach(b => b.addEventListener('click', () => loadLineage(b.dataset.trip)));
  } catch (e) {
    console.error(e);
    box.innerHTML = '<div style="color:#f88;font-size:.8rem;">検索に失敗しました。</div>';
  }
}

const LINEAGE_DEPTH_COLORS = ['#e2537a', '#4f74d9', '#3aa675', '#c78f3d', '#8a63c9', '#5a9bbf', '#888888'];

// 個別系譜ビューと一覧ビューの切り替え（個別表示中は他の人のリストを出さない）
function showLineageView(on) {
  const ids = ['chainsSearchCard', 'chainsListCard', 'chainsHelpCard'];
  ids.forEach(id => { const el = qs(id); if (el) el.style.display = on ? 'none' : ''; });
  const card = qs('lineageFigCard');
  if (card) card.style.display = on ? 'block' : 'none';
}

async function loadLineage(trip) {
  const card = qs('lineageFigCard');
  const body = qs('lineageFigBody');
  const title = qs('lineageFigTitle');
  if (!card || !body) return;
  showLineageView(true);
  body.innerHTML = '<div style="font-size:.8rem;color:var(--muted);">系譜を辿っています…</div>';
  if (title) title.textContent = `系譜図：◆${String(trip).replace(/^◆/, '')}`;

  // 固定URL化＋共有リンク提示
  const url = new URL(window.location.href);
  url.searchParams.set('tab', 'tab-chains');
  url.searchParams.set('lineage', trip);
  ['key_type', 'q'].forEach(k => url.searchParams.delete(k));
  history.replaceState(null, '', url);
  const shareInput = qs('lineageShareUrl');
  if (shareInput) shareInput.value = url.toString();

  card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  try {
    const res = await fetch(`api.php?mode=trip_lineage&trip=${encodeURIComponent(trip)}&max_nodes=80`, { credentials: 'include' });
    const data = await res.json();
    if (!data.ok) throw new Error(data.msg || data.error || 'API error');
    renderLineageFig(data, body);
  } catch (e) {
    console.error(e);
    body.innerHTML = `<div style="color:#f88;font-size:.8rem;">読み込みに失敗しました: ${escapeHtml(String(e.message || e))}</div>`;
  }
}

function renderLineageFig(data, body) {
  const nodes = [...(data.nodes || [])].sort((a, b) => (b.first_ts || 0) - (a.first_ts || 0));
  if (nodes.length === 0) { body.innerHTML = '<div style="font-size:.8rem;color:var(--muted);">系譜データがありません。</div>'; return; }
  const t0 = Math.min(...nodes.map(n => n.first_ts || Infinity));
  const t1 = Math.max(...nodes.map(n => n.last_ts || 0));
  const span = Math.max(t1 - t0, 86400);

  const labelW = 230, chartW = 620, rowH = 21, padTop = 20, padRight = 56;
  const W = labelW + chartW + padRight;
  const H = padTop + nodes.length * rowH + 8;
  const x = ts => labelW + (ts - t0) / span * chartW;
  const escSvg = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');

  let g = '';
  const y0 = new Date(t0 * 1000).getFullYear(), y1 = new Date(t1 * 1000).getFullYear();
  for (let y = y0; y <= y1 + 1; y++) {
    const ts = Date.UTC(y, 0, 1) / 1000 - 9 * 3600;
    if (ts < t0 || ts > t1) continue;
    g += `<line x1="${x(ts)}" y1="${padTop - 6}" x2="${x(ts)}" y2="${H - 6}" stroke="rgba(255,255,255,.12)"/>`;
    g += `<text x="${x(ts) + 2}" y="${padTop - 8}" font-size="9" fill="rgba(255,255,255,.45)">${y}</text>`;
  }
  nodes.forEach((n, i) => {
    const y = padTop + i * rowH;
    const bx = x(n.first_ts), bw = Math.max(x(n.last_ts) - bx, 3);
    const col = LINEAGE_DEPTH_COLORS[Math.min(n.depth, LINEAGE_DEPTH_COLORS.length - 1)];
    const disp = '◆' + String(n.trip).replace(/^◆/, '');
    const label = `${n.main_name || ''} ${disp}`;
    g += `<text x="${labelW - 6}" y="${y + 13}" font-size="10" text-anchor="end" fill="var(--fg,#eee)">${escSvg(label.length > 30 ? label.slice(0, 29) + '…' : label)}</text>`;
    g += `<rect x="${bx}" y="${y + 3}" width="${bw}" height="${rowH - 8}" rx="4" fill="${col}" fill-opacity="${n.depth === 0 ? '0.95' : '0.7'}" stroke="rgba(255,255,255,.25)"><title>${escSvg(disp)}（主な名前: ${escSvg(n.main_name || '-')}） ${n.games}戦 / ${_fmtDay(n.first_ts)}〜${_fmtDay(n.last_ts)}${n.via_name ? ' / 「' + escSvg(n.via_name) + '」経由で関連' : ' / 起点'}</title></rect>`;
    g += `<text x="${bx + bw + 4}" y="${y + 13}" font-size="9" fill="rgba(255,255,255,.55)">${n.games}戦</text>`;
  });
  const legend = LINEAGE_DEPTH_COLORS.slice(0, 4).map((c, i) =>
    `<span style="display:inline-block;width:10px;height:10px;background:${c};border-radius:2px;margin:0 3px 0 10px;vertical-align:middle;"></span>${i === 0 ? '起点' : `関連${i}段目`}`).join('');
  body.innerHTML = `<svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" style="max-width:100%;height:auto;min-width:560px;">${g}</svg>` +
    `<div style="font-size:.72rem;color:var(--muted);margin-top:4px;">${legend}</div>` +
    `<div style="font-size:.72rem;color:var(--muted);margin-top:2px;">起点トリップと名前の共有で連鎖的に関連づいたトリップ ${data.count}件（棒＝各トリップの初参戦〜最終参戦、上ほど新しく下ほど古い）。${data.truncated ? '規模が大きいため戦数上位の一部のみ表示しています。' : ''}バーにマウスを載せると経由した名前が見えます。</div>`;
}

// --- セレクトを全選択肢表示のピル選択に置き換える ---
const _pillRefreshers = [];
function setupPillGroup(selectId) {
  const sel = qs(selectId);
  if (!sel || sel.dataset.pillified) return;
  sel.dataset.pillified = '1';
  sel.style.display = 'none';
  const prev = sel.previousElementSibling;
  if (prev && prev.tagName === 'BR') prev.remove();
  const group = ce('div');
  group.className = 'pill-group';
  const refresh = () => {
    [...group.children].forEach(b => b.classList.toggle('active', b.dataset.value === sel.value));
  };
  [...sel.options].forEach(opt => {
    const b = ce('button');
    b.type = 'button';
    b.className = 'pill';
    b.dataset.value = opt.value;
    b.textContent = opt.textContent;
    b.addEventListener('click', () => {
      if (sel.value !== opt.value) {
        sel.value = opt.value;
        sel.dispatchEvent(new Event('change'));
      }
      refresh();
    });
    group.appendChild(b);
  });
  sel.insertAdjacentElement('afterend', group);
  sel.addEventListener('change', refresh);
  refresh();
  _pillRefreshers.push(refresh);
}
function refreshPillGroups() { _pillRefreshers.forEach(fn => fn()); }

// --- ランキング検索設定のCookie保存・復元（直前の検索条件をデフォルトにする） ---
const MONTHLY_PREF_COOKIE = 'zinro_rank_pref';
function saveMonthlyPrefs(p) {
  try {
    document.cookie = `${MONTHLY_PREF_COOKIE}=${encodeURIComponent(JSON.stringify(p))};path=/;max-age=31536000;SameSite=Lax`;
  } catch (e) { /* Cookie不可環境では保存しない */ }
}
function loadMonthlyPrefs() {
  try {
    const c = document.cookie.split('; ').find(x => x.startsWith(MONTHLY_PREF_COOKIE + '='));
    if (!c) return null;
    return JSON.parse(decodeURIComponent(c.slice(MONTHLY_PREF_COOKIE.length + 1)));
  } catch (e) { return null; }
}
function applyMonthlyPrefs() {
  const p = loadMonthlyPrefs();
  if (!p) return;
  const setVal = (id, v) => {
    const el = qs(id);
    if (el && v != null && [...el.options].some(o => o.value === String(v))) el.value = String(v);
  };
  setVal('monthlyScope', p.scope);
  setVal('monthlyKind', p.kind);
  setVal('monthlyKeyType', p.keyType);
  setVal('monthlyMinCount', p.minCount);
  setVal('monthlyNoTrip', p.noTrip);
  // 対象月/年・配役の選択肢は非同期で入るため、読込完了後にも適用できるよう控えておく
  if (p.ym && qs('monthlyYm')) qs('monthlyYm').dataset.pref = p.ym;
  if (p.year && qs('monthlyYear')) qs('monthlyYear').dataset.pref = p.year;
  if (p.haiyaku && qs('monthlyHaiyaku')) qs('monthlyHaiyaku').dataset.pref = p.haiyaku;
  updateMonthlyScopeUI();
  updateMonthlyKeyTypeUI();
  refreshPillGroups();
}

// --- RM一斉検索 ---
async function loadSharedTripsCount() {
  const box = qs('sharedTripsCountBox');
  if (!box) return;
  try {
    const data = await apiGet({ mode: 'shared_trips_count' });
    box.textContent = `共有トリップ登録数: ${data.count ?? 0}件`;
  } catch (e) {
    console.error(e);
    box.textContent = '';
  }
}

async function fetchSharedTripSet(trips) {
  if (!trips.length) return new Set();
  try {
    const p = new URLSearchParams({ mode: 'shared_trips', trips: trips.join('\n') });
    const res = await fetch('api.php?' + p.toString(), { credentials: 'include' });
    const data = await res.json();
    if (!data.ok) return new Set();
    return new Set((data.shared_trips || []).map(t => String(t || '').replace(/^◆/, '')));
  } catch (e) {
    console.error(e);
    return new Set();
  }
}

async function fetchMultiSummary(keyType, keys) {
  if (!keys.length) return {};
  try {
    const p = new URLSearchParams({
      mode: 'multi_summary',
      key_type: keyType,
      trips: keys.join('\n')
    });
    const res = await fetch('api.php?' + p.toString(), { credentials: 'include' });
    const data = await res.json();
    return data.ok ? (data.results || {}) : {};
  } catch (e) {
    console.error(e);
    return {};
  }
}

async function loadMultiTrip(opts = {}) {
  const raw = qs('multiTripInput')?.value || '';
  const container = qs('multiTripResult');
  if (!raw) return;
  container.innerHTML = '読み込み中…';
  const trips = raw.split(/[\n◆]+/).map(s => s.trim()).filter(s => s).map(s => s.replace(/^◆/, ''));
  
  lastRMState = { fromRM: true, scrollY: window.scrollY };

  try {
    const [sharedSet, summaryMap] = await Promise.all([
      fetchSharedTripSet(trips),
      fetchMultiSummary('trip', trips)
    ]);
    const res = await fetch('api.php?mode=multi_trip', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ trips: trips.join('\n') }),
      credentials: 'include'
    });
    const data = await res.json();
    if (data.ok) {
      let html = '<table class="rm-trip-table"><thead><tr><th>名前</th><th>トリップ</th><th>共有</th><th>通常</th><th>カオス</th><th>初登場</th><th>戦数</th><th>同トリ数</th><th>ログ</th></tr></thead><tbody>';
      data.results.forEach(r => {
        const plainTrip = String(r.trip || '').replace(/^◆/, '');
        const tDisp = '◆' + plainTrip;
        const shared = sharedSet.has(plainTrip);
        const summary = summaryMap[plainTrip] || {};
        const normal = summary.normal || {};
        const chaos = summary.chaos || {};
        const normalText = `${normal.games ?? 0}戦 ${normal.win_rate ?? 0}%`;
        const chaosText = `${chaos.games ?? 0}戦 ${chaos.win_rate ?? 0}%`;
        html += `<tr>
          <td>${escapeHtml(r.main_name || '')}</td>
          <td><button class="trip-link-button" data-trip="${escapeHtml(r.trip)}">${escapeHtml(tDisp)}</button></td>
          <td>${shared ? '<span style="color:#ff9f9f;font-weight:600;">登録あり</span>' : ''}</td>
          <td class="num">${escapeHtml(normalText)}</td>
          <td class="num">${escapeHtml(chaosText)}</td>
          <td>${escapeHtml(summary.first_ym || '')}</td>
          <td class="num">${r.total_games}</td>
          <td class="num">${r.unique_names}</td>
          <td>${(r.logs || []).map(id => `<a href="https://zinro.net/m/log.php?id=${id}" target="_blank">${id}</a>`).join('/')}</td>
        </tr>`;
      });
      html += '</tbody></table>';
      container.innerHTML = html;
      
      if (!opts.skipHistory) {
        lastSearchState = { tab: 'tab-rm', type: 'rm', params: { raw }, scrollY: window.scrollY };
      }
    } else { container.textContent = data.error; }
  } catch (e) { container.textContent = '通信エラー'; }
}

// --- 名前チェイン ---
function initNameChainTab() {
  // 必要ならイベント設定
}

async function loadNameChains(page) {
  if (nameChainIsLoading) return;
  nameChainIsLoading = true;
  nameChainCurrentPage = page;
  const order = qs('name-chain-order')?.value || 'last_ts_desc';
  const box = qs('name-chain-box');
  const pager = qs('name-chain-pager');
  const busy = qs('name-chain-busy');
  if (busy) busy.style.display = 'block';
  if (box) box.innerHTML = '';

  try {
    const res = await fetch(`api.php?mode=name_chain_list&page=${page}&limit=50&order=${order}`, { credentials: 'include' });
    const data = await res.json();
    if (data.ok) {
      const tbl = ce('table'); tbl.className = 'name-chain-table';
      tbl.innerHTML = `<thead><tr><th>最新名</th><th>最新トリップ</th><th>代表トリップ</th><th>初登場</th><th>最終</th><th>期間</th><th>戦数</th><th>トリ数</th></tr></thead><tbody></tbody>`;
      const tb = tbl.querySelector('tbody');
      (data.rows || []).forEach(r => {
        const tr = ce('tr');
        tr.innerHTML = `
          <td>${escapeHtml(r.latest_name)}</td>
          <td>${escapeHtml(r.latest_trip)}</td>
          <td>${escapeHtml(r.representative_trip)}</td>
          <td>${nh_formatDate(r.first_ts)}</td>
          <td>${nh_formatDate(r.last_ts)}</td>
          <td>${r.span_days}</td><td>${r.games_total}</td><td>${r.trip_count}</td>`;
        tb.appendChild(tr);
      });
      box.appendChild(tbl);
      // ページャー実装（簡易）
      if (pager) {
        const maxP = Math.ceil(data.total / 50);
        pager.innerHTML = `Page ${page} / ${maxP} 
          ${page > 1 ? `<button onclick="loadNameChains(${page-1})">前へ</button>` : ''}
          ${page < maxP ? `<button onclick="loadNameChains(${page+1})">次へ</button>` : ''}`;
      }
    }
  } catch (e) { if(box) box.textContent = 'Error'; }
  finally { nameChainIsLoading = false; if(busy) busy.style.display = 'none'; }
}

async function loadTripChainRank(page) {
  // 名前変遷の実装（必要に応じて追加）
  const tbody = qs('trip-chain-tbody');
  if(!tbody) return;
  const minGames = qs('chain-min-games')?.value || 10;
  const minSpan = qs('chain-min-span')?.value || 30;
  const both = qs('chain-both-modes')?.checked ? 1 : 0;
  
  qs('namehistoryLoading').style.display = 'block';
  tbody.innerHTML = '';
  
  try {
    const res = await fetch(`api.php?mode=trip_chain_rank&limit=50&offset=${page*50}&min_games=${minGames}&min_span_days=${minSpan}&both_modes=${both}`, { credentials: 'include' });
    const data = await res.json();
    if(data.ok) {
      data.rows.forEach((r, i) => {
        const tr = ce('tr');
        tr.innerHTML = `<td>${i+1}</td><td>${escapeHtml(r.main_name)}</td><td>◆${escapeHtml(r.plain_trip)}</td>
          <td class="num">${r.span_days}</td><td class="num">${r.games_total}</td><td class="num">${r.name_count}</td>
          <td><button class="btn-ghost" onclick="showTripChainDetail('${r.plain_trip}')">詳細</button></td>`;
        tbody.appendChild(tr);
      });
    }
  } catch(e){}
  finally { qs('namehistoryLoading').style.display = 'none'; }
}

async function showTripChainDetail(plain) {
  const box = qs('trip-chain-detail');
  box.textContent = '読み込み中...';
  try {
    const res = await fetch(`api.php?mode=trip_chain_detail&plain_trip=${encodeURIComponent(plain)}`, { credentials: 'include' });
    const data = await res.json();
    if(data.ok) {
      box.innerHTML = `<h3>◆${escapeHtml(plain)} の変遷</h3>
      <p>期間: ${nh_formatDate(data.summary.first_ts)} 〜 ${nh_formatDate(data.summary.last_ts)} (${data.summary.span_days}日)</p>`;
      const tbl = ce('table'); tbl.className = 'sub-table';
      tbl.innerHTML = `<thead><tr><th>名前</th><th>初</th><th>終</th><th>戦数</th></tr></thead><tbody></tbody>`;
      const tb = tbl.querySelector('tbody');
      data.names.forEach(n => {
        tb.innerHTML += `<tr><td>${escapeHtml(n.name)}</td><td>${nh_formatDate(n.first_ts)}</td><td>${nh_formatDate(n.last_ts)}</td><td class="num">${n.games_total}</td></tr>`;
      });
      box.appendChild(tbl);
    }
  } catch(e) { box.textContent = 'エラー'; }
}

// --- 村タブ ---
function initVillageTab(parent) {
  const wrapper = ce('div'); wrapper.className = 'village-tab-wrapper';
  wrapper.innerHTML = `<h3>最近の村</h3><div class="village-list-box"></div><div class="village-detail-box"></div>`;
  parent.appendChild(wrapper);
  loadVillageList(wrapper.querySelector('.village-list-box'), wrapper.querySelector('.village-detail-box'));
}

async function loadVillageList(listBox, detailBox) {
  listBox.textContent = '読み込み中…';
  try {
    const res = await fetch('api.php?mode=village_recent&limit=10', { credentials: 'include' });
    const data = await res.json();
    if(data.ok) {
      listBox.innerHTML = '';
      const tbl = ce('table'); tbl.className = 'village-list-table';
      tbl.innerHTML = `<thead><tr><th>村名</th><th>日時</th><th>配役</th></tr></thead><tbody></tbody>`;
      data.rows.forEach(r => {
        const tr = ce('tr');
        const dt = new Date(r.started_at*1000).toLocaleString('ja-JP');
        tr.innerHTML = `<td><button class="village-detail-button" data-id="${r.id}">${escapeHtml(r.village_name)}</button></td><td>${dt}</td><td>${escapeHtml(r.haiyaku)}</td>`;
        tr.querySelector('button').addEventListener('click', () => loadVillageDetailInto(r.id, detailBox));
        tbl.querySelector('tbody').appendChild(tr);
      });
      listBox.appendChild(tbl);
      if(data.rows.length) loadVillageDetailInto(data.rows[0].id, detailBox);
    }
  } catch(e) { listBox.textContent = 'エラー'; }
}

async function loadVillageDetailInto(id, detailBox) {
  detailBox.textContent = '読み込み中...';
  try {
    const res = await fetch(`api.php?mode=village_detail&id=${id}`, { credentials: 'include' });
    const data = await res.json();
    if(data.ok) renderVillageDetail(data, detailBox);
  } catch(e) { detailBox.textContent = 'エラー'; }
}

function renderVillageDetail(data, box) {
  const v = data.village;
  const dt = new Date(v.started_at*1000).toLocaleString('ja-JP');
  box.innerHTML = `<h3>${escapeHtml(v.village_name)}</h3>
    <div class="village-meta">開始: ${dt} / ログID: <a href="https://zinro.net/m/log.php?id=${v.log_id}" target="_blank">${v.log_id}</a></div>`;
  
  const tbl = ce('table'); tbl.className = 'village-player-table';
  tbl.innerHTML = `<thead><tr><th>名前</th><th>役職</th><th>勝敗</th></tr></thead><tbody></tbody>`;
  data.players.forEach(p => {
    const tr = ce('tr');
    const label = p.name + (p.trip ? ' ◆'+p.trip : '');
    tr.innerHTML = `<td><button class="name-trip-link-button" data-label="${escapeHtml(p.name)}">${escapeHtml(label)}</button></td>
      <td>${p.role}</td><td>${p.win ? '勝' : '負'}</td>`;
    tbl.querySelector('tbody').appendChild(tr);
  });
  box.appendChild(tbl);
}

function buildPersonalShareUrl(keyType, q) {
  const base = location.origin + location.pathname; // /zinro/
  const u = new URL(base);
  u.searchParams.set('tab', 'tab-personal');
  u.searchParams.set('key_type', keyType);
  u.searchParams.set('q', q);
  return u.toString();
}

async function copyToClipboard(text) {
  // https環境なら基本これでOK
  if (navigator.clipboard && window.isSecureContext) {
    await navigator.clipboard.writeText(text);
    return;
  }
  // フォールバック
  const ta = document.createElement('textarea');
  ta.value = text;
  ta.style.position = 'fixed';
  ta.style.left = '-9999px';
  document.body.appendChild(ta);
  ta.focus();
  ta.select();
  document.execCommand('copy');
  ta.remove();
}

function toastMini(msg) {
  const t = document.createElement('div');
  t.textContent = msg;
  Object.assign(t.style, {
    position: 'fixed',
    left: '50%',
    bottom: '18px',
    transform: 'translateX(-50%)',
    padding: '8px 12px',
    borderRadius: '999px',
    background: 'rgba(0,0,0,0.75)',
    color: '#fff',
    fontSize: '12px',
    zIndex: 999999,
  });
  document.body.appendChild(t);
  setTimeout(() => t.remove(), 1200);
}

// =========================================================
// 6. DOMContentLoaded (初期化エントリーポイント)
// =========================================================
document.addEventListener('DOMContentLoaded', async () => {
  loadStatsUpdatedAt();

  // イベントデリゲーション
  document.addEventListener('click', (e) => {
    const btn = e.target.closest('.name-trip-link-button');
    if (btn) {
      jumpToPersonalByLabel(btn.dataset.label ?? btn.textContent);
      return;
    }
      const hbtn = e.target.closest('.hist-mini');
  if (hbtn) {
    const type = hbtn.dataset.histType === 'trip' ? 'trip' : 'name';
    const q = hbtn.dataset.q ?? hbtn.textContent;
    runPersonalSearch(type, q);
    return;
  }
    const tbtn = e.target.closest('.trip-link-button');
    if (tbtn) {
      jumpToPersonalByTrip(tbtn.dataset.trip ?? tbtn.textContent);
      return;
    }
  });

  // タブボタン
  document.querySelectorAll('.tab-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.preventDefault();
      const tabId = btn.dataset.tab;
      if (!isProgrammaticTabChange) { lastSearchState = null; showBackGeneral(); }
      
      document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
      document.querySelectorAll('.tab-panel').forEach(p => p.style.display = 'none');
      btn.classList.add('active');
      const panel = qs(tabId);
      if (panel) panel.style.display = 'block';

      // タブごとの初期化
      if (tabId === 'tab-village' && !village_loadedOnce) {
        village_loadedOnce = true;
        initVillageTab(qs('tab-village'));
      }
      if (tabId === 'tab-monthly') {
        if (!monthlyOptionsLoaded) { monthlyOptionsLoaded = true; loadMonths(); loadHaiyakuOptions(); }
        resetMonthlyControls();
      }
      if (tabId === 'tab-chains' && !chainsLoadedOnce) { chainsLoadedOnce = true; loadChainsList(); }
      if (tabId === 'tab-chains' && !isProgrammaticTabChange) showLineageView(false);
      if (tabId === 'tab-personal') renderSearchHistoryButtons();
      if (tabId === 'tab-login') loadProfileForLoginTab();
      if (tabId === 'tab-rm-stats' && !window._rmStatsLoaded) { window._rmStatsLoaded = true; loadRmStatsList(); }
      if (tabId === 'tab-ai-trial') loadAiTrial();
      if (tabId === 'tab-rm') loadSharedTripsCount();
      if (tabId === 'tab-bbs') loadBbs(0);

      const url = new URL(window.location.href);
      url.searchParams.set('tab', tabId);
      history.replaceState(null, '', url);
    });
  });

  // UIイベントバインド
  if (qs('personalSearchBtn')) {
    qs('personalSearchBtn').addEventListener('click', () => {
      const type = (qs('key_type')?.value === 'trip') ? 'trip' : 'name';
      runPersonalSearch(type);
    });
  }
  if (qs('personalNameSearchBtn')) qs('personalNameSearchBtn').addEventListener('click', () => runPersonalSearch('name'));
  if (qs('personalTripSearchBtn')) qs('personalTripSearchBtn').addEventListener('click', () => runPersonalSearch('trip'));

  if (qs('monthlyLoadBtn')) qs('monthlyLoadBtn').addEventListener('click', () => { monthlyOffset = 0; loadMonthly(); });
  if (qs('classStatsLoadBtn')) qs('classStatsLoadBtn').addEventListener('click', loadClassStats);
  
  const ymSel = qs('monthlyYm');
  if (ymSel) ymSel.addEventListener('change', () => { monthlyOffset = 0; });

const kindSel = qs('monthlyKind');
if (kindSel) {
    kindSel.addEventListener('change', () => {
        monthlyOffset = 0;
        const tbody = qs('monthlyTable')?.querySelector('tbody');
        if (tbody) tbody.innerHTML = ''; // 明示的に空にする
        const pager = qs('monthlyPager');
        if (pager) pager.textContent = '';
        const wrap = qs('monthlyTableWrap');
        if (wrap) wrap.style.display = 'none';
        updateMonthlyKeyTypeUI();
    });
}


  const scopeSel = qs('monthlyScope');
  if (scopeSel) {
    scopeSel.value = 'all';
    scopeSel.addEventListener('change', () => { monthlyOffset = 0; updateMonthlyScopeUI(); });
    updateMonthlyScopeUI();
  }
  const classStatsScopeSel = qs('classStatsScope');
  if (classStatsScopeSel) {
    classStatsScopeSel.value = 'all';
    classStatsScopeSel.addEventListener('change', updateClassStatsScopeUI);
    updateClassStatsScopeUI();
  }
  if(qs('monthlyKeyType')) {
    qs('monthlyKeyType').value = 'trip';
    updateMonthlyKeyTypeUI();
  }

  // ランキングタブの選択肢はドロップダウンでなく全表示ピルで選ぶ
  setupPillGroup('monthlyScope');
  setupPillGroup('monthlyKind');
  setupPillGroup('monthlyKeyType');
  setupPillGroup('monthlyNoTrip');
  setupPillGroup('monthlyMinCount');

  // 前回の検索条件(Cookie)をデフォルトとして反映
  applyMonthlyPrefs();

  if (qs('multiTripBtn')) qs('multiTripBtn').addEventListener('click', loadMultiTrip);
  restoreLoginIdentity();
  if (qs('loginBtn')) qs('loginBtn').addEventListener('click', doLogin);
  if (qs('loginRememberInput')) qs('loginRememberInput').addEventListener('change', (e) => {
    if (!e.target.checked) localStorage.removeItem(LOGIN_IDENTITY_STORAGE_KEY);
  });
  if (qs('logoutBtn')) qs('logoutBtn').addEventListener('click', doLogout);
  if (qs('profileSaveBtn')) qs('profileSaveBtn').addEventListener('click', saveProfile);
  if (qs('wikiSaveBtn')) qs('wikiSaveBtn').addEventListener('click', saveWiki);
  restoreBbsIdentity();
  if (qs('bbsNameInput')) qs('bbsNameInput').addEventListener('input', saveBbsIdentity);
  if (qs('bbsTripKeyInput')) qs('bbsTripKeyInput').addEventListener('input', saveBbsIdentity);
  if (qs('bbsPostBtn')) qs('bbsPostBtn').addEventListener('click', postBbs);
  //if (qs('commentSendBtn')) qs('commentSendBtn').addEventListener('click', sendComment);
const cbtn = qsAny(['commentSendBtn', 'comment-send']);
if (cbtn) cbtn.addEventListener('click', sendComment);

  if (qs('chainsTripSearchBtn')) qs('chainsTripSearchBtn').addEventListener('click', () => {
    const t = (qs('chainsTripQuery')?.value || '').trim();
    if (t) loadLineage(t);
  });
  if (qs('chainsNameSearchBtn')) qs('chainsNameSearchBtn').addEventListener('click', searchChainsByName);
  if (qs('chainsNameQuery')) qs('chainsNameQuery').addEventListener('keydown', e => { if (e.key === 'Enter') searchChainsByName(); });
  if (qs('chainsTripQuery')) qs('chainsTripQuery').addEventListener('keydown', e => { const t = (e.target.value || '').trim(); if (e.key === 'Enter' && t) loadLineage(t); });
  if (qs('lineageBackBtn')) qs('lineageBackBtn').addEventListener('click', () => {
    showLineageView(false);
    const url = new URL(window.location.href);
    url.searchParams.delete('lineage');
    history.replaceState(null, '', url);
    if (!chainsLoadedOnce) { chainsLoadedOnce = true; loadChainsList(); }
  });
  if (qs('lineageShareCopyBtn')) qs('lineageShareCopyBtn').addEventListener('click', async () => {
    const v = qs('lineageShareUrl')?.value || '';
    try { await navigator.clipboard.writeText(v); qs('lineageShareCopyBtn').textContent = 'コピー済み'; setTimeout(() => { qs('lineageShareCopyBtn').textContent = 'コピー'; }, 1500); }
    catch (e) { qs('lineageShareUrl')?.select(); }
  });

  if (qs('chain-reload-btn')) qs('chain-reload-btn').addEventListener('click', () => loadTripChainRank(0));
  if (qs('name-chain-run')) qs('name-chain-run').addEventListener('click', () => loadNameChains(1));

  initNameChainTab();
  initWikiEditUI(); // ここで呼び出し（関数定義より後なので安全）
renderSearchHistoryButtons();


  await fetchMe();
  refreshLoginUI();

  // URL初期タブ
  const url = new URL(window.location.href);
  const initialTabId = url.searchParams.get('tab') || 'tab-about';
  switchTab(initialTabId);

  // ★ 共有URLから個別系譜を直接表示
  const lineageParam = (url.searchParams.get('lineage') || '').trim();
  if (lineageParam && initialTabId === 'tab-chains') {
    loadLineage(lineageParam);
  }

  // ★ 外部URLから個人戦績を直接表示（自動検索）
  const kt = (url.searchParams.get('key_type') || '').trim(); // 'name' or 'trip'
  const q  = (url.searchParams.get('q') || '').trim();

  if (q && (kt === 'name' || kt === 'trip')) {
    // 個人タブを開く（tab指定がなくてもここで個人へ誘導）
    switchTab('tab-personal');

    // 入力欄に反映
    if (kt === 'trip') {
      if (qs('personalTripQuery')) qs('personalTripQuery').value = q;
      if (qs('personalNameQuery')) qs('personalNameQuery').value = '';
    } else {
      if (qs('personalNameQuery')) qs('personalNameQuery').value = q;
      if (qs('personalTripQuery')) qs('personalTripQuery').value = '';
    }

    // hiddenにも反映してから実行
    if (qs('key_type'))  qs('key_type').value  = kt;
    if (qs('key_value')) qs('key_value').value = q;

    // 検索実行（あなたの既存ルートに乗せる）
    runPersonalSearch(kt, q);
  }
});

// ─── RM集計タブ ───
let _rmStatsOffset = 0;
let _rmStatsQuery = "";

function _rmYmd(ts) {
  if (!ts) return "";
  const d = new Date((ts + 9*3600) * 1000);
  return d.getUTCFullYear() + "/" + String(d.getUTCMonth()+1).padStart(2,"0") + "/" + String(d.getUTCDate()).padStart(2,"0");
}

async function loadRmStatsList(query, offset) {
  _rmStatsQuery = (query ?? _rmStatsQuery) || "";
  _rmStatsOffset = offset ?? 0;
  const el = document.getElementById("rm-stats-content");
  if (!el) return;
  el.innerHTML = '<div style="opacity:.5;">読み込み中...</div>';
  try {
    const params = new URLSearchParams({mode:"rm_list",limit:"50",offset:String(_rmStatsOffset)});
    if (_rmStatsQuery) params.set("q", _rmStatsQuery);
    const r = await fetch("mtdk_api.php?" + params.toString(), { credentials: "include" });
    const j = await r.json();
    if (!j.ok) { el.innerHTML = '<div style="color:#f88;">取得失敗</div>'; return; }
    const rows = j.rows || [];
    if (!rows.length) { el.innerHTML = '<div style="opacity:.5;">該当するRMがありません</div>'; return; }
    const total = j.total || 0;
    let html = '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
    html += '<tr style="border-bottom:1px solid rgba(255,255,255,.15);"><th style="text-align:left;padding:6px 8px;">#</th><th style="text-align:left;padding:6px 8px;">RM名</th><th style="text-align:right;padding:6px 8px;">ゲーム数</th><th style="text-align:right;padding:6px 8px;">初回</th><th style="text-align:right;padding:6px 8px;">最終</th></tr>';
    rows.forEach((r, i) => {
      const n = _rmStatsOffset + i + 1;
      html += '<tr class="rm-stats-row" data-rm="' + _escH(r.rm_name || "") + '" style="cursor:pointer;border-bottom:1px solid rgba(255,255,255,.05);" onmouseover="this.style.background=\'rgba(255,255,255,.06)\'" onmouseout="this.style.background=\'none\'">';
      html += '<td style="padding:5px 8px;opacity:.5;">' + n + '</td>';
      html += '<td style="padding:5px 8px;">' + _escH(r.rm_name) + '</td>';
      html += '<td style="padding:5px 8px;text-align:right;">' + r.games + '</td>';
      html += '<td style="padding:5px 8px;text-align:right;opacity:.5;font-size:11px;">' + _rmYmd(r.first_ts) + '</td>';
      html += '<td style="padding:5px 8px;text-align:right;opacity:.5;font-size:11px;">' + _rmYmd(r.last_ts) + '</td>';
      html += '</tr>';
    });
    html += '</table>';
    // ページネーション
    const canPrev = _rmStatsOffset > 0;
    const canNext = _rmStatsOffset + 50 < total;
    html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-top:12px;">';
    html += '<button id="rm-stats-prev" ' + (canPrev?"":"disabled") + ' style="padding:6px 14px;border-radius:6px;border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.06);color:#fff;font-size:12px;cursor:' + (canPrev?"pointer":"not-allowed") + ';opacity:' + (canPrev?"1":".4") + ';">◀ 前へ</button>';
    html += '<span style="font-size:12px;opacity:.6;">' + (_rmStatsOffset+1) + '〜' + Math.min(_rmStatsOffset+50, total) + ' / ' + total + '件</span>';
    html += '<button id="rm-stats-next" ' + (canNext?"":"disabled") + ' style="padding:6px 14px;border-radius:6px;border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.06);color:#fff;font-size:12px;cursor:' + (canNext?"pointer":"not-allowed") + ';opacity:' + (canNext?"1":".4") + ';">次へ ▶</button>';
    html += '</div>';
    el.innerHTML = html;
    // イベント
    el.querySelectorAll(".rm-stats-row").forEach(tr => {
      tr.addEventListener("click", () => loadRmStatsDetail(tr.dataset.rm));
    });
    el.querySelector("#rm-stats-prev")?.addEventListener("click", () => { if(canPrev) loadRmStatsList(null, _rmStatsOffset - 50); });
    el.querySelector("#rm-stats-next")?.addEventListener("click", () => { if(canNext) loadRmStatsList(null, _rmStatsOffset + 50); });
  } catch(e) { el.innerHTML = '<div style="color:#f88;">エラー: ' + e.message + '</div>'; }
}

async function loadRmStatsDetail(rmName) {
  const el = document.getElementById("rm-stats-content");
  if (!el) return;
  el.innerHTML = '<div style="opacity:.5;">読み込み中...</div>';
  let detailOffset = 0;

  async function render(offset) {
    detailOffset = offset;
    try {
      const params = new URLSearchParams({mode:"rm_detail",rm_name:rmName,limit:"20",offset:String(offset)});
      const r = await fetch("mtdk_api.php?" + params.toString(), { credentials: "include" });
      const j = await r.json();
      if (!j.ok || !j.found) { el.innerHTML = '<div style="opacity:.5;">データがありません</div>'; return; }
      let participants = null;
      try {
        const pp = new URLSearchParams({mode:"rm_participants",rm_name:rmName,limit:"30"});
        const pr = await fetch("mtdk_api.php?" + pp.toString(), { credentials: "include" });
        const pj = await pr.json();
        if (pj.ok) participants = pj.players || [];
      } catch (e) {
        console.error(e);
      }

      let html = '<div style="margin-bottom:12px;"><button id="rm-detail-back" style="padding:6px 14px;border-radius:6px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;font-size:12px;cursor:pointer;">← 一覧に戻る</button> <span style="font-size:15px;font-weight:bold;margin-left:8px;">' + _escH(j.rm_name) + '</span></div>';

      // サマリー
      html += '<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap;">';
      html += '<div style="background:rgba(255,255,255,.05);border-radius:8px;padding:10px 16px;text-align:center;"><div style="font-size:20px;font-weight:bold;">' + j.total_games + '</div><div style="font-size:10px;opacity:.5;">総ゲーム数</div></div>';
      html += '<div style="background:rgba(255,255,255,.05);border-radius:8px;padding:10px 16px;text-align:center;"><div style="font-size:13px;font-weight:bold;">' + _rmYmd(j.first_ts) + '</div><div style="font-size:10px;opacity:.5;">初回</div></div>';
      html += '<div style="background:rgba(255,255,255,.05);border-radius:8px;padding:10px 16px;text-align:center;"><div style="font-size:13px;font-weight:bold;">' + _rmYmd(j.last_ts) + '</div><div style="font-size:10px;opacity:.5;">最終</div></div>';
      html += '</div>';

      // 村名Top
      if (j.village_top?.length) {
        html += '<div style="font-size:12px;font-weight:bold;color:rgba(255,255,255,.65);margin-bottom:6px;">🏘 よく建てる村名</div>';
        html += '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:16px;">';
        j.village_top.forEach(v => {
          html += '<span style="padding:3px 10px;border-radius:12px;background:rgba(255,255,255,.07);font-size:11px;">' + _escH(v.village_name) + ' <span style="opacity:.5;">' + v.games + '</span></span>';
        });
        html += '</div>';
      }

      // 配役Top
      if (j.haiyaku_top?.length) {
        html += '<div style="font-size:12px;font-weight:bold;color:rgba(255,255,255,.65);margin-bottom:6px;">🎭 よく使う配役</div>';
        html += '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:16px;">';
        j.haiyaku_top.forEach(h => {
          html += '<span style="padding:3px 10px;border-radius:12px;background:rgba(255,255,255,.07);font-size:11px;">' + _escH(h.haiyaku || "不明") + ' <span style="opacity:.5;">' + h.games + '</span></span>';
        });
        html += '</div>';
      }

      if (participants?.length) {
        html += '<div style="font-size:12px;font-weight:bold;color:rgba(255,255,255,.65);margin-bottom:6px;">参加者ランキング</div>';
        html += '<table style="width:100%;border-collapse:collapse;font-size:12px;margin-bottom:16px;">';
        participants.slice(0, 30).forEach((p, i) => {
          const trip = p.trip ? '◆' + String(p.trip).replace(/^◆/, '') : '';
          html += '<tr style="border-bottom:1px solid rgba(255,255,255,.05);">';
          html += '<td style="padding:4px 6px;opacity:.4;">' + (i + 1) + '</td>';
          html += '<td style="padding:4px 6px;"><button class="name-trip-link-button" data-label="' + _escH(p.name || "") + '">' + _escH(p.name || "") + '</button></td>';
          html += '<td style="padding:4px 6px;">' + (trip ? '<button class="trip-link-button" data-trip="' + _escH(p.trip) + '">' + _escH(trip) + '</button>' : '') + '</td>';
          html += '<td style="padding:4px 6px;text-align:right;">' + (p.count || 0) + '</td>';
          html += '<td style="padding:4px 6px;font-size:10px;">' + (p.log_ids || []).map(id => '<a href="https://zinro.net/m/log.php?id=' + id + '" target="_blank" rel="noopener">' + id + '</a>').join('/') + '</td>';
          html += '</tr>';
        });
        html += '</table>';
      }

      // ゲーム一覧
      html += '<div style="font-size:12px;font-weight:bold;color:rgba(255,255,255,.65);margin-bottom:6px;">ゲーム一覧</div>';
      html += '<table style="width:100%;border-collapse:collapse;font-size:12px;">';
      j.games.forEach((g,i) => {
        const n = offset + i + 1;
        const href = "https://zinro.net/m/log.php?id=" + g.log_id;
        html += '<tr style="border-bottom:1px solid rgba(255,255,255,.05);">';
        html += '<td style="padding:4px 6px;opacity:.4;">' + n + '</td>';
        html += '<td style="padding:4px 6px;"><a href="' + href + '" target="_blank" rel="noopener" style="color:#8af;text-decoration:none;">' + _escH(g.village_name) + '</a></td>';
        html += '<td style="padding:4px 6px;opacity:.5;font-size:10px;white-space:nowrap;">' + _escH(g.haiyaku || "") + '</td>';
        html += '<td style="padding:4px 6px;text-align:right;opacity:.5;font-size:10px;white-space:nowrap;">' + _rmYmd(g.started_at) + '</td>';
        html += '</tr>';
      });
      html += '</table>';

      // ページネーション
      const canPrev = offset > 0;
      const canNext = offset + 20 < j.total_games;
      html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px;">';
      html += '<button id="rm-detail-prev" ' + (canPrev?"":"disabled") + ' style="padding:5px 12px;border-radius:6px;border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.06);color:#fff;font-size:11px;cursor:' + (canPrev?"pointer":"not-allowed") + ';opacity:' + (canPrev?"1":".4") + ';">◀ 前へ</button>';
      html += '<span style="font-size:11px;opacity:.5;">' + (offset+1) + '〜' + Math.min(offset+20,j.total_games) + ' / ' + j.total_games + '</span>';
      html += '<button id="rm-detail-next" ' + (canNext?"":"disabled") + ' style="padding:5px 12px;border-radius:6px;border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.06);color:#fff;font-size:11px;cursor:' + (canNext?"pointer":"not-allowed") + ';opacity:' + (canNext?"1":".4") + ';">次へ ▶</button>';
      html += '</div>';

      el.innerHTML = html;
      el.querySelector("#rm-detail-back")?.addEventListener("click", () => loadRmStatsList());
      el.querySelector("#rm-detail-prev")?.addEventListener("click", () => { if(canPrev) render(offset-20); });
      el.querySelector("#rm-detail-next")?.addEventListener("click", () => { if(canNext) render(offset+20); });

      // URL更新
      const url = new URL(window.location.href);
      url.searchParams.set("tab", "tab-rm-stats");
      url.searchParams.set("rm", rmName);
      history.replaceState(null, "", url);
    } catch(e) { el.innerHTML = '<div style="color:#f88;">エラー: ' + e.message + '</div>'; }
  }
  render(0);
}

function _escH(s) { return String(s??"").replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c])); }

function formatBbsDate(ts) {
  if (!ts) return '';
  return new Date(ts * 1000).toLocaleString('ja-JP');
}

async function loadStatsUpdatedAt() {
  const personalEl = qs('personalStatsUpdatedAt');
  const rankingEl = qs('rankingStatsUpdatedAt');
  if (!personalEl && !rankingEl) return;
  try {
    const data = await apiGet({ mode: 'stats_updated_at' });
    const statsTimestamp = Number(data.stats_updated_at || 0);
    const checkedTimestamp = Number(data.stats_checked_at || 0);
    const rankingTimestamp = Number(data.ranking_built_at || 0);
    const checkedText = checkedTimestamp > 0
      ? new Date(checkedTimestamp * 1000).toLocaleString('ja-JP')
      : '取得できません';
    if (personalEl) {
      const updatedText = statsTimestamp > 0
        ? new Date(statsTimestamp * 1000).toLocaleString('ja-JP')
        : '取得できません';
      personalEl.textContent = `戦績データ更新：${updatedText} ／ 確認：${checkedText}`;
    }
    if (rankingEl) {
      const rankingText = rankingTimestamp > 0
        ? new Date(rankingTimestamp * 1000).toLocaleString('ja-JP')
        : '取得できません';
      rankingEl.textContent = `ランキング集計：${rankingText} ／ 戦績確認：${checkedText}`;
    }
  } catch (error) {
    console.error(error);
    if (personalEl) personalEl.textContent = '戦績データ更新：取得できません ／ 確認：取得できません';
    if (rankingEl) rankingEl.textContent = 'ランキング集計：取得できません ／ 戦績確認：取得できません';
  }
}

const BBS_IDENTITY_STORAGE_KEYS = {
  name: 'mtdk_bbs_name',
  tripKey: 'mtdk_bbs_trip_key'
};

function restoreBbsIdentity() {
  try {
    const nameEl = qs('bbsNameInput');
    const tripEl = qs('bbsTripKeyInput');
    if (nameEl) nameEl.value = localStorage.getItem(BBS_IDENTITY_STORAGE_KEYS.name) || '';
    if (tripEl) tripEl.value = localStorage.getItem(BBS_IDENTITY_STORAGE_KEYS.tripKey) || '';
  } catch (e) {
    console.warn('掲示板の入力情報を復元できませんでした。', e);
  }
}

function saveBbsIdentity() {
  try {
    localStorage.setItem(BBS_IDENTITY_STORAGE_KEYS.name, qs('bbsNameInput')?.value || '');
    localStorage.setItem(BBS_IDENTITY_STORAGE_KEYS.tripKey, qs('bbsTripKeyInput')?.value || '');
  } catch (e) {
    console.warn('掲示板の入力情報を保存できませんでした。', e);
  }
}

async function loadBbs(offset = 0) {
  bbsOffset = Math.max(0, offset);
  const list = qs('bbsList');
  const pager = qs('bbsPager');
  if (!list) return;
  list.textContent = '読み込み中…';
  if (pager) pager.textContent = '';
  try {
    const data = await apiGet({ mode: 'bbs', limit: 20, offset: bbsOffset });
    const posts = data.posts || [];
    if (!posts.length) {
      list.textContent = '投稿はまだありません。';
    } else {
      list.innerHTML = posts.map(p => {
        const trip = p.trip ? ' ' + p.trip : '';
        const name = p.name || '名無しさん';
        const nameUrl = buildPersonalShareUrl('name', name);
        const tripUrl = p.trip ? buildPersonalShareUrl('trip', p.trip) : '';
        return `<div style="border-bottom:1px solid var(--border);padding:8px 0;">
          <div>
            <a href="${escapeHtml(nameUrl)}" style="font-weight:600;color:var(--fg);">${escapeHtml(name)}</a>
            ${tripUrl ? `<a href="${escapeHtml(tripUrl)}" style="color:var(--muted);font-size:.75rem;">${escapeHtml(trip)}</a>` : ''}
            <span style="color:var(--muted);font-size:.75rem;margin-left:8px;">${escapeHtml(formatBbsDate(p.created_at))}</span>
          </div>
          <div style="white-space:pre-wrap;color:var(--fg);margin-top:4px;">${escapeHtml(p.body || '')}</div>
        </div>`;
      }).join('');
    }
    if (pager) {
      const total = data.total || 0;
      const canPrev = bbsOffset > 0;
      const canNext = !!data.has_more;
      pager.innerHTML = total
        ? `${bbsOffset + 1}〜${Math.min(bbsOffset + 20, total)} / ${total}件 `
          + `<button id="bbsPrevBtn" class="btn btn-ghost" ${canPrev ? '' : 'disabled'}>前へ</button> `
          + `<button id="bbsNextBtn" class="btn btn-ghost" ${canNext ? '' : 'disabled'}>次へ</button>`
        : '';
      qs('bbsPrevBtn')?.addEventListener('click', () => { if (canPrev) loadBbs(bbsOffset - 20); });
      qs('bbsNextBtn')?.addEventListener('click', () => { if (canNext) loadBbs(bbsOffset + 20); });
    }
  } catch (e) {
    console.error(e);
    list.textContent = '掲示板の読み込みに失敗しました。';
  }
}

async function postBbs() {
  const nameEl = qs('bbsNameInput');
  const tripEl = qs('bbsTripKeyInput');
  const bodyEl = qs('bbsBodyInput');
  const msg = qs('bbsPostMsg');
  const btn = qs('bbsPostBtn');
  const name = (nameEl?.value || '').trim();
  const trip_key = (tripEl?.value || '').trim();
  const body = (bodyEl?.value || '').trim();
  saveBbsIdentity();
  if (!name || !body) {
    if (msg) msg.textContent = '名前と本文を入力してください。';
    return;
  }
  if (btn) btn.disabled = true;
  if (msg) msg.textContent = '投稿中…';
  try {
    const res = await fetch('api.php?mode=bbs', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ name, trip_key, body })
    });
    const data = await res.json();
    if (!data.ok) {
      if (msg) msg.textContent = data.error || '投稿に失敗しました。';
      return;
    }
    if (bodyEl) bodyEl.value = '';
    if (msg) msg.textContent = data.trip ? `投稿しました ${data.trip}` : '投稿しました。';
    loadBbs(0);
  } catch (e) {
    console.error(e);
    if (msg) msg.textContent = '通信エラー';
  } finally {
    if (btn) btn.disabled = false;
  }
}

let aiTrialLoaded = false;
let aiTrialData = null;
let aiTrialPage = 0;
const AI_TRIAL_PAGE_SIZE = 30;
let aiGameLoaded = false;
let aiGameData = null;
let aiGameSelectedKey = '';

function selectAiSubtab(name) {
  const game = name === 'game';
  const playerPanel = qs('aiSubPlayer');
  const gamePanel = qs('aiSubGame');
  if (playerPanel) playerPanel.style.display = game ? 'none' : '';
  if (gamePanel) gamePanel.style.display = game ? '' : 'none';
  document.querySelectorAll('[data-ai-subtab]').forEach(button => {
    const active = button.dataset.aiSubtab === name;
    button.classList.toggle('is-active', active);
    button.setAttribute('aria-selected', active ? 'true' : 'false');
  });
  if (game) loadAiGameAnalysis();
}

async function loadAiGameAnalysis() {
  const list = qs('aiGameList');
  const meta = qs('aiGameMeta');
  if (!list) return;
  try {
    if (meta) meta.textContent = '最新の解析結果を確認中…';
    const response = await fetch(`ai-game-analysis.json?t=${Date.now()}`, { cache: 'no-store' });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const data = await response.json();
    aiGameLoaded = true;
    aiGameData = data;
    const created = data.created_at ? new Date(data.created_at).toLocaleString('ja-JP') : '不明';
    if (meta) meta.textContent = `最終反映: ${created} / モデル: ${data.model || 'Qwen'} / ${data.results?.length || 0}試合`;
    const search = qs('aiGameSearch');
    if (search) search.oninput = renderAiGameAnalysis;
    list.onclick = event => {
      const button = event.target.closest('[data-ai-game-key]');
      if (!button) return;
      aiGameSelectedKey = button.dataset.aiGameKey || '';
      renderAiGameAnalysis();
    };
    renderAiGameAnalysis();
  } catch (error) {
    console.error(error);
    list.innerHTML = '<div class="card">ゲーム解析を読み込めませんでした。</div>';
  }
}

function aiGamePlayerLink(participant, fallbackName = '') {
  const name = participant?.name || fallbackName;
  const trip = String(participant?.trip || '').trim();
  const nameLink = name ? `<a href="${escapeHtml(buildPersonalShareUrl('name', name))}">${escapeHtml(name)}</a>` : '名前不明';
  const tripLink = trip && trip !== 'なし' ? ` <small><a href="${escapeHtml(buildPersonalShareUrl('trip', trip))}">${escapeHtml(trip)}</a></small>` : '';
  return `${nameLink}${tripLink}`;
}

function renderAiGameAnalysis() {
  const list = qs('aiGameList');
  if (!list || !aiGameData) return;
  const query = (qs('aiGameSearch')?.value || '').trim().toLowerCase();
  const rows = (aiGameData.results || []).filter(game => {
    const players = (game.participants || []).map(player => `${player.name || ''} ${player.trip || ''}`).join(' ');
    const notable = (game.notable_players || []).map(player => `${player.name || ''} ${player.comment || ''}`).join(' ');
    const perspectiveText = Object.values(game.perspectives || {}).join(' ');
    return !query || `${game.log_id} ${game.village_name || ''} ${game.haiyaku || ''} ${game.title || ''} ${game.mvp_name || ''} ${game.mvp_reason || ''} ${game.analysis || ''} ${perspectiveText} ${players} ${notable}`.toLowerCase().includes(query);
  });
  const count = qs('aiGameFilteredCount');
  if (count) count.textContent = `${rows.length}試合`;
  if (!rows.length) {
    aiGameSelectedKey = '';
    list.innerHTML = '<div class="card">検索条件に一致するゲーム解析はありません。</div>';
    return;
  }
  const gameKey = game => `${Number(game.log_id)}:${Number(game.game_index || 0)}`;
  if (!rows.some(game => gameKey(game) === aiGameSelectedKey)) aiGameSelectedKey = gameKey(rows[0]);
  const selected = rows.find(game => gameKey(game) === aiGameSelectedKey) || rows[0];
  const indexHtml = rows.map(game => {
    const played = game.started_at ? new Date(Number(game.started_at) * 1000).toLocaleString('ja-JP') : '日時不明';
    const key = gameKey(game);
    return `<button type="button" class="ai-game-index-item${key === aiGameSelectedKey ? ' is-active' : ''}" data-ai-game-key="${escapeHtml(key)}" aria-pressed="${key === aiGameSelectedKey ? 'true' : 'false'}">
      <span class="ai-game-index-date">${escapeHtml(played)}</span>
      <strong>${escapeHtml(game.village_name || '村名不明')}</strong>
      <span>RM ${escapeHtml(game.rm_name || '不明')}</span>
      <span>${escapeHtml(game.haiyaku || '配役不明')}</span>
      <small>ゲームID ${Number(game.log_id)}</small>
    </button>`;
  }).join('');
  const detailHtml = [selected].map(game => {
      const played = game.started_at ? new Date(Number(game.started_at) * 1000).toLocaleString('ja-JP') : '日時不明';
      const participants = Array.isArray(game.participants) ? game.participants : [];
      const participantByName = new Map(participants.map(player => [player.name, player]));
      const evaluationByName = new Map((game.player_evaluations || []).map(item => [item.name, item]));
      const notable = (game.notable_players || []).map(player => `<li><b>${aiGamePlayerLink(participantByName.get(player.name), player.name)}</b><span>${escapeHtml(player.comment || '')}</span></li>`).join('');
      const participantList = participants.map(player => {
        const evaluation = evaluationByName.get(player.name) || {};
        const score = Math.max(0, Math.min(100, Number(evaluation.score ?? 50)));
        const mvpBadge = player.name === game.mvp_name ? '<span class="ai-game-player-mvp">MVP</span>' : '';
        const role = player.role ? `<span class="ai-game-role">${escapeHtml(player.role)}</span>` : '<span class="ai-game-role is-unknown">役職不明</span>';
        return `<li><div class="ai-game-player-head">${aiGamePlayerLink(player)}${role}${mvpBadge}<b>${score}点</b></div><p>${escapeHtml(evaluation.comment || 'この試合での評価材料が限られています。')}</p></li>`;
      }).join('');
      const mvp = participantByName.get(game.mvp_name);
      return `<article class="card ai-game-card">
        <div class="ai-game-meta-row"><span>${played}</span><span>${escapeHtml(game.village_name || '村名不明')}</span><span>RM ${escapeHtml(game.rm_name || '不明')}</span><span>${escapeHtml(game.haiyaku || '配役不明')}</span><span>ゲームID ${Number(game.log_id)}</span></div>
        <div class="ai-game-body">
          ${participantList ? `<aside class="ai-game-participants"><h4>参加プレイヤー（${Number(game.player_count || participants.length)}人）</h4><ul>${participantList}</ul></aside>` : ''}
          <div class="ai-game-report">
            <div class="ai-game-card-head"><h3>${escapeHtml(game.title || game.village_name || 'ゲーム解析')}</h3><a class="btn btn-ghost" href="${escapeHtml(game.source_url || `https://zinro.net/m/log.php?id=${Number(game.log_id)}`)}" target="_blank" rel="noopener">ゲームログを見る</a></div>
            <section class="ai-game-mvp"><span>MVP</span><h4>${aiGamePlayerLink(mvp, game.mvp_name || '選出なし')}</h4><p>${escapeHtml(game.mvp_reason || '')}</p></section>
            ${aiPerspectiveTabs(game.perspectives, {
              overall: game.analysis,
              influence: game.interaction_analysis || game.analysis,
              reasoning: game.strategic_structure || game.analysis,
              collaboration: game.interaction_analysis || game.analysis,
              decisions: [game.turning_point_analysis, game.counterfactual].filter(Boolean).join('\n\n') || game.analysis
            })}
            ${notable ? `<section class="ai-game-notable"><h4>注目プレイヤー</h4><ul>${notable}</ul></section>` : ''}
          </div>
        </div>
      </article>`;
    }).join('');
  list.innerHTML = `<div class="ai-game-browser"><nav class="ai-game-index" aria-label="ゲーム解析一覧">${indexHtml}</nav><div class="ai-game-detail">${detailHtml}</div></div>`;
}

function calculateAiScoreStats(rows) {
  const scores = (Array.isArray(rows) ? rows : [])
    .map(row => Number(row.influence_score))
    .filter(Number.isFinite);
  if (!scores.length) return { count: 0, mean: 0, standardDeviation: 0 };
  const mean = scores.reduce((sum, score) => sum + score, 0) / scores.length;
  const variance = scores.reduce((sum, score) => sum + (score - mean) ** 2, 0) / scores.length;
  return { count: scores.length, mean, standardDeviation: Math.sqrt(variance) };
}

function calculateAiDeviation(score, stats) {
  if (!Number.isFinite(score)) return NaN;
  if (!stats || !Number.isFinite(stats.standardDeviation) || stats.standardDeviation === 0) return 50;
  return 50 + 10 * (score - stats.mean) / stats.standardDeviation;
}

function aiTrialRadar(axes) {
  const labels = ['発言', '盤面', '質問', '回答', '他者', '根拠'];
  const keys = ['発言数/戦', '盤面語率', '質問率', '被質問応答率', '他者名言及率', '根拠語率'];
  const center = 90;
  const radius = 61;
  const point = (index, scale) => {
    const angle = -Math.PI / 2 + index * Math.PI / 3;
    return `${(center + Math.cos(angle) * radius * scale).toFixed(1)},${(center + Math.sin(angle) * radius * scale).toFixed(1)}`;
  };
  const rings = [0.25, 0.5, 0.75, 1].map(scale =>
    `<polygon points="${keys.map((_, index) => point(index, scale)).join(' ')}" fill="none" stroke="rgba(148,163,184,.2)"/>`
  ).join('');
  const values = keys.map((key, index) => point(index, Math.max(0, Math.min(1000, Number(axes[key]) || 0)) / 1000)).join(' ');
  const labelText = labels.map((label, index) => {
    const angle = -Math.PI / 2 + index * Math.PI / 3;
    const x = center + Math.cos(angle) * 78;
    const y = center + Math.sin(angle) * 78 + 4;
    return `<text x="${x.toFixed(1)}" y="${y.toFixed(1)}" text-anchor="middle">${label}</text>`;
  }).join('');
  return `<svg class="ai-trial-radar" viewBox="0 0 180 180" role="img" aria-label="6軸評価グラフ">${rings}<polygon points="${values}" fill="rgba(56,189,248,.3)" stroke="#38bdf8" stroke-width="2"/>${labelText}</svg>`;
}

function renderAiScoreDistribution(rows) {
  const root = qs('aiScoreDistribution');
  const summary = qs('aiScoreDistributionSummary');
  if (!root) return;
  const scores = (Array.isArray(rows) ? rows : []).map(row => Number(row.influence_score))
    .filter(Number.isFinite).map(score => Math.max(0, Math.min(1000, score)));
  if (!scores.length) {
    root.innerHTML = '<p class="ai-score-distribution-empty">表示できるスコアがありません。</p>';
    if (summary) summary.textContent = '0人';
    return;
  }
  const stats = aiTrialScoreStats || calculateAiScoreStats(rows);
  aiTrialScoreStats = stats;
  const deviations = scores.map(score => calculateAiDeviation(score, stats));
  const scoreBinStep = 50, deviationBinStep = 5;
  const scoreBins = Array.from({ length: 21 }, () => 0);
  const deviationBins = Array.from({ length: 21 }, () => 0);
  scores.forEach(score => { scoreBins[Math.min(20, Math.round(score / scoreBinStep))] += 1; });
  deviations.forEach(value => { deviationBins[Math.min(20, Math.round(Math.max(0, Math.min(100, value)) / deviationBinStep))] += 1; });
  const width = 1000, height = 318;
  const margin = { top: 58, right: 24, bottom: 50, left: 72 };
  const plotWidth = width - margin.left - margin.right, plotHeight = height - margin.top - margin.bottom;
  const maxCount = Math.max(...scoreBins, ...deviationBins);
  const roundedMax = Math.max(1, Math.ceil(maxCount / 100) * 100);
  const x = (index, length) => margin.left + (index / (length - 1)) * plotWidth;
  const y = count => margin.top + plotHeight - (count / roundedMax) * plotHeight;
  const scorePoints = scoreBins.map((count, index) => `${x(index, scoreBins.length).toFixed(1)},${y(count).toFixed(1)}`).join(' ');
  const deviationPoints = deviationBins.map((count, index) => `${x(index, deviationBins.length).toFixed(1)},${y(count).toFixed(1)}`).join(' ');
  const areaPoints = `${margin.left},${margin.top + plotHeight} ${scorePoints} ${margin.left + plotWidth},${margin.top + plotHeight}`;
  const scoreTicks = [0, 500, 1000].map(value => { const tickX = margin.left + value / 1000 * plotWidth; return `<line x1="${tickX}" y1="${margin.top}" x2="${tickX}" y2="${margin.top + plotHeight}" class="ai-score-grid-line ai-score-grid-line-major"/><text x="${tickX}" y="${height - 15}" text-anchor="middle" class="ai-score-axis-label">${value}</text>`; }).join('');
  const deviationTicks = [0, 50, 100].map(value => { const tickX = margin.left + value / 100 * plotWidth; return `<text x="${tickX}" y="22" text-anchor="middle" class="ai-deviation-axis-label">偏差値 ${value}</text>`; }).join('');
  const yTicks = [0, Math.round(roundedMax / 2), roundedMax].map(value => { const tickY = y(value); return `<line x1="${margin.left}" y1="${tickY}" x2="${margin.left + plotWidth}" y2="${tickY}" class="ai-score-grid-line"/><text x="${margin.left - 12}" y="${tickY + 6}" text-anchor="end" class="ai-score-axis-label">${value.toLocaleString('ja-JP')}</text>`; }).join('');
  root.innerHTML = `<svg viewBox="0 0 ${width} ${height}" role="img" aria-labelledby="aiScoreChartTitle aiScoreChartDesc"><title id="aiScoreChartTitle">会話影響力スコアと偏差値のプレイヤー分布</title><desc id="aiScoreChartDesc">橙色は0から1000までの会話影響力スコア、桜色は平均50、標準偏差10の偏差値、縦軸はプレイヤー人数です。</desc><defs><linearGradient id="aiScoreAreaGradient" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f2a064" stop-opacity=".34"/><stop offset="1" stop-color="#f2a064" stop-opacity=".02"/></linearGradient></defs>${yTicks}${scoreTicks}${deviationTicks}<polygon points="${areaPoints}" fill="url(#aiScoreAreaGradient)"/><polyline points="${scorePoints}" class="ai-score-line"/><polyline points="${deviationPoints}" class="ai-deviation-line"/>${scoreBins.map((count, index) => `<circle cx="${x(index, scoreBins.length).toFixed(1)}" cy="${y(count).toFixed(1)}" r="4" class="ai-score-point"><title>${index * scoreBinStep}点付近: ${count.toLocaleString('ja-JP')}人</title></circle>`).join('')}${deviationBins.map((count, index) => `<circle cx="${x(index, deviationBins.length).toFixed(1)}" cy="${y(count).toFixed(1)}" r="4" class="ai-deviation-point"><title>偏差値${index * deviationBinStep}付近: ${count.toLocaleString('ja-JP')}人</title></circle>`).join('')}<text x="18" y="${margin.top + plotHeight / 2}" text-anchor="middle" transform="rotate(-90 18 ${margin.top + plotHeight / 2})" class="ai-score-axis-title">人数</text><text x="${margin.left + plotWidth / 2}" y="${height - 2}" text-anchor="middle" class="ai-score-axis-title">会話影響力スコア（0〜1000）</text></svg>`;
  const sorted = [...scores].sort((a, b) => a - b), middle = Math.floor(sorted.length / 2);
  const median = sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
  if (summary) summary.innerHTML = `<span class="ai-chart-legend ai-chart-legend-score">点数</span><span class="ai-chart-legend ai-chart-legend-deviation">偏差値</span>${scores.length.toLocaleString('ja-JP')}人・点数中央値 ${Math.round(median)}`;
}

async function loadAiTrial() {
  if (aiTrialLoaded) return;
  const list = qs('aiTrialList');
  const meta = qs('aiTrialMeta');
  if (!list) return;
  try {
    const data = await fetchAiTrialData();
    aiTrialLoaded = true;
    aiTrialData = data;
    aiTrialScoreStats = calculateAiScoreStats(data.results || []);
    renderAiScoreDistribution(data.results || []);
    const created = data.created_at ? new Date(data.created_at).toLocaleString('ja-JP') : '';
    if (meta) meta.textContent = `最終反映: ${created} / モデル: ${data.model || 'Qwen'} / 解析済み: ${data.results?.length || 0}人`;
    ['aiTrialSearch', 'aiTrialMinGames', 'aiTrialSort'].forEach(id => qs(id)?.addEventListener(id === 'aiTrialSearch' ? 'input' : 'change', () => { aiTrialPage = 0; renderAiTrial(); }));
    renderAiTrial();
  } catch (error) {
    console.error(error);
    list.innerHTML = '<div class="card">解析結果を読み込めませんでした。</div>';
  }
}

function renderAiTrial() {
  const list = qs('aiTrialList');
  const pager = qs('aiTrialPager');
  if (!list || !aiTrialData) return;
  const query = (qs('aiTrialSearch')?.value || '').trim().toLowerCase();
  const minGames = Number(qs('aiTrialMinGames')?.value || 0);
  const sort = qs('aiTrialSort')?.value || 'latest';
  const rawValue = (row, key) => Number(row.objective?.raw?.[key] || 0);
  const rows = (aiTrialData.results || []).filter(row => {
    const perspectiveText = Object.values(row.perspectives || {}).join(' ');
    const haystack = `${row.display_name || ''} ${row.trip || ''} ${(row.names || []).join(' ')} ${row.analysis || ''} ${perspectiveText}`.toLowerCase();
    return (!query || haystack.includes(query)) && Number(row.games_sampled || 0) >= minGames;
  });
  const sorters = {
    latest: (a, b) => Number(b.last_played || 0) - Number(a.last_played || 0),
    answers: (a, b) => rawValue(b, '被質問応答率%') - rawValue(a, '被質問応答率%'),
    talk: (a, b) => rawValue(b, '発言数/戦') - rawValue(a, '発言数/戦'),
    questions: (a, b) => rawValue(b, '質問率%') - rawValue(a, '質問率%'),
    reasons: (a, b) => rawValue(b, '根拠語率%') - rawValue(a, '根拠語率%'),
    influence: (a, b) => Number(b.influence_score ?? -1) - Number(a.influence_score ?? -1)
  };
  rows.sort(sorters[sort] || sorters.latest);
  const count = qs('aiTrialFilteredCount');
  if (count) count.textContent = `${rows.length}人`;
  const pages = Math.max(1, Math.ceil(rows.length / AI_TRIAL_PAGE_SIZE));
  aiTrialPage = Math.min(aiTrialPage, pages - 1);
  const visibleRows = rows.slice(aiTrialPage * AI_TRIAL_PAGE_SIZE, (aiTrialPage + 1) * AI_TRIAL_PAGE_SIZE);
  list.innerHTML = visibleRows.map(row => {
      const raw = row.objective?.raw || {};
      const rawLabels = [['発言数/戦',''],['盤面語率%','%'],['質問率%','%'],['被質問応答率%','%'],['他者名言及率%','%'],['根拠語率%','%']];
      const axisRows = rawLabels.map(([key, unit]) => `<div><span>${escapeHtml(key)}</span><b>${Number(raw[key] || 0)}${unit}</b></div>`).join('');
      const logs = (row.log_ids || []).map(id => `<a href="https://zinro.net/m/log.php?id=${Number(id)}" target="_blank" rel="noopener">${Number(id)}</a>`).join(' / ');
      const analysis = typeof row.analysis === 'string' ? row.analysis : '';
      const influenceValue = Number(row.influence_score);
      const deviationValue = calculateAiDeviation(influenceValue, aiTrialScoreStats);
      const influence = Number.isFinite(influenceValue) ? `<div class="ai-influence-score"><b>${influenceValue}</b><span>/ 1000　会話影響力</span><span class="ai-deviation-badge">偏差値 ${deviationValue.toFixed(1)}</span><p>${escapeHtml(row.influence_reason || '')}</p></div>` : '';
      const displayName = row.display_name || '名前不明';
      const nameUrl = buildPersonalShareUrl('name', displayName);
      const tripUrl = row.trip ? buildPersonalShareUrl('trip', row.trip) : '';
      return `<article class="card ai-trial-card">
        <h3><a href="${escapeHtml(nameUrl)}">${escapeHtml(displayName)}</a> ${tripUrl ? `<small><a href="${escapeHtml(tripUrl)}">${escapeHtml(row.trip)}</a></small>` : ''}</h3>
        <div class="ai-trial-last">最終参加: ${row.last_played ? new Date(Number(row.last_played) * 1000).toLocaleString('ja-JP') : '不明'}</div>
        ${influence}<div class="ai-trial-visual">${aiTrialRadar(row.objective?.radar || {})}<div class="ai-trial-axes">${axisRows}</div></div>
        ${aiPerspectiveTabs(row.perspectives, {
          overall: analysis,
          influence: row.influence_reason || analysis,
          reasoning: analysis,
          collaboration: row.personality_analysis || analysis,
          decisions: row.postgame_analysis || analysis
        })}
        ${aiPhaseAnalysis(row)}
        <div class="ai-trial-notes"><b>確認標本</b> ${Number(row.games_sampled || 0)}戦・本人発言${Number(row.target_lines || 0)}件<br>${logs}</div>
        ${aiTrialHistory(row)}
      </article>`;
    }).join('') || '<div class="card">条件に一致する解析結果はありません。</div>';
  if (pager) {
    pager.innerHTML = `<button class="btn btn-ghost" id="aiTrialPrev" ${aiTrialPage === 0 ? 'disabled' : ''}>前へ</button><span>${aiTrialPage + 1} / ${pages}</span><button class="btn btn-ghost" id="aiTrialNext" ${aiTrialPage + 1 >= pages ? 'disabled' : ''}>次へ</button>`;
    qs('aiTrialPrev')?.addEventListener('click', () => { if (aiTrialPage > 0) { aiTrialPage--; renderAiTrial(); scrollTo({ top: qs('tab-ai-trial').offsetTop, behavior: 'smooth' }); } });
    qs('aiTrialNext')?.addEventListener('click', () => { if (aiTrialPage + 1 < pages) { aiTrialPage++; renderAiTrial(); scrollTo({ top: qs('tab-ai-trial').offsetTop, behavior: 'smooth' }); } });
  }
}

// RM検索ボタン・Enter
document.addEventListener("DOMContentLoaded", () => {
  document.querySelectorAll('[data-ai-subtab]').forEach(button => button.addEventListener('click', () => selectAiSubtab(button.dataset.aiSubtab)));
  document.addEventListener('click', event => {
    const button = event.target.closest('.ai-perspective-tab');
    if (!button) return;
    const wrap = button.closest('.ai-perspectives');
    if (!wrap) return;
    const selected = button.dataset.aiPerspective;
    wrap.querySelectorAll('.ai-perspective-tab').forEach(tab => {
      const active = tab === button;
      tab.classList.toggle('is-active', active);
      tab.setAttribute('aria-selected', active ? 'true' : 'false');
    });
    wrap.querySelectorAll('[data-ai-perspective-panel]').forEach(panel => {
      panel.hidden = panel.dataset.aiPerspectivePanel !== selected;
    });
  });
  const btn = document.getElementById("rm-stats-search-btn");
  const inp = document.getElementById("rm-stats-search");
  if (btn && inp) {
    btn.addEventListener("click", () => { window._rmStatsLoaded = true; loadRmStatsList(inp.value.trim(), 0); });
    inp.addEventListener("keydown", (e) => { if(e.key==="Enter") { window._rmStatsLoaded = true; loadRmStatsList(inp.value.trim(), 0); } });
  }
  // URLからRM詳細を復元
  const params = new URLSearchParams(window.location.search);
  if (params.get("tab") === "tab-rm-stats" && params.get("rm")) {
    setTimeout(() => {
      const tabBtn = document.querySelector('[data-tab="tab-rm-stats"]');
      if (tabBtn) tabBtn.click();
      loadRmStatsDetail(params.get("rm"));
    }, 200);
  }
});
