const { useState, useMemo, useRef, useEffect } = React;

// ─── Mock Data ───
let EVENTS = [
  { id: "kobe-marathon-expo-2025", name: "神戸マラソンEXPO 2025", dates: ["2025-11-14", "2025-11-15"], status: "completed", totalSegments: 47, totalIssues: 3 },
  { id: "kyoto-marathon-expo-2026", name: "京都マラソンEXPO 2026", dates: ["2026-02-14", "2026-02-15"], status: "completed", totalSegments: 38, totalIssues: 1 },
  { id: "osaka-event-2026", name: "大阪城ホールイベント 2026", dates: ["2026-03-28"], status: "live", totalSegments: 12, totalIssues: 0 },
];

let SPEAKERS = {
  SPEAKER_00: { name: "菅野", color: "#2563EB", role: "本部" },
  SPEAKER_01: { name: "山本", color: "#0891B2", role: "誘導" },
  SPEAKER_02: { name: "米森", color: "#059669", role: "2号館" },
  SPEAKER_03: { name: "平岡", color: "#D97706", role: "警備" },
  SPEAKER_04: { name: "森本", color: "#7C3AED", role: "ボランティア" },
  SPEAKER_05: { name: "ミラ", color: "#DB2777", role: "受付" },
};

let SEGMENTS = [
  { id: 1, start: 156.3, end: 182.8, speaker: "SPEAKER_00", text: "菅野です。すいません、大変遅くなり申し訳ないんですが、今日の締めの段取り確認をしたいと思います。19時20分に出展事務局に集まっていただきたいかと思うんですが、ご都合の悪い方はいらっしゃいますでしょうか。特になければ20分でお願いします。", hour: 18 },
  { id: 2, start: 189.5, end: 208.1, speaker: "SPEAKER_01", text: "3号館了解しました。3号館も了解しました。", hour: 18 },
  { id: 3, start: 358.4, end: 369.9, speaker: "SPEAKER_04", text: "またアミノサウルスの音を調整したのでまたうるさくなったら教えてください。", hour: 18 },
  { id: 4, start: 2238.3, end: 2266.3, speaker: "SPEAKER_05", text: "ミラです。菅野さん取れます？どうぞ。今更ながらの六甲バターさんから忘れ物で携帯2個持ち込まれました。", hour: 18 },
  { id: 5, start: 2269.4, end: 2293.8, speaker: "SPEAKER_00", text: "いつからの忘れ物なんやろうね。一応そんなに時間は経ってないみたいなんで、もしかしたらまだいるかもしれないですけど。19時半ぐらいかなみたいな感じらしいです。", hour: 18 },
  { id: 6, start: 2838.5, end: 2853.8, speaker: "SPEAKER_00", text: "スガノさん、携帯はクリアしました。2つともクリア。了解。はい同じ方でした。", hour: 18 },
  { id: 7, start: 3024.1, end: 3042.1, speaker: "SPEAKER_00", text: "皆さん一応情報です。1号館の働いているボランティアに関しては安倍さんがいる窓口1の横の盗難扉から帰ることになりました。", hour: 19 },
  { id: 8, start: 3160.9, end: 3189.5, speaker: "SPEAKER_03", text: "平岡、陽次さんどうぞ。上の扉手動にしてもらうのは20時でいいですか？", hour: 19 },
  { id: 9, start: 3189.5, end: 3200.0, speaker: "SPEAKER_00", text: "はい。じゃあ1号館扉20時で両方とも手動にしてもらいます。警備さんに言っておきます。お願いします。", hour: 19 },
  { id: 10, start: 3295.7, end: 3325.5, speaker: "SPEAKER_01", text: "山本くんもう3倍してくれてます。山本くん3倍してくれてますか？はい、あの駅の階段下と右矢印のところですね。2人配置してます。", hour: 19 },
  { id: 11, start: 3329.6, end: 3339.6, speaker: "SPEAKER_00", text: "はい、そろそろちょっと大人目が、じゃあ候補を始めてもらっていいですか。まもなく受付終了となります、みたいな形で。", hour: 19 },
  { id: 12, start: 3389.1, end: 3393.1, speaker: "SPEAKER_01", text: "山本さん、一人階段方向から一人ランナー受付の方向かってます。", hour: 19 },
  { id: 13, start: 3438.6, end: 3467.5, speaker: "SPEAKER_00", text: "森本ちゃん、ちょっともう終わるカーを出すために真ん中の扉に前閉めようか。森本ちゃんボランティア同棲も閉めてもらっていい？", hour: 19 },
  { id: 14, start: 3495.8, end: 3522.9, speaker: "SPEAKER_02", text: "米森から菅野さん、どうぞ。今日は2号館のシャッターってゲストクリア・ランナークリアになればちょっと開くんでしたっけ？", hour: 19 },
  { id: 15, start: 3523.1, end: 3547.4, speaker: "SPEAKER_00", text: "21時ぐらいまで必要があればっていう感じです。了解です。それで言うと今ASICSさんの補充がトラックが4トンぐらいが来てて、今荷物を搬入口のところにまず下ろしたいと。シャッターが開けばそのタイミングで段ボールを入れたいという相談を受けているんですが。", hour: 19 },
  { id: 16, start: 3549.6, end: 3579.5, speaker: "SPEAKER_02", text: "そうですね。トラックだけ早返したいということなので、それに荷物だけ固めて外に置くような感じです。", hour: 19 },
  { id: 17, start: 3579.7, end: 3600.3, speaker: "SPEAKER_00", text: "山本くん、逆に1階から来ている人は向かってますか？駅下、岡本くんいますか？", hour: 19 },
];

let HOUR_BLOCKS = [
  { hour: 18, label: "18:00–19:00", file: "2025_11_14_18.mp3" },
  { hour: 19, label: "19:00–20:00", file: "2025_11_14_19.mp3" },
  { hour: 20, label: "20:00–21:00", file: "2025_11_14_20.mp3" },
];

const LIVE_HOURS = [
  { hour: 13, label: "13:00–14:00", status: "done", summary: "搬入完了・設営確認・特記なし", track: "hourly" },
  { hour: 14, label: "14:00–15:00", status: "done", summary: "来場者誘導開始・混雑なし・ブース整列確認済み", track: "hourly" },
  { hour: 15, label: "15:00–16:00", status: "done", summary: "ブース電源トラブル発生 → 15:42解決", track: "hourly", hasIssue: true },
  { hour: 16, label: "16:00–17:00", status: "processing", summary: "解析中...", track: "realtime" },
  { hour: 17, label: "17:00–18:00", status: "waiting", summary: "" },
  { hour: 18, label: "18:00–19:00", status: "waiting", summary: "" },
];

let REPORT_DATA = {
  overview: "神戸マラソンEXPO 2025は2日間で延べ約8,000名が来場。全体的に円滑な運営が行われたが、初日夕方の搬入タイミングと閉館オペレーションに改善余地あり。無線通信による現場連携は適切に機能し、発生事項は全て当日中に解決した。",
  keyDecisions: [
    { time: "19:20", date: "11/14", summary: "出展事務局集合・締め段取り確認開始", speaker: "菅野", seekTo: 3024.1, hour: 19 },
    { time: "19:50", date: "11/14", summary: "1号館扉を20:00に手動切替・警備へ連絡", speaker: "菅野・平岡", seekTo: 3160.9, hour: 19 },
    { time: "19:55", date: "11/14", summary: "受付終了アナウンス開始指示", speaker: "菅野", seekTo: 3329.6, hour: 19 },
    { time: "19:58", date: "11/14", summary: "ASICSトラック搬入タイミング調整", speaker: "米森・菅野", seekTo: 3495.8, hour: 19 },
    { time: "10:15", date: "11/15", summary: "2日目開場前の動線確認完了", speaker: "山本", seekTo: 615.0, hour: 10 },
  ],
  issues: [
    { type: "忘れ物", date: "11/14", severity: "low", summary: "六甲バターブースから携帯2個 → 同一人物・解決済み", seekTo: 2238.3, hour: 18 },
    { type: "搬入調整", date: "11/14", severity: "medium", summary: "ASICS補充トラック4t到着・シャッター開放タイミング要調整", seekTo: 3523.1, hour: 19 },
    { type: "設備", date: "11/15", severity: "low", summary: "東蔵エリアPA音量調整 → 開場前に解決", seekTo: 450.0, hour: 9 },
  ],
  dailySummaries: {
    "2025-11-14": "閉館オペレーションに集中。19:20から本部が段取り確認を開始し、20:00の受付終了に向けて誘導・扉管理・搬入調整を並行実施。携帯忘れ物1件発生したが解決済み。",
    "2025-11-15": "2日目は円滑に進行。開場前の動線確認が奏功し、混雑なく誘導完了。PA調整1件あったが開場前に解決。全体的に初日の反省が活かされた運営となった。",
  },
  summarySource: "provisional",
  summaryVersion: null,
  summaryProvisional: null,
};
let CHANNEL_LABELS = { default: "default" };

// ─── Utils ───
const fmt = (sec) => `${Math.floor(sec / 60)}:${String(Math.floor(sec % 60)).padStart(2, "0")}`;
const fmtHourMinute = (absSec) => {
  const total = Math.max(0, Number(absSec || 0));
  const h = Math.floor(total / 3600) % 24;
  const m = Math.floor((total % 3600) / 60);
  return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
};
const HOUR_OFFSETS = {};
const SPEAKER_COLORS = ["#2563EB", "#0891B2", "#059669", "#D97706", "#7C3AED", "#DB2777", "#DC2626", "#0EA5E9"];

function hourLabel(hour) {
  const h = Number(hour) || 0;
  if (h === 23) return "23:00-翌00:00";
  return `${String(h).padStart(2, "0")}:00-${String((h + 1) % 24).padStart(2, "0")}:00`;
}

function inferFileStartSec(filePath, fallbackHour) {
  const file = String(filePath || "").split("/").pop() || "";
  const stem = file.replace(/\.[^.]+$/, "");

  // 2025_11_14_13_01_17_xxxx / 2025-11-14-13-01-17-xxxx
  let m = stem.match(/^\d{4}[-_]\d{2}[-_]\d{2}[-_]([01]\d|2[0-3])[-_](\d{2})(?:[-_](\d{2}))?/);
  if (m) {
    const h = Number(m[1] || 0);
    const mi = Number(m[2] || 0);
    const s = Number(m[3] || 0);
    return h * 3600 + mi * 60 + s;
  }

  // 19_xxx
  m = stem.match(/^([01]?\d|2[0-3])_/);
  if (m) {
    return Number(m[1]) * 3600;
  }
  return (Number(fallbackHour) || 0) * 3600;
}

function fileBaseName(filePath) {
  const raw = String(filePath || "");
  if (!raw) return "";
  const parts = raw.split("/");
  return String(parts[parts.length - 1] || "").trim();
}

function normalizeChannelId(raw) {
  const s = String(raw || "").trim().toLowerCase();
  if (!s) return "default";
  return s.replace(/[^a-z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "default";
}

function channelDisplayLabel(channelId) {
  const cid = normalizeChannelId(channelId);
  const custom = String((CHANNEL_LABELS || {})[cid] || "").trim();
  return custom || cid;
}

function resolveSegmentHour(seg) {
  const segAbsStart = Number(seg?.start);
  if (Number.isFinite(segAbsStart)) return Math.floor(Math.max(0, segAbsStart) / 3600) % 24;
  const segHour = Number(seg?.hour);
  return Number.isFinite(segHour) ? segHour : 0;
}

function deriveOverviewText(summary, segments) {
  const direct = String(summary?.overview || "").trim();
  if (direct) return direct;

  const decisions = Array.isArray(summary?.keyDecisions) ? summary.keyDecisions : [];
  const decisionLines = decisions
    .slice(0, 12)
    .map((d) => {
      const t = d?.time ? String(d.time) : null;
      const s = String(d?.summary || "").trim();
      if (!s) return "";
      return t ? `${t} ${s}` : s;
    })
    .filter(Boolean);
  if (decisionLines.length > 0) return decisionLines.join("\n");

  const segLines = (Array.isArray(segments) ? segments : [])
    .slice(0, 12)
    .map((s) => String(s?.text || "").trim())
    .filter(Boolean);
  return segLines.join("\n");
}

function compareSegmentsForDisplay(a, b) {
  const startA = Number(a?.start || 0);
  const startB = Number(b?.start || 0);
  const delta = startA - startB;
  // Small timestamp jitter (sub-second) can invert perceived utterance order.
  // For near-simultaneous segments, use id as a stable tie-breaker.
  if (Math.abs(delta) < 0.75) {
    const idA = Number(a?.rawId ?? a?.id);
    const idB = Number(b?.rawId ?? b?.id);
    if (Number.isFinite(idA) && Number.isFinite(idB) && idA !== idB) {
      return idA - idB;
    }
  }
  if (delta !== 0) return delta;
  const endA = Number(a?.end || 0);
  const endB = Number(b?.end || 0);
  if (endA !== endB) return endA - endB;
  return String(a?.sourceFile || "").localeCompare(String(b?.sourceFile || ""));
}

function rebuildHourOffsets() {
  Object.keys(HOUR_OFFSETS).forEach((k) => delete HOUR_OFFSETS[k]);
  HOUR_BLOCKS.forEach((hb) => {
    const hour = Number(hb.hour) || 0;
    const fromPayload = Number(hb.fileStartSec);
    HOUR_OFFSETS[hour] = Number.isFinite(fromPayload) ? fromPayload : inferFileStartSec(hb.file, hour);
  });
}

function applySnapshot(snapshot) {
  const speakers = snapshot?.speakers || {};
  const segments = Array.isArray(snapshot?.segments) ? snapshot.segments : [];
  const blocks = Array.isArray(snapshot?.hourBlocks) ? snapshot.hourBlocks : [];
  const summary = snapshot?.summary || {};
  const provisionalSummary = snapshot?.summaryProvisional || summary?.provisional || null;
  const snapshotDate = String(snapshot?.event?.date || snapshot?.date || "").trim();

  const speakerEntries = Object.entries(speakers);
  SPEAKERS = {};
  speakerEntries.forEach(([id, sp], idx) => {
    SPEAKERS[id] = {
      name: sp?.name || id,
      role: sp?.role || "speaker",
      color: sp?.color || SPEAKER_COLORS[idx % SPEAKER_COLORS.length],
    };
  });
  if (Object.keys(SPEAKERS).length === 0) {
    SPEAKERS = { SPEAKER_00: { name: "Unknown", role: "speaker", color: SPEAKER_COLORS[0] } };
  }

  SEGMENTS = segments.map((seg, idx) => {
    const start = Number(seg.start) || 0;
    const rawEnd = Number(seg.end);
    const end = Number.isFinite(rawEnd) && rawEnd > start ? rawEnd : (start + 1);
    const hasStartRelValue = seg.startRel !== null && seg.startRel !== undefined && seg.startRel !== "";
    const hasEndRelValue = seg.endRel !== null && seg.endRel !== undefined && seg.endRel !== "";
    const rawStartRel = hasStartRelValue ? Number(seg.startRel) : NaN;
    const rawEndRel = hasEndRelValue ? Number(seg.endRel) : NaN;
    // Defensive: derive hour from absolute start first to avoid stale/legacy hour drift.
    const derivedHour = Math.floor(Math.max(0, start) / 3600) % 24;
    const source = fileBaseName(seg.sourceFile || "");
    const rawId = seg.id ?? idx + 1;
    // Keep a UI-unique id to avoid React key collisions across files/channels
    // (AI payload ids are often reused per source file).
    const uniqueId = `${source || "unknown"}:${start}:${end}:${idx}:${rawId}`;
    return {
      id: uniqueId,
      rawId,
      start,
      end,
      // startRel/endRel are file-relative seconds persisted by pipeline.
      // Playback seek should prefer these when available.
      startRel: Number.isFinite(rawStartRel) ? Math.max(0, rawStartRel) : null,
      endRel: Number.isFinite(rawEndRel) ? Math.max(0, rawEndRel) : null,
      speaker: seg.speaker || "SPEAKER_00",
      text: seg.text || "",
      hour: derivedHour,
      sourceFile: source,
      channelId: normalizeChannelId(seg.channelId),
    };
  });

  if (blocks.length > 0) {
    HOUR_BLOCKS = blocks.map((hb) => ({
      hour: Number(hb.hour) || 0,
      label: hb.label || hourLabel(hb.hour),
      file: hb.file || "",
      sourceFile: fileBaseName(hb.sourceFile || hb.file || ""),
      channelId: normalizeChannelId(hb.channelId),
      fileStartSec: Number.isFinite(Number(hb.fileStartSec))
        ? Number(hb.fileStartSec)
        : inferFileStartSec(hb.file, hb.hour),
    }));
  } else {
    const hours = [...new Set(SEGMENTS.map((s) => s.hour))].sort((a, b) => a - b);
    HOUR_BLOCKS = hours.map((h) => ({ hour: h, label: hourLabel(h), file: "", fileStartSec: h * 3600, channelId: "default" }));
  }

  const nextLabels = {};
  const rawLabels = snapshot?.channelLabels && typeof snapshot.channelLabels === "object" ? snapshot.channelLabels : {};
  Object.entries(rawLabels).forEach(([k, v]) => {
    const cid = normalizeChannelId(k);
    const label = String(v || "").trim();
    nextLabels[cid] = label || cid;
  });
  HOUR_BLOCKS.forEach((hb) => {
    const cid = normalizeChannelId(hb?.channelId);
    if (!nextLabels[cid]) nextLabels[cid] = cid;
  });
  SEGMENTS.forEach((seg) => {
    const cid = normalizeChannelId(seg?.channelId);
    if (!nextLabels[cid]) nextLabels[cid] = cid;
  });
  if (!nextLabels.default) nextLabels.default = "default";
  CHANNEL_LABELS = nextLabels;

  const resolvedOverview = deriveOverviewText(summary, SEGMENTS);
  const normalizedDecisions = (Array.isArray(summary.keyDecisions) ? summary.keyDecisions : []).map((item) => {
    const row = { ...(item || {}) };
    if (!String(row?.date || "").trim() && snapshotDate) row.date = snapshotDate;
    if (!String(row?.time || "").trim() && Number.isFinite(Number(row?.seekTo))) row.time = fmtHourMinute(Number(row.seekTo));
    row.channelId = normalizeChannelId(row.channelId);
    return row;
  });
  const normalizedIssues = (Array.isArray(summary.issues) ? summary.issues : []).map((item) => {
    const row = { ...(item || {}) };
    if (!String(row?.date || "").trim() && snapshotDate) row.date = snapshotDate;
    row.channelId = normalizeChannelId(row.channelId);
    return row;
  });
  REPORT_DATA = {
    ...REPORT_DATA,
    overview: resolvedOverview,
    keyDecisions: normalizedDecisions,
    issues: normalizedIssues,
    reportDocument: (snapshot?.reportDocument && typeof snapshot.reportDocument === "object") ? snapshot.reportDocument : null,
    summarySource: String(snapshot?.summarySource || "provisional"),
    summaryVersion: snapshot?.summaryVersion || null,
    summaryProvisional: provisionalSummary,
    dailySummaries: {
      [snapshotDate || "unknown"]: resolvedOverview,
    },
  };

  rebuildHourOffsets();
}

// ─── Icons ───
const IconRadio = () => (
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
    <path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.4"/>
    <path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.4"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/><circle cx="12" cy="12" r="2"/>
  </svg>
);
const IconPlay = () => <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><polygon points="6,3 20,12 6,21"/></svg>;
const IconPause = () => <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><rect x="5" y="3" width="4" height="18" rx="1"/><rect x="15" y="3" width="4" height="18" rx="1"/></svg>;
const IconSearch = () => <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>;
const IconChevronRight = () => <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"/></svg>;
const IconShare = () => <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>;
const IconLink = ({ size = 13 }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <path d="M10 13a5 5 0 0 0 7.07 0l2.83-2.83a5 5 0 1 0-7.07-7.07L11 4" />
    <path d="M14 11a5 5 0 0 0-7.07 0L4.1 13.83a5 5 0 0 0 7.07 7.07L13 19" />
  </svg>
);
const IconSparkle = () => <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3l1.912 5.813a2 2 0 0 0 1.275 1.275L21 12l-5.813 1.912a2 2 0 0 0-1.275 1.275L12 21l-1.912-5.813a2 2 0 0 0-1.275-1.275L3 12l5.813-1.912a2 2 0 0 0 1.275-1.275L12 3z"/></svg>;
const IconWave = () => <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M2 12h2M6 8v8M10 5v14M14 9v6M18 7v10M22 12h-2"/></svg>;
const IconHeadphones = () => <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>;

// ─── Header ───
function Header({ page, setPage, selectedEvent, setSelectedEvent, selectedDate, setSelectedDate }) {
  const currentEvent = EVENTS.find(e => e.id === selectedEvent);
  const isDetail = ["report", "viewer", "live"].includes(page.view);

  return (
    <header style={{ height: 52, background: "#fff", borderBottom: "1px solid #E2E8F0", display: "flex", alignItems: "center", padding: "0 24px", gap: 0, position: "sticky", top: 0, zIndex: 100, flexShrink: 0 }}>
      {/* ロゴ */}
      <div style={{ display: "flex", alignItems: "center", gap: 9, marginRight: 20, cursor: "pointer", flexShrink: 0 }} onClick={() => setPage({ view: "dashboard" })}>
        <svg width="28" height="28" viewBox="280 70 470 430" fill="none" xmlns="http://www.w3.org/2000/svg">
          <path fill="#0F172A" d="
M510.085907,399.032562 C501.245972,397.018219 493.243042,400.718079 484.943939,400.775208 C461.832306,400.934265 442.196472,393.405182 428.987427,373.804504 C417.072906,356.124786 419.733978,337.846771 430.523224,320.703369 C443.898254,299.451385 463.975128,289.054474 488.802338,288.902222 C498.016357,288.845703 504.095642,286.559082 508.690186,278.235168 C514.710327,267.328339 530.077209,258.678162 545.792603,263.195435 C551.642517,264.876953 556.806946,261.738190 558.389099,256.355927 C559.820862,251.485092 556.778625,246.327957 551.043091,244.437897 C542.928467,241.763855 534.693359,241.755310 526.426025,243.846817 C524.844543,244.246887 523.276794,244.983093 521.059937,244.340515 C520.764221,237.900635 521.648132,231.325394 520.996521,224.717285 C520.024658,214.862381 518.052002,205.321976 514.697327,196.061218 C513.502136,192.761734 513.436218,189.632126 514.312988,186.300568 C520.532898,162.666031 541.750305,147.074341 566.038513,148.732422 C575.834045,149.401123 583.720276,154.165680 589.835327,161.844772 C591.695679,164.180862 593.171265,166.870407 595.756165,168.557144 C598.512512,170.355743 601.388000,171.733276 604.532837,169.487274 C607.657593,167.255630 608.272705,164.162292 607.228088,160.616440 C604.612549,151.738403 600.088684,144.066269 592.485352,138.600998 C577.329163,127.706642 560.589233,126.900993 543.361755,131.964508 C525.116455,137.327164 511.318787,148.565094 502.063782,165.268692 C501.283264,166.677338 500.868744,168.428345 498.945068,169.102936 C496.416748,168.550659 494.973572,166.324570 493.007050,164.873520 C473.197906,150.256760 451.928711,144.351486 428.523529,155.309082 C414.179779,162.024399 405.527405,173.773499 404.725586,189.709702 C404.112915,201.885742 409.649780,212.015945 419.920715,218.978470 C424.381561,222.002441 429.513397,220.631485 432.632965,216.251190 C435.828400,211.764252 435.121704,207.453278 430.457428,202.980286 C421.733704,194.614380 422.349854,183.366882 431.917358,175.913559 C436.214325,172.566071 441.017120,170.486572 446.418030,169.657562 C466.493958,166.576080 486.063934,179.996002 494.089050,197.315918 C502.268036,214.968018 504.490112,233.304932 499.843048,252.314240 C499.094666,255.375504 498.134216,258.362823 496.649506,261.211884 C493.708374,266.855774 489.711731,269.126648 482.894684,269.583801 C444.701630,272.145142 412.544037,298.205811 403.399323,337.117096 C402.609589,340.477539 401.918945,343.251953 397.887970,344.428833 C365.189606,353.975647 325.582214,344.708038 303.869812,314.795929 C291.949738,298.374237 287.551178,279.693756 288.503967,259.634949 C289.617126,236.200806 295.809540,214.368210 309.569427,195.070190 C321.480164,178.365555 337.138702,166.242477 355.993042,158.350891 C360.610626,156.418152 363.354126,153.749176 365.355621,149.021713 C376.625946,122.401596 397.313538,107.659744 425.418640,102.703056 C435.223907,100.973770 444.977417,101.318504 454.643097,103.644081 C457.846771,104.414894 460.097961,103.653816 462.594666,101.250870 C489.331451,75.517662 533.171265,72.306419 563.413269,93.768433 C565.654602,95.359077 567.718628,95.768318 570.375732,95.298599 C603.961609,89.361298 636.193298,103.957329 653.680298,133.035629 C655.149414,135.478592 656.961548,136.928268 659.654663,137.879105 C690.147156,148.645050 707.323853,170.602417 713.108276,201.984711 C713.679321,205.082977 714.348877,208.329163 714.002625,211.397186 C713.186584,218.627640 716.519714,223.842468 720.527771,229.418610 C743.997375,262.070709 744.630920,308.621338 722.257629,341.787750 C720.001587,345.132111 719.162720,348.723541 718.437256,352.453918 C711.338623,388.951599 683.243103,417.120148 646.652039,424.374542 C640.171753,425.659332 639.550171,426.561066 640.524597,433.030426 C642.380371,445.351410 644.218018,457.675171 646.053711,469.999176 C647.828552,481.914490 642.467224,488.125763 630.426270,488.121857 C625.593811,488.120270 620.732788,487.783478 615.934753,488.189514 C605.843140,489.043518 599.800293,485.049683 595.351807,475.589264 C586.014282,455.731262 573.840637,437.616333 557.065918,423.033630 C543.513000,411.251678 528.281860,402.695038 510.085907,399.032562
M673.624878,345.355927 C676.436768,343.674652 677.767029,341.128113 677.948547,337.920776 C678.164734,334.101501 678.583557,330.278717 678.533936,326.462433 C678.422791,317.927368 674.087769,314.256073 665.522766,314.596649 C657.210022,314.927246 648.880127,314.802002 640.560181,314.987823 C636.938599,315.068665 635.575928,313.552429 635.642822,309.900757 C635.813660,300.579712 635.749146,291.252960 635.674744,281.929321 C635.621216,275.227661 632.638916,272.239990 625.910400,272.065826 C622.083679,271.966797 618.249268,271.972900 614.422974,272.087433 C607.606995,272.291382 604.401489,275.539764 604.347717,282.352020 C604.276794,291.342682 604.106445,300.341736 604.413391,309.322327 C604.567810,313.840240 602.807373,315.086731 598.610840,314.991211 C589.624451,314.786713 580.627441,314.790222 571.640076,314.972290 C565.089966,315.104950 562.328491,318.094177 562.206421,324.618195 C562.134766,328.446442 562.156433,332.277527 562.201721,336.106598 C562.289734,343.547424 565.352844,346.599701 572.903015,346.654144 C581.394287,346.715393 589.892822,346.873566 598.375305,346.595367 C602.828491,346.449310 604.598633,347.715973 604.424133,352.450134 C604.093079,361.428955 604.250305,370.430084 604.367920,379.419769 C604.455933,386.145844 608.029541,389.329224 614.795532,389.163269 C617.955139,389.085785 621.125122,389.048828 624.279419,389.209229 C632.195618,389.611847 635.650574,386.453247 635.690125,378.469788 C635.734619,369.478821 635.827942,360.484772 635.640198,351.497498 C635.562256,347.765198 636.985840,346.548187 640.609253,346.616730 C649.430725,346.783691 658.257568,346.663086 667.082275,346.655273 C669.077332,346.653503 671.095398,346.782318 673.624878,345.355927
M669.297852,243.473190 C677.353271,246.149429 684.099304,236.364838 680.604614,229.539734 C667.214600,203.389145 646.923889,187.470551 616.828796,185.965256 C604.046753,185.325912 592.076721,188.587585 580.897705,194.717438 C576.022339,197.390778 574.161804,202.380768 576.084839,206.930542 C578.048706,211.576996 582.811951,213.908646 588.056213,212.544144 C589.960571,212.048691 591.740845,211.039978 593.541748,210.189255 C603.504395,205.483032 613.954285,204.118134 624.692383,206.315201 C641.667175,209.788284 653.580933,219.991745 661.576233,235.085464 C663.297791,238.335571 664.842590,241.704895 669.297852,243.473190
M387.870728,291.668640 C394.793732,293.585693 400.553528,289.455597 400.642639,282.108826 C400.896179,261.214630 391.718658,245.230331 374.421936,233.968246 C362.418854,226.152908 349.122711,223.992004 335.172058,227.295059 C329.679382,228.595535 326.987091,233.567093 328.144104,238.757111 C329.338318,244.113770 333.896606,247.465485 339.235809,246.301376 C362.499908,241.229095 381.780151,260.539490 381.246307,281.400757 C381.125183,286.134369 382.976379,289.396637 387.870728,291.668640z"/>
        </svg>
        <div style={{ lineHeight: 1 }}>
          <span style={{ fontSize: 15, fontWeight: 800, color: "#0F172A", letterSpacing: -0.3 }}>ZipFlow </span>
          <span style={{ fontSize: 15, fontWeight: 400, color: "#0F172A", letterSpacing: -0.3 }}>Chronicle</span>
        </div>
      </div>

      <div style={{ width: 1, height: 20, background: "#E2E8F0", marginRight: 16, flexShrink: 0 }} />

      {/* ダッシュボード時：シンプル */}
      {!isDetail && (
        <span style={{ fontSize: 13, fontWeight: 700, color: "#0F172A" }}>イベント一覧</span>
      )}

      {/* 詳細ページ時：イベント選択 + 日付ボタン */}
      {isDetail && (
        <div style={{ display: "flex", alignItems: "center", gap: 10, flex: 1, minWidth: 0 }}>
          {/* イベント選択ドロップダウン */}
          <select
            value={selectedEvent}
            onChange={e => {
              const ev = EVENTS.find(ev => ev.id === e.target.value);
              setSelectedEvent(e.target.value);
              if (ev) setSelectedDate(ev.dates[0]);
            }}
            style={{ background: "#F8FAFC", border: "1px solid #E2E8F0", borderRadius: 4, padding: "6px 10px", color: "#0F172A", fontSize: 13, fontFamily: "inherit", fontWeight: 700, outline: "none", cursor: "pointer", maxWidth: 260 }}
          >
            {EVENTS.map(ev => (
              <option key={ev.id} value={ev.id}>{ev.name}</option>
            ))}
          </select>

          {/* 日付スライド式 */}
          {(() => {
            const dates = page.view === "report"
              ? ["all", ...(currentEvent?.dates || [])]
              : (currentEvent?.dates || []);
            const visibleCount = 3;
            const allDates = dates;
            const currentIdx = allDates.indexOf(selectedDate);
            const startIdx = Math.max(0, Math.min(currentIdx, allDates.length - visibleCount));
            const visible = allDates.slice(startIdx, startIdx + visibleCount);
            const canPrev = startIdx > 0;
            const canNext = startIdx + visibleCount < allDates.length;
            return (
              <div style={{ display: "flex", alignItems: "center", gap: 3 }}>
                <button onClick={() => { const newStart = Math.max(0, startIdx - 1); setSelectedDate(allDates[newStart]); }}
                  disabled={!canPrev}
                  style={{ width: 24, height: 28, border: "1px solid #E2E8F0", borderRadius: 4, background: canPrev ? "#fff" : "#F8FAFC", color: canPrev ? "#64748B" : "#E2E8F0", cursor: canPrev ? "pointer" : "default", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
                  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
                </button>
                {visible.map(d => (
                  <button key={d} onClick={() => setSelectedDate(d)}
                    style={{ padding: "5px 11px", borderRadius: 4, border: selectedDate === d ? "none" : "1px solid #E2E8F0", cursor: "pointer", background: selectedDate === d ? "#2563EB" : "#fff", color: selectedDate === d ? "#fff" : "#64748B", fontSize: 12, fontWeight: 600, fontFamily: d === "all" ? "inherit" : "monospace", transition: "all 0.15s", whiteSpace: "nowrap" }}>
                    {d === "all" ? "全体" : d.slice(5)}
                  </button>
                ))}
                <button onClick={() => { const newStart = Math.min(allDates.length - visibleCount, startIdx + 1); setSelectedDate(allDates[newStart + visibleCount - 1]); }}
                  disabled={!canNext}
                  style={{ width: 24, height: 28, border: "1px solid #E2E8F0", borderRadius: 4, background: canNext ? "#fff" : "#F8FAFC", color: canNext ? "#64748B" : "#E2E8F0", cursor: canNext ? "pointer" : "default", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
                  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
                </button>
              </div>
            );
          })()}

          {/* ページ切り替えタブ */}
          <div style={{ marginLeft: "auto", display: "flex", gap: 2, flexShrink: 0 }}>
            {currentEvent?.status === "live" && (
              <button onClick={() => setPage({ view: "live", event: currentEvent })}
                style={{ padding: "5px 12px", borderRadius: 4, border: "none", cursor: "pointer", background: page.view === "live" ? "#F0FDF4" : "transparent", color: page.view === "live" ? "#16A34A" : "#94A3B8", fontSize: 12, fontWeight: 600, display: "flex", alignItems: "center", gap: 4 }}>
                <div style={{ width: 5, height: 5, borderRadius: "50%", background: "#22C55E" }} />ライブ
              </button>
            )}
            <button onClick={() => setPage({ view: "report", event: currentEvent })}
              style={{ padding: "5px 12px", borderRadius: 4, border: "none", cursor: "pointer", background: page.view === "report" ? "#EFF6FF" : "transparent", color: page.view === "report" ? "#2563EB" : "#94A3B8", fontSize: 12, fontWeight: 600 }}>
              報告書
            </button>
            <button onClick={() => setPage({ view: "viewer", event: currentEvent, date: selectedDate === "all" ? currentEvent?.dates[0] : selectedDate })}
              style={{ padding: "5px 12px", borderRadius: 4, border: "none", cursor: "pointer", background: page.view === "viewer" ? "#EFF6FF" : "transparent", color: page.view === "viewer" ? "#2563EB" : "#94A3B8", fontSize: 12, fontWeight: 600, display: "flex", alignItems: "center", gap: 5 }}>
              <IconHeadphones />ログを聴く
            </button>
          </div>
        </div>
      )}
    </header>
  );
}

// ─── Dashboard ───
function Dashboard({ setPage }) {
  return (
    <div style={{ padding: "32px", maxWidth: 960, margin: "0 auto" }}>
      <div style={{ marginBottom: 24 }}>
        <h1 style={{ fontSize: 20, fontWeight: 800, color: "#0F172A", letterSpacing: -0.5 }}>イベント一覧</h1>
        <p style={{ fontSize: 13, color: "#94A3B8", marginTop: 3 }}>管理中のイベントと状況</p>
      </div>

      <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
        {EVENTS.map(ev => {
          const isLive = ev.status === "live";
          return (
            <div key={ev.id} style={{
              background: "#fff",
              border: `1px solid ${isLive ? "#BFDBFE" : "#E2E8F0"}`,
              borderRadius: 6,
              boxShadow: isLive ? "0 0 0 3px rgba(37,99,235,0.05)" : "none",
              overflow: "hidden",
            }}>
              {/* メイン行 */}
              <div style={{ padding: "18px 22px", display: "flex", alignItems: "center", gap: 16 }}>
                {/* ステータスドット */}
                <div style={{ width: 32, height: 32, borderRadius: 4, background: isLive ? "#EFF6FF" : "#F8FAFC", border: `1px solid ${isLive ? "#BFDBFE" : "#E2E8F0"}`, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
                  {isLive
                    ? <div style={{ width: 9, height: 9, borderRadius: "50%", background: "#22C55E", boxShadow: "0 0 0 3px rgba(34,197,94,0.2)" }} />
                    : <span style={{ fontSize: 13 }}>📋</span>
                  }
                </div>

                {/* 情報 */}
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 3 }}>
                    <span style={{ fontSize: 14, fontWeight: 700, color: "#0F172A" }}>{ev.name}</span>
                    {isLive && <span style={{ fontSize: 9, fontWeight: 700, padding: "2px 7px", borderRadius: 3, background: "#F0FDF4", color: "#16A34A", border: "1px solid #BBF7D0" }}>進行中</span>}
                    {!isLive && <span style={{ fontSize: 9, fontWeight: 500, color: "#CBD5E1" }}>完了</span>}
                  </div>
                  <div style={{ fontSize: 12, color: "#94A3B8", display: "flex", gap: 14 }}>
                    <span style={{ fontFamily: "monospace" }}>{ev.dates.join(" · ")}</span>
                    <span>{ev.totalSegments}件のログ</span>
                    {ev.totalIssues > 0 && <span style={{ color: "#EF4444" }}>発生事項 {ev.totalIssues}件</span>}
                  </div>
                </div>

                {/* ボタン群 */}
                <div style={{ display: "flex", alignItems: "center", gap: 8, flexShrink: 0 }}>
                  {isLive && (
                    <button onClick={() => setPage({ view: "live", event: ev })}
                      style={{ padding: "7px 14px", borderRadius: 4, cursor: "pointer", background: "#F0FDF4", color: "#16A34A", fontSize: 12, fontWeight: 700, display: "flex", alignItems: "center", gap: 5, border: "1px solid #BBF7D0" }}>
                      <div style={{ width: 5, height: 5, borderRadius: "50%", background: "#22C55E" }} />ライブ
                    </button>
                  )}
                  {/* 報告書：アウトライン */}
                  <button onClick={() => setPage({ view: "report", event: ev })}
                    style={{ padding: "7px 14px", borderRadius: 4, border: "1px solid #E2E8F0", cursor: "pointer", background: "#fff", color: "#334155", fontSize: 12, fontWeight: 600 }}>
                    報告書
                  </button>
                  <button onClick={() => setPage({ view: "viewer", event: ev, date: ev.dates[ev.dates.length - 1] })}
                    style={{ padding: "7px 14px", borderRadius: 4, border: "1px solid #E2E8F0", cursor: "pointer", background: "#fff", color: "#334155", fontSize: 12, fontWeight: 600, display: "flex", alignItems: "center", gap: 5 }}>
                    <IconHeadphones /> ログを聴く
                  </button>
                </div>
              </div>
            </div>
          );
        })}
      </div>

      {/* 統計 */}
      <div style={{ marginTop: 24, display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10 }}>
        {[
          { label: "管理イベント数", value: "3", unit: "件" },
          { label: "総ログ数", value: "97", unit: "件" },
          { label: "解決済み発生事項", value: "4", unit: "件" },
        ].map((s, i) => (
          <div key={i} style={{ background: "#fff", borderRadius: 6, border: "1px solid #E2E8F0", padding: "16px 18px" }}>
            <div style={{ fontSize: 10, color: "#94A3B8", fontWeight: 700, textTransform: "uppercase", letterSpacing: 1.2, marginBottom: 5 }}>{s.label}</div>
            <div style={{ fontSize: 26, fontWeight: 800, color: "#0F172A", letterSpacing: -1 }}>
              {s.value}<span style={{ fontSize: 13, fontWeight: 500, color: "#94A3B8", marginLeft: 3 }}>{s.unit}</span>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── Live ───
function Live({ event }) {
  const statusConf = {
    done: { icon: "●", color: "#16A34A", bg: "#F0FDF4", border: "#D1FAE5" },
    processing: { icon: "●", color: "#2563EB", bg: "#EFF6FF", border: "#BFDBFE" },
    waiting: { icon: "○", color: "#CBD5E1", bg: "#F8FAFC", border: "#E2E8F0" },
  };

  return (
    <div style={{ maxWidth: 640, margin: "0 auto", padding: "28px 20px" }}>
      <div style={{ marginBottom: 20 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 4 }}>
          <h1 style={{ fontSize: 17, fontWeight: 800, color: "#0F172A" }}>{event?.name || "大阪城ホールイベント 2026"}</h1>
          <span style={{ fontSize: 9, fontWeight: 700, padding: "2px 8px", borderRadius: 3, background: "#F0FDF4", color: "#16A34A", border: "1px solid #BBF7D0", display: "flex", alignItems: "center", gap: 4 }}>
            <span style={{ width: 5, height: 5, borderRadius: "50%", background: "#22C55E", display: "inline-block" }} />進行中
          </span>
        </div>
        <div style={{ fontSize: 12, color: "#94A3B8", marginBottom: 12, fontFamily: "monospace" }}>2026年3月28日 · リアルタイム更新中</div>
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          <button style={{ padding: "7px 14px", borderRadius: 4, border: "1px solid #E2E8F0", background: "#fff", cursor: "pointer", display: "flex", alignItems: "center", gap: 5, fontSize: 12, fontWeight: 600, color: "#334155" }}>
            <IconShare /> 関係者限定URLをコピー
          </button>
          <button onClick={() => setPage({ view: "report", event })}
            style={{ padding: "7px 14px", borderRadius: 4, border: "1px solid #E2E8F0", background: "#fff", cursor: "pointer", fontSize: 12, fontWeight: 600, color: "#334155", display: "flex", alignItems: "center", gap: 4 }}>
            報告書を見る <IconChevronRight />
          </button>
          <button onClick={() => setPage({ view: "dashboard" })}
            style={{ padding: "7px 14px", borderRadius: 4, border: "none", background: "transparent", cursor: "pointer", fontSize: 12, fontWeight: 500, color: "#94A3B8" }}>
            ← 一覧
          </button>
        </div>
      </div>

      <div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
        {LIVE_HOURS.map((block, i) => {
          const sc = statusConf[block.status];
          return (
            <div key={i} style={{ background: sc.bg, borderRadius: 4, border: `1px solid ${sc.border}`, padding: "12px 16px" }}>
              <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                <span style={{ fontSize: 9, color: sc.color, fontWeight: 700, flexShrink: 0 }}>{sc.icon}</span>
                <span style={{ fontSize: 12, fontWeight: 700, color: sc.color, fontFamily: "monospace", width: 92, flexShrink: 0 }}>{block.label}</span>
                {block.status === "done" && <span style={{ fontSize: 13, color: "#334155", flex: 1 }}>{block.summary}</span>}
                {block.status === "processing" && (
                  <span style={{ fontSize: 12, color: "#2563EB", flex: 1, display: "flex", alignItems: "center", gap: 6 }}>
                    <span style={{ display: "flex", gap: 2 }}>
                      {[0,1,2].map(j => <span key={j} style={{ width: 4, height: 4, borderRadius: "50%", background: "#2563EB", display: "inline-block", animation: `blink 1s ${j*0.2}s infinite` }} />)}
                    </span>
                    解析中...
                  </span>
                )}
                {block.status === "waiting" && <span style={{ fontSize: 12, color: "#CBD5E1", flex: 1 }}>待機中</span>}
                <div style={{ display: "flex", gap: 5, flexShrink: 0 }}>
                  {block.hasIssue && <span style={{ fontSize: 9, fontWeight: 700, color: "#DC2626", background: "#FEF2F2", border: "1px solid #FECACA", padding: "1px 6px", borderRadius: 3 }}>要確認</span>}
                  {block.track === "hourly" && block.status === "done" && <span style={{ fontSize: 9, color: "#059669", fontWeight: 700, background: "#F0FDF4", padding: "1px 5px", borderRadius: 3 }}>確定</span>}
                  {block.track === "realtime" && block.status === "done" && <span style={{ fontSize: 9, color: "#2563EB", fontWeight: 700, background: "#EFF6FF", padding: "1px 5px", borderRadius: 3 }}>速報</span>}
                </div>
              </div>
            </div>
          );
        })}
      </div>

      <div style={{ marginTop: 14, padding: "10px 14px", borderRadius: 4, background: "#F8FAFC", border: "1px solid #E2E8F0", fontSize: 11, color: "#94A3B8", lineHeight: 1.9 }}>
        <span style={{ fontSize: 9, fontWeight: 700, color: "#059669", background: "#F0FDF4", padding: "1px 5px", borderRadius: 3, marginRight: 5 }}>確定</span>1時間分の音声を精度高く解析
        <br />
        <span style={{ fontSize: 9, fontWeight: 700, color: "#2563EB", background: "#EFF6FF", padding: "1px 5px", borderRadius: 3, marginRight: 5 }}>速報</span>会話ごとのリアルタイム解析（後で確定版に更新）
      </div>
    </div>
  );
}

// ─── Report ───
function Report({ event, setPage, selectedDate, setSelectedDate }) {
  const activeTab = selectedDate || "all";
  const [miniPlayer, setMiniPlayer] = useState(null);
  const [summaryGenerated, setSummaryGenerated] = useState(false);

  const sameDate = (itemDate, targetDate) => {
    const a = String(itemDate || "").trim();
    const b = String(targetDate || "").trim();
    if (!a || !b) return false;
    return a === b || a === b.slice(5) || a.slice(5) === b;
  };
  const filteredDecisions = activeTab === "all"
    ? REPORT_DATA.keyDecisions
    : REPORT_DATA.keyDecisions.filter(d => sameDate(d.date, activeTab));
  const filteredIssues = activeTab === "all"
    ? REPORT_DATA.issues
    : REPORT_DATA.issues.filter(i => sameDate(i.date, activeTab));
  const reportDoc = (activeTab !== "all" && REPORT_DATA.reportDocument && typeof REPORT_DATA.reportDocument === "object")
    ? REPORT_DATA.reportDocument
    : null;
  const reportOverviewText = reportDoc
    ? (Array.isArray(reportDoc?.sections?.overview) ? reportDoc.sections.overview.join("\n") : String(REPORT_DATA.overview || ""))
    : (activeTab === "all" ? REPORT_DATA.overview : REPORT_DATA.dailySummaries[activeTab]);
  const displayDecisions = reportDoc && Array.isArray(reportDoc?.sections?.highlights)
    ? reportDoc.sections.highlights
    : filteredDecisions;
  const displayIssues = reportDoc && Array.isArray(reportDoc?.sections?.issues)
    ? reportDoc.sections.issues
    : filteredIssues;

  const severityConf = {
    low: { color: "#D97706", bg: "#FFFBEB", border: "#FDE68A" },
    medium: { color: "#DC2626", bg: "#FEF2F2", border: "#FECACA" },
  };

  return (
    <div style={{ display: "flex", height: "calc(100vh - 52px)", overflow: "hidden" }}>

      {/* 左サイドペイン（ナビ専用） */}
      <aside style={{ width: 220, borderRight: "1px solid #E2E8F0", display: "flex", flexDirection: "column", background: "#fff", flexShrink: 0 }}>

        {/* イベント名 */}
        <div style={{ padding: "18px 16px 14px", borderBottom: "1px solid #F1F5F9" }}>
          <div style={{ fontSize: 13, fontWeight: 800, color: "#0F172A", marginBottom: 2 }}>{event?.name || "神戸マラソンEXPO 2025"}</div>
          <div style={{ fontSize: 11, color: "#94A3B8", fontFamily: "monospace" }}>2025-11-14 · 11-15</div>
        </div>

        <div style={{ height: 1, background: "#F1F5F9" }} />

        {/* ログを聴く */}
        <div style={{ padding: "0 10px" }}>
          <div style={{ fontSize: 9, fontWeight: 700, color: "#CBD5E1", textTransform: "uppercase", letterSpacing: 1.3, marginBottom: 6, padding: "0 4px" }}>ログを聴く</div>
          {(event?.dates || ["2025-11-14", "2025-11-15"]).map(d => (
            <button key={d} onClick={() => setPage({ view: "viewer", event, date: d })}
              style={{ width: "100%", padding: "8px 10px", borderRadius: 4, border: "1px solid transparent", cursor: "pointer", background: "transparent", color: "#2563EB", fontSize: 12, fontWeight: 600, display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 2, fontFamily: "inherit", transition: "all 0.12s" }}
              onMouseEnter={e => { e.currentTarget.style.background = "#EFF6FF"; e.currentTarget.style.borderColor = "#BFDBFE"; }}
              onMouseLeave={e => { e.currentTarget.style.background = "transparent"; e.currentTarget.style.borderColor = "transparent"; }}
            >
              <span style={{ display: "flex", alignItems: "center", gap: 6 }}><IconHeadphones />{d}</span>
              <IconChevronRight />
            </button>
          ))}
        </div>

        {/* スペーサー */}
        <div style={{ flex: 1 }} />

        {/* 総括生成・戻るボタン */}
        <div style={{ padding: "12px 10px 16px", borderTop: "1px solid #F1F5F9", display: "flex", flexDirection: "column", gap: 6 }}>
          <button onClick={() => setSummaryGenerated(true)}
            style={{ width: "100%", padding: "8px 12px", borderRadius: 4, border: "none", cursor: "pointer", background: summaryGenerated ? "#F0FDF4" : "#2563EB", color: summaryGenerated ? "#16A34A" : "#fff", fontSize: 12, fontWeight: 700, display: "flex", alignItems: "center", justifyContent: "center", gap: 5, fontFamily: "inherit", transition: "all 0.2s" }}>
            <IconSparkle />{summaryGenerated ? "総括生成済み" : "総括レポート生成"}
          </button>
        </div>
      </aside>

      {/* メインエリア */}
      <div style={{ flex: 1, overflowY: "auto", padding: "28px 32px", paddingBottom: miniPlayer ? 76 : 28 }}>
        <div style={{ maxWidth: 680 }}>

          {/* 概要 */}
          <div style={{ background: "#FAFBFF", borderRadius: 4, border: "1px solid #E8ECEF", padding: "16px 18px", marginBottom: 20 }}>
            <div style={{ fontSize: 10, fontWeight: 700, color: "#94A3B8", marginBottom: 8, textTransform: "uppercase", letterSpacing: 1.4 }}>概要</div>
            <div style={{ fontSize: 13, lineHeight: 1.9, color: "#334155" }}>
              {reportOverviewText}
            </div>
          </div>

          {/* 特記事項 */}
          <div style={{ marginBottom: 20 }}>
            <div style={{ fontSize: 11, fontWeight: 700, color: "#0F172A", marginBottom: 8, display: "flex", alignItems: "center", gap: 6 }}>
              特記事項
              <span style={{ fontSize: 10, color: "#94A3B8", fontWeight: 400 }}>{displayDecisions.length}件</span>
            </div>
            <div style={{ background: "#fff", borderRadius: 4, border: "1px solid #E2E8F0", overflow: "hidden" }}>
              {displayDecisions.map((d, i) => (
                <div key={i}
                  style={{ display: "flex", alignItems: "center", gap: 12, padding: "13px 16px", borderBottom: i < displayDecisions.length - 1 ? "1px solid #F8FAFC" : "none", transition: "background 0.1s" }}
                  onMouseEnter={e => e.currentTarget.style.background = "#F8FAFC"}
                  onMouseLeave={e => e.currentTarget.style.background = "transparent"}
                >
                  <span style={{ fontFamily: "monospace", fontSize: 11, color: "#6366F1", width: 36, flexShrink: 0, fontWeight: 700 }}>{d.time}</span>
                  {activeTab === "all" && <span style={{ fontSize: 10, color: "#CBD5E1", width: 26, flexShrink: 0, fontFamily: "monospace" }}>{d.date}</span>}
                  <span style={{ flex: 1, fontSize: 13, color: "#334155" }}>{d.summary}</span>
                  <button onClick={() => setMiniPlayer(d)}
                    style={{ width: 26, height: 26, borderRadius: "50%", border: "none", background: miniPlayer === d ? "#2563EB" : "#EFF6FF", color: miniPlayer === d ? "#fff" : "#2563EB", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, transition: "all 0.15s" }}>
                    <IconPlay />
                  </button>
                </div>
              ))}
            </div>
          </div>

          {/* 発生事項（メインペインへ移動） */}
          <div>
            <div style={{ fontSize: 11, fontWeight: 700, color: "#0F172A", marginBottom: 8, display: "flex", alignItems: "center", gap: 6 }}>
              🔴 発生事項
              <span style={{ fontSize: 10, color: "#94A3B8", fontWeight: 400 }}>{displayIssues.length}件</span>
            </div>
            {displayIssues.length === 0
              ? <div style={{ fontSize: 13, color: "#CBD5E1", padding: "16px", background: "#fff", borderRadius: 4, border: "1px solid #E2E8F0", textAlign: "center" }}>この期間の発生事項はありません</div>
              : (
                <div style={{ background: "#fff", borderRadius: 4, border: "1px solid #E2E8F0", overflow: "hidden" }}>
                  {displayIssues.map((issue, i) => {
                    const sc = severityConf[issue.severity] || severityConf.medium;
                    return (
                      <div key={i}
                        style={{ display: "flex", alignItems: "center", gap: 12, padding: "13px 16px", borderBottom: i < filteredIssues.length - 1 ? "1px solid #F8FAFC" : "none", transition: "background 0.1s" }}
                        onMouseEnter={e => e.currentTarget.style.background = "#F8FAFC"}
                        onMouseLeave={e => e.currentTarget.style.background = "transparent"}
                      >
                        <span style={{ fontSize: 9, fontWeight: 700, color: sc.color, background: sc.bg, border: `1px solid ${sc.border}`, padding: "2px 7px", borderRadius: 3, flexShrink: 0 }}>{issue.type}</span>
                        {activeTab === "all" && <span style={{ fontSize: 10, color: "#CBD5E1", width: 26, flexShrink: 0, fontFamily: "monospace" }}>{issue.date}</span>}
                        <span style={{ flex: 1, fontSize: 13, color: "#334155" }}>{issue.summary}{issue.action && <span style={{ display: "block", marginTop: 4, color: "#64748B", fontSize: 12 }}>対応：{issue.action}</span>}</span>
                        <button onClick={() => setMiniPlayer(issue)}
                          style={{ width: 26, height: 26, borderRadius: "50%", border: "none", background: miniPlayer === issue ? "#2563EB" : "#EFF6FF", color: miniPlayer === issue ? "#fff" : "#2563EB", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, transition: "all 0.15s" }}>
                          <IconPlay />
                        </button>
                      </div>
                    );
                  })}
                </div>
              )
            }
          </div>
        </div>
      </div>

      {/* ミニプレーヤー */}
      {miniPlayer && (
        <div style={{ position: "fixed", bottom: 0, left: 0, right: 0, background: "#fff", borderTop: "1px solid #E2E8F0", padding: "10px 28px", display: "flex", alignItems: "center", gap: 12, boxShadow: "0 -4px 16px rgba(0,0,0,0.07)", zIndex: 50, animation: "fadeIn 0.15s" }}>
          <button style={{ width: 32, height: 32, borderRadius: "50%", border: "none", background: "#2563EB", color: "#fff", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 2px 8px rgba(37,99,235,0.3)", flexShrink: 0 }}><IconPlay /></button>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 12, fontWeight: 600, color: "#0F172A", marginBottom: 4, display: "flex", alignItems: "center", gap: 6 }}>
              {miniPlayer.time && <span style={{ fontFamily: "monospace", color: "#6366F1", fontSize: 11 }}>{miniPlayer.time}</span>}
              <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{miniPlayer.summary}</span>
            </div>
            <div style={{ height: 3, background: "#EFF6FF", borderRadius: 2, overflow: "hidden" }}>
              <div style={{ width: "35%", height: "100%", background: "#2563EB", borderRadius: 2 }} />
            </div>
          </div>
          <button onClick={() => setMiniPlayer(null)}
            style={{ width: 24, height: 24, borderRadius: "50%", border: "1px solid #E2E8F0", background: "#fff", cursor: "pointer", color: "#94A3B8", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, fontSize: 12 }}>×</button>
        </div>
      )}
    </div>
  );
}

// ─── Viewer ───
function Viewer({ event, date, setPage, initialHour = null, initialFile = "", initialChannel = "", dataVersion = 0 }) {
  const [selectedHour, setSelectedHour] = useState(HOUR_BLOCKS[0]?.hour ?? 0);
  const [selectedChannel, setSelectedChannel] = useState("default");
  const [preferredSourceFile, setPreferredSourceFile] = useState("");
  const [searchQuery, setSearchQuery] = useState("");
  const [activeSegment, setActiveSegment] = useState(null);
  const [isPlaying, setIsPlaying] = useState(false);
  const [playbackTime, setPlaybackTime] = useState(0);
  const [seekWarning, setSeekWarning] = useState("");
  const scrollRef = useRef(null);
  const segmentRefs = useRef({});
  const audioRef = useRef(null);
  const pendingSeekRef = useRef(null);
  const debugSeek = useMemo(() => {
    try {
      const params = new URLSearchParams(window.location.search || "");
      return params.get("debugSeek") === "1";
    } catch (_e) {
      return false;
    }
  }, []);
  const debugSeekLog = (label, payload) => {
    if (!debugSeek) return;
    try {
      console.log(`[seek-debug] ${label}`, payload);
    } catch (_e) {
      // ignore
    }
  };
  const hourValues = useMemo(
    () => Array.from(new Set(HOUR_BLOCKS.map((h) => Number(h.hour)).filter((h) => Number.isFinite(h)))).sort((a, b) => a - b),
    [event?.id, date, dataVersion]
  );
  const blocksForHour = useMemo(
    () => HOUR_BLOCKS.filter((h) => Number(h.hour) === Number(selectedHour) && normalizeChannelId(h.channelId) === selectedChannel),
    [selectedHour, selectedChannel, dataVersion]
  );
  const channelsForDate = useMemo(() => {
    const channelSet = new Set();
    HOUR_BLOCKS.forEach((h) => {
      channelSet.add(normalizeChannelId(h.channelId));
    });
    SEGMENTS.forEach((s) => {
      channelSet.add(normalizeChannelId(s.channelId));
    });
    const list = Array.from(channelSet).sort((a, b) => a.localeCompare(b));
    return list.length > 0 ? list : ["default"];
  }, [dataVersion]);
  const channelOptionRows = useMemo(
    () => channelsForDate.map((cid) => ({ id: cid, label: channelDisplayLabel(cid) })),
    [channelsForDate, dataVersion]
  );
  const currentBlock = useMemo(() => {
    if (blocksForHour.length === 0) {
      return null;
    }
    const preferred = fileBaseName(preferredSourceFile || "");
    if (!preferred) return blocksForHour[0];
    return blocksForHour.find((h) => fileBaseName(h?.sourceFile || h?.file || "") === preferred) || blocksForHour[0];
  }, [blocksForHour, preferredSourceFile, selectedHour]);
  const currentSourceFile = fileBaseName(currentBlock?.sourceFile || currentBlock?.file || "");
  const showSeekWarning = (msg) => {
    setSeekWarning(msg);
    window.clearTimeout(window.__seekWarnTimer);
    window.__seekWarnTimer = window.setTimeout(() => setSeekWarning(""), 2800);
  };
  const matchesSelectedHour = (seg) => {
    if (!seg) return false;
    const segHour = resolveSegmentHour(seg);
    const sameHour = Number.isFinite(segHour) && segHour === Number(selectedHour);
    if (!sameHour) return false;
    const sameChannel = normalizeChannelId(seg.channelId) === selectedChannel;
    if (!sameChannel) return false;
    // Safety guard: when channel is pinned and audio source is selected, keep source-aligned view.
    if (currentSourceFile) {
      return fileBaseName(seg.sourceFile || "") === currentSourceFile;
    }
    return true;
  };

  const filteredSegments = useMemo(() => {
    let segs = SEGMENTS.filter((s) => matchesSelectedHour(s))
      .sort(compareSegmentsForDisplay);
    if (searchQuery.trim()) segs = segs.filter(s => s.text.toLowerCase().includes(searchQuery.toLowerCase()));
    return segs;
  }, [selectedHour, selectedChannel, searchQuery, dataVersion]);
  const HOUR_EDGE_BUFFER_SEC = 0;
  const hourStartSec = (Number(selectedHour) || 0) * 3600;
  const hourEndSec = hourStartSec + 3600;
  const decisionList = Array.isArray(REPORT_DATA.keyDecisions) ? REPORT_DATA.keyDecisions : [];
  const issueList = Array.isArray(REPORT_DATA.issues) ? REPORT_DATA.issues : [];
  const provisionalDecisionList = Array.isArray(REPORT_DATA?.summaryProvisional?.keyDecisions) ? REPORT_DATA.summaryProvisional.keyDecisions : [];
  const provisionalIssueList = Array.isArray(REPORT_DATA?.summaryProvisional?.issues) ? REPORT_DATA.summaryProvisional.issues : [];
  const resolveItemHour = (item) => {
    const seekAbs = Number(item?.seekToAbsoluteSec);
    if (Number.isFinite(seekAbs) && seekAbs > 0) {
      return Math.floor(Math.max(0, seekAbs) / 3600) % 24;
    }
    const explicitHour = Number(item?.hour);
    if (Number.isFinite(explicitHour)) {
      return explicitHour;
    }
    const seekTo = Number(item?.seekTo);
    if (Number.isFinite(seekTo) && seekTo > 24 * 3600) {
      return Math.floor(Math.max(0, seekTo) / 3600) % 24;
    }
    const t = String(item?.time || "").trim();
    const m = t.match(/^(\d{1,2}):(\d{2})/);
    if (m) {
      return Number(m[1]) % 24;
    }
    return null;
  };
  const formatDecisionTime = (item) => {
    const t = String(item?.time || "").trim();
    const m = t.match(/^(\d{1,2}):(\d{2})/);
    if (m) {
      return `${String(Number(m[1])).padStart(2, "0")}:${m[2]}`;
    }
    const seekTo = Number(item?.seekTo);
    if (Number.isFinite(seekTo)) {
      return fmtHourMinute(seekTo);
    }
    return t || "--:--";
  };
  const resolveItemAbsoluteForFilter = (item) => {
    const seekAbs = Number(item?.seekToAbsoluteSec);
    if (Number.isFinite(seekAbs) && seekAbs > 0) return seekAbs;
    const seekTo = Number(item?.seekTo);
    if (Number.isFinite(seekTo) && seekTo > 24 * 3600) return seekTo;
    const t = String(item?.time || "").trim();
    const m = t.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?/);
    const byClock = m
      ? ((Number(m[1]) % 24) * 3600 + Number(m[2]) * 60 + Number(m[3] || 0))
      : NaN;
    if (Number.isFinite(byClock)) return byClock;
    return NaN;
  };
  const filterSummaryItems = (items) => (
    items
      .filter((d) => {
        const seekTo = resolveItemAbsoluteForFilter(d);
        const inWindow = Number.isFinite(seekTo) && seekTo >= (hourStartSec - HOUR_EDGE_BUFFER_SEC) && seekTo <= (hourEndSec + HOUR_EDGE_BUFFER_SEC);
        const sameHour = resolveItemHour(d) === Number(selectedHour) || inWindow;
        if (!sameHour) return false;
        return normalizeChannelId(d?.channelId) === selectedChannel;
      })
      .sort((a, b) => Number(a?.seekTo || 0) - Number(b?.seekTo || 0))
  );
  const visibleDecisions = (() => {
    const primary = filterSummaryItems(decisionList);
    if (primary.length > 0 || REPORT_DATA.summarySource !== "daily_report") return primary;
    return filterSummaryItems(provisionalDecisionList);
  })();
  const visibleIssues = (() => {
    const primary = filterSummaryItems(issueList);
    if (primary.length > 0 || REPORT_DATA.summarySource !== "daily_report") return primary;
    return filterSummaryItems(provisionalIssueList);
  })();

  const fileOffsetSec = Number(currentBlock?.fileStartSec ?? HOUR_OFFSETS[selectedHour] ?? hourStartSec);
  // Playback timeline must stay file-relative.
  const toRelativeSec = (absSec) => Math.max(0, Number(absSec || 0) - fileOffsetSec);
  const audioSrc = event?.id && date && currentBlock?.file
    ? `/audio/${event.id}/${date}/${currentBlock.file}`
    : "";
  const sortedHourSegments = useMemo(
    () => SEGMENTS
      .filter((s) => matchesSelectedHour(s))
      .sort(compareSegmentsForDisplay),
    [selectedHour, selectedChannel, dataVersion]
  );
  const allHourSegments = useMemo(
    () => SEGMENTS
      .filter((s) => resolveSegmentHour(s) === Number(selectedHour) && normalizeChannelId(s.channelId) === selectedChannel)
      .sort(compareSegmentsForDisplay),
    [selectedHour, selectedChannel, dataVersion]
  );
  const activeSegmentIndex = useMemo(
    () => sortedHourSegments.findIndex((s) => s.id === activeSegment),
    [sortedHourSegments, activeSegment]
  );
  const hourSourceStats = useMemo(() => {
    const bySource = {};
    sortedHourSegments.forEach((s) => {
      const src = fileBaseName(s?.sourceFile || "") || "(unknown)";
      bySource[src] = (bySource[src] || 0) + 1;
    });
    return Object.entries(bySource)
      .map(([source, count]) => ({ source, count: Number(count || 0) }))
      .sort((a, b) => b.count - a.count);
  }, [sortedHourSegments]);
  const knownHourSourceStats = useMemo(
    () => hourSourceStats.filter((r) => r.source !== "(unknown)"),
    [hourSourceStats]
  );
  const hourChannelCount = useMemo(
    () => new Set(allHourSegments.map((s) => normalizeChannelId(s.channelId))).size,
    [allHourSegments]
  );
  const hasMultiSourceInHour = knownHourSourceStats.length > 1 && hourChannelCount <= 1;
  const resolveBlockForTarget = (targetHour, targetSourceFile = "") => {
    const desiredHour = Number.isFinite(Number(targetHour)) ? Number(targetHour) : Number(selectedHour);
    const desiredSource = fileBaseName(targetSourceFile || "");
    const byHour = HOUR_BLOCKS.filter((x) => Number(x.hour) === desiredHour && normalizeChannelId(x.channelId) === selectedChannel);
    if (byHour.length === 0) return null;
    if (!desiredSource) return byHour[0];
    return byHour.find((x) => fileBaseName(x?.sourceFile || x?.file || "") === desiredSource) || byHour[0];
  };

  useEffect(() => {
    const cur = sortedHourSegments.find((s) => {
      if (!(playbackTime >= s.start && playbackTime <= s.end)) return false;
      if (!currentSourceFile) return true;
      return fileBaseName(s.sourceFile || "") === currentSourceFile;
    });
    if (cur) {
      setActiveSegment(cur.id);
      segmentRefs.current[cur.id]?.scrollIntoView({ behavior: "smooth", block: "nearest" });
    } else {
      setActiveSegment(null);
    }
  }, [playbackTime, sortedHourSegments, currentSourceFile]);

  useEffect(() => {
    const firstHour = hourValues[0] ?? 0;
    setSelectedHour((prev) => (hourValues.some((h) => Number(h) === Number(prev)) ? prev : firstHour));
    setSelectedChannel("default");
    setPreferredSourceFile("");
    setSearchQuery("");
    setActiveSegment(null);
    setPlaybackTime(0);
    setIsPlaying(false);
  }, [event?.id, date, hourValues]);

  useEffect(() => {
    if (!channelsForDate.includes(selectedChannel)) {
      setSelectedChannel(channelsForDate[0] || "default");
      setPreferredSourceFile("");
    }
  }, [channelsForDate, selectedChannel]);

  useEffect(() => {
    if (!HOUR_BLOCKS.length) return;
    if (initialChannel) {
      setSelectedChannel(normalizeChannelId(initialChannel));
    }
    if (initialFile) {
      const matched = HOUR_BLOCKS.find((hb) => String(hb?.file || "").endsWith(initialFile));
      if (matched) {
        setSelectedHour(Number(matched.hour) || 0);
        setPreferredSourceFile(fileBaseName(matched?.sourceFile || matched?.file || ""));
        return;
      }
    }
    if (Number.isFinite(Number(initialHour))) {
      const h = Number(initialHour);
      if (HOUR_BLOCKS.some((hb) => Number(hb.hour) === h)) {
        setSelectedHour(h);
      }
    }
  }, [initialHour, initialFile, initialChannel, event?.id, date]);

  useEffect(() => {
    const el = audioRef.current;
    if (!el) return;
    el.pause();
    const defaultSeek = Math.max(0, hourStartSec - fileOffsetSec);
    el.currentTime = defaultSeek;
    setIsPlaying(false);
    setPlaybackTime(fileOffsetSec + defaultSeek);
    setActiveSegment(null);
  }, [audioSrc, selectedHour, hourStartSec, fileOffsetSec]);

  useEffect(() => {
    if (!blocksForHour.length) {
      setPreferredSourceFile("");
      return;
    }
    const preferred = fileBaseName(preferredSourceFile || "");
    if (preferred && blocksForHour.some((b) => fileBaseName(b?.sourceFile || b?.file || "") === preferred)) {
      return;
    }
    setPreferredSourceFile(fileBaseName(blocksForHour[0]?.sourceFile || blocksForHour[0]?.file || ""));
  }, [selectedHour, blocksForHour, preferredSourceFile]);

  const handleTimeUpdate = () => {
    const el = audioRef.current;
    if (!el) return;
    setPlaybackTime(fileOffsetSec + el.currentTime);
  };

  const togglePlay = () => {
    const el = audioRef.current;
    if (!el || !audioSrc) return;
    if (el.paused) {
      el.play().then(() => setIsPlaying(true)).catch(() => {});
    } else {
      el.pause();
      setIsPlaying(false);
    }
  };

  const nudgePlayback = (deltaSec) => {
    const el = audioRef.current;
    if (!el || !audioSrc) return;
    const duration = Number(el.duration);
    const hasDuration = Number.isFinite(duration) && duration > 0;
    const next = Math.max(0, el.currentTime + Number(deltaSec || 0));
    const clamped = hasDuration ? Math.min(next, Math.max(0, duration - 0.1)) : next;
    el.currentTime = clamped;
    setPlaybackTime(fileOffsetSec + clamped);
  };

  const resolveSeekWithinDuration = (preferredSeek, fallbackSeek, el) => {
    const preferred = Number(preferredSeek);
    const fallback = Number(fallbackSeek);
    const duration = Number(el?.duration);
    const hasDuration = Number.isFinite(duration) && duration > 0;
    const canUsePreferred = Number.isFinite(preferred) && preferred >= 0;
    const canUseFallback = Number.isFinite(fallback) && fallback >= 0;
    if (!hasDuration) {
      if (canUsePreferred) return preferred;
      if (canUseFallback) return fallback;
      return 0;
    }
    if (canUsePreferred && preferred <= duration + 1) return preferred;
    if (canUseFallback && fallback <= duration + 1) return fallback;
    const base = canUsePreferred ? preferred : (canUseFallback ? fallback : 0);
    return Math.max(0, Math.min(base, Math.max(0, duration - 0.25)));
  };

  const runAbsoluteSeek = (absSec, hourForOffset, targetSourceFile = "") => {
    const el = audioRef.current;
    if (!el || !audioSrc) return;
    const h = Number.isFinite(Number(hourForOffset)) ? Number(hourForOffset) : Number(selectedHour);
    const block = resolveBlockForTarget(h, targetSourceFile) || currentBlock;
    const offset = Number(block?.fileStartSec ?? HOUR_OFFSETS[h] ?? h * 3600);
    const rawSeek = Math.max(0, Number(absSec || 0) - offset);
    const seek = resolveSeekWithinDuration(rawSeek, rawSeek, el);
    if (Math.abs(seek - rawSeek) > 0.75) {
      debugSeekLog("absolute_seek_clamped_to_duration", {
        absSec,
        rawSeek,
        clampedSeek: seek,
        duration: Number(el?.duration),
        hourForOffset: h,
        targetSourceFile: fileBaseName(targetSourceFile || ""),
        blockSource: fileBaseName(block?.sourceFile || block?.file || ""),
      });
    }
    el.currentTime = seek;
    el.play().then(() => setIsPlaying(true)).catch(() => {});
    setPlaybackTime(offset + seek);
  };

  const seekToAbsolute = (absSec, targetHour, targetSourceFile = "") => {
    const desiredHour = Number.isFinite(Number(targetHour)) ? Number(targetHour) : Number(selectedHour);
    const block = resolveBlockForTarget(desiredHour, targetSourceFile);
    const desiredSource = fileBaseName(block?.sourceFile || block?.file || targetSourceFile || "");
    const shouldSwitchHour = desiredHour !== Number(selectedHour);
    const shouldSwitchSource = desiredSource && desiredSource !== fileBaseName(preferredSourceFile || "");
    if (shouldSwitchHour || shouldSwitchSource) {
      pendingSeekRef.current = { absSec: Number(absSec || 0), targetHour: desiredHour, targetSourceFile: desiredSource };
      if (shouldSwitchSource) setPreferredSourceFile(desiredSource);
      setSelectedHour(desiredHour);
      return;
    }
    const el = audioRef.current;
    if (!el) return;
    if (el.readyState >= 1) {
      runAbsoluteSeek(absSec, desiredHour, desiredSource);
      return;
    }
    pendingSeekRef.current = { absSec: Number(absSec || 0), targetHour: desiredHour, targetSourceFile: desiredSource };
  };

  const playSegment = (seg, opts = {}) => {
    if (!seg) return;
    const el = audioRef.current;
    if (opts.toggleStop && activeSegment === seg.id) {
      if (!el || !audioSrc) return;
      if (isPlaying) {
        el.pause();
        setIsPlaying(false);
        return;
      }
      el.play().then(() => setIsPlaying(true)).catch(() => {});
      return;
    }
    const absStart = Number(seg?.start || 0);
    const segHour = Number.isFinite(absStart)
      ? (Math.floor(Math.max(0, absStart) / 3600) % 24)
      : Number(seg?.hour || selectedHour);
    const targetSource = fileBaseName(seg?.sourceFile || "");
    setActiveSegment(seg.id);
    if (segHour !== Number(selectedHour) || (targetSource && targetSource !== fileBaseName(preferredSourceFile || ""))) {
      pendingSeekRef.current = {
        absSec: absStart,
        targetHour: segHour,
        targetSourceFile: targetSource,
      };
      if (targetSource) setPreferredSourceFile(targetSource);
      setSelectedHour(segHour);
      return;
    }
    if (!el || !audioSrc) {
      pendingSeekRef.current = {
        absSec: absStart,
        targetHour: segHour,
        targetSourceFile: targetSource,
      };
      return;
    }
    const hasStartRel = seg?.startRel !== null && seg?.startRel !== undefined && seg?.startRel !== "";
    const startRel = hasStartRel ? Number(seg?.startRel) : NaN;
    const seekPreferred = Number.isFinite(startRel) ? Math.max(0, startRel) : NaN;
    const seekFallback = Math.max(0, absStart - fileOffsetSec);
    const seek = resolveSeekWithinDuration(seekPreferred, seekFallback, el);
    el.currentTime = seek;
    el.play().then(() => setIsPlaying(true)).catch(() => {});
    setPlaybackTime(fileOffsetSec + seek);
  };

  const moveSegment = (direction) => {
    if (!sortedHourSegments.length) return;
    let nextIndex = activeSegmentIndex;
    if (nextIndex < 0) {
      nextIndex = direction > 0 ? 0 : sortedHourSegments.length - 1;
    } else {
      nextIndex = Math.max(0, Math.min(sortedHourSegments.length - 1, nextIndex + direction));
    }
    const target = sortedHourSegments[nextIndex];
    if (target) playSegment(target);
  };

  const handleSegmentClick = (seg) => {
    playSegment(seg, { toggleStop: true });
  };

  const resolveClickableSegment = (seekTo, targetHour, targetSourceFile = "") => {
    const desiredHour = Number(targetHour);
    const byHour = SEGMENTS.filter((s) => {
      const absStart = Number(s?.start);
      if (!Number.isFinite(absStart)) return false;
      return (Math.floor(Math.max(0, absStart) / 3600) % 24) === desiredHour;
    });
    if (byHour.length === 0) {
      return null;
    }
    const desiredSource = fileBaseName(targetSourceFile || "");
    const sameSource = desiredSource
      ? byHour.filter((s) => fileBaseName(s.sourceFile || "") === desiredSource)
      : [];
    if (desiredSource && sameSource.length === 0) {
      return null;
    }
    const candidates = sameSource.length > 0 ? sameSource : byHour;

    if (Number.isFinite(Number(seekTo))) {
      const within = candidates.find((s) => s.start <= seekTo && s.end >= seekTo);
      if (within) {
        return within;
      }
      const nearest = candidates.reduce((best, cur) => {
        if (!best) return cur;
        const b = Math.abs(Number(best.start) - Number(seekTo));
        const c = Math.abs(Number(cur.start) - Number(seekTo));
        return c < b ? cur : best;
      }, null);
      if (nearest) {
        return nearest;
      }
    }

    // Fallback order:
    // 1) first segment in same source file
    // 2) first segment in same hour
    return candidates[0] || byHour[0] || null;
  };

  const parseItemClockToAbsolute = (item) => {
    const t = String(item?.time || "").trim();
    const m = t.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?/);
    if (!m) return NaN;
    const hh = Number(m[1]) % 24;
    const mm = Number(m[2]);
    const ss = Number(m[3] || 0);
    if (!Number.isFinite(hh) || !Number.isFinite(mm) || !Number.isFinite(ss)) return NaN;
    return (hh * 3600) + (mm * 60) + ss;
  };

  const resolveItemAbsoluteSeek = (item, fallbackSeg = null) => {
    const seekAbsDirect = Number(item?.seekToAbsoluteSec);
    if (Number.isFinite(seekAbsDirect) && seekAbsDirect > 0) {
      return seekAbsDirect;
    }
    const hasSeekRelDirect = item?.seekToRelativeSec !== null && item?.seekToRelativeSec !== undefined && item?.seekToRelativeSec !== "";
    const seekRelDirect = hasSeekRelDirect ? Number(item?.seekToRelativeSec) : NaN;
    if (Number.isFinite(seekRelDirect) && seekRelDirect >= 0) {
      const src = fileBaseName(item?.sourceFile || "");
      const srcBlock = src
        ? (HOUR_BLOCKS.find((hb) => fileBaseName(hb?.sourceFile || hb?.file || "") === src) || null)
        : null;
      if (srcBlock) {
        const srcHour = Number(srcBlock.hour);
        const srcOffset = Number(srcBlock.fileStartSec ?? HOUR_OFFSETS[srcHour] ?? srcHour * 3600);
        return srcOffset + seekRelDirect;
      }
    }
    let seekTo = Number(item?.seekTo);
    if (Number.isFinite(seekTo) && seekTo > 0) {
      const src = fileBaseName(item?.sourceFile || "");
      const srcBlock = src
        ? (HOUR_BLOCKS.find((hb) => fileBaseName(hb?.sourceFile || hb?.file || "") === src) || null)
        : null;

      if (srcBlock) {
        const srcHour = Number(srcBlock.hour);
        const srcOffset = Number(srcBlock.fileStartSec ?? HOUR_OFFSETS[srcHour] ?? srcHour * 3600);
        // If seekTo looks relative (small), convert to absolute using file start.
        if (seekTo < 24 * 3600 && seekTo < Math.max(3600, srcOffset - 300)) {
          seekTo = srcOffset + seekTo;
        }
      }
      return seekTo;
    }

    if (fallbackSeg && Number.isFinite(Number(fallbackSeg.start))) {
      return Number(fallbackSeg.start);
    }

    const byClock = parseItemClockToAbsolute(item);
    if (Number.isFinite(byClock)) {
      return byClock;
    }
    return NaN;
  };

  const getSummarySeekPlan = (item) => {
    const targetHour = resolveItemHour(item);
    const absGuess = resolveItemAbsoluteSeek(item, null);
    const seg = resolveClickableSegment(absGuess, targetHour, item?.sourceFile || "");
    const absSeek = resolveItemAbsoluteSeek(item, seg);
    if (Number.isFinite(absSeek)) {
      return { playable: true, mode: "absolute", targetHour, absSeek, seg, reason: "" };
    }
    if (seg) {
      return { playable: true, mode: "segment", targetHour, absSeek: NaN, seg, reason: "" };
    }
    return { playable: false, mode: "none", targetHour, absSeek: NaN, seg: null, reason: "missing_segment" };
  };

  const explainSummarySeekFailure = (reason) => {
    if (reason === "source_mismatch") return "参照元ファイルが一致しないため再生できません";
    return "対応する音声セグメントが見つからないため再生できません";
  };

  const executeSummarySeek = (item, plan) => {
    const resolved = plan || getSummarySeekPlan(item);
    if (!resolved.playable) {
      showSeekWarning(explainSummarySeekFailure(resolved.reason));
      return;
    }
    if (resolved.mode === "absolute" && Number.isFinite(resolved.absSeek)) {
      seekToAbsolute(resolved.absSeek, resolved.targetHour, item?.sourceFile || "");
      return;
    }
    if (resolved.seg) {
      handleSegmentClick(resolved.seg);
      return;
    }
    showSeekWarning("対応する音声セグメントが見つからないため再生できません");
  };

  return (
    <div style={{ display: "flex", height: "calc(100vh - 52px)", overflow: "hidden" }}>
      <audio
        ref={audioRef}
        src={audioSrc || undefined}
        onLoadedMetadata={() => {
          const pending = pendingSeekRef.current;
          if (!pending) return;
          pendingSeekRef.current = null;
          runAbsoluteSeek(pending.absSec, pending.targetHour, pending.targetSourceFile || "");
        }}
        onTimeUpdate={handleTimeUpdate}
        onPlay={() => setIsPlaying(true)}
        onPause={() => setIsPlaying(false)}
        onEnded={() => {
          setIsPlaying(false);
          if (activeSegmentIndex >= 0 && activeSegmentIndex < (sortedHourSegments.length - 1)) {
            moveSegment(1);
          }
        }}
        onError={() => {
          const el = audioRef.current;
          debugSeekLog("audio_error", {
            audioSrc,
            selectedHour,
            currentSourceFile,
            currentTime: Number(el?.currentTime),
            duration: Number(el?.duration),
            readyState: Number(el?.readyState),
            networkState: Number(el?.networkState),
            errorCode: Number(el?.error?.code),
          });
        }}
        style={{ display: "none" }}
      />


      {/* サイドバー */}
      <aside style={{ width: 210, borderRight: "1px solid #E2E8F0", padding: 14, display: "flex", flexDirection: "column", gap: 16, overflowY: "auto", background: "#fff", flexShrink: 0 }}>
        <div style={{ height: 1, background: "#F1F5F9" }} />

        <div>
          <label style={{ display: "block", fontSize: 9, fontWeight: 700, textTransform: "uppercase", letterSpacing: 1.5, color: "#94A3B8", marginBottom: 5 }}>検索</label>
          <div style={{ position: "relative" }}>
            <div style={{ position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)", color: "#94A3B8", display: "flex" }}><IconSearch /></div>
            <input value={searchQuery} onChange={e => setSearchQuery(e.target.value)} placeholder="キーワード..."
              style={{ width: "100%", padding: "7px 10px 7px 28px", borderRadius: 4, border: "1px solid #E2E8F0", background: "#F8FAFC", fontSize: 12, fontFamily: "inherit", outline: "none", color: "#1E293B" }} />
          </div>
        </div>

        <div>
          <div style={{ marginBottom: 12 }}>
              <label style={{ display: "block", fontSize: 9, fontWeight: 700, textTransform: "uppercase", letterSpacing: 1.5, color: "#94A3B8", marginBottom: 5 }}>channel</label>
              <select
                value={selectedChannel}
                onChange={e => {
                  const raw = String(e.target.value || "default");
                  const next = normalizeChannelId(raw);
                  setSelectedChannel(next);
                  setPreferredSourceFile("");
                }}
                style={{ width: "100%", padding: "7px 9px", borderRadius: 4, border: "1px solid #E2E8F0", background: "#F8FAFC", color: "#334155", fontSize: 12, fontFamily: "monospace", outline: "none" }}
              >
                {channelOptionRows.map((row) => (
                  <option key={`channel-${row.id}`} value={row.id}>{row.label}</option>
                ))}
              </select>
            </div>

          <label style={{ display: "block", fontSize: 9, fontWeight: 700, textTransform: "uppercase", letterSpacing: 1.5, color: "#94A3B8", marginBottom: 5 }}>時間帯</label>
          {hourValues.map((h) => {
            const count = SEGMENTS.filter((s) => {
              if (resolveSegmentHour(s) !== Number(h)) return false;
              return normalizeChannelId(s?.channelId) === selectedChannel;
            }).length;
            const active = Number(selectedHour) === Number(h);
            return (
              <button key={`h-${h}`} onClick={() => { setSelectedHour(h); setPreferredSourceFile(""); }}
                style={{ width: "100%", padding: "7px 10px", borderRadius: 4, border: active ? "1px solid #BFDBFE" : "1px solid #F1F5F9", background: active ? "#EFF6FF" : "#FAFBFE", color: active ? "#1D4ED8" : "#64748B", fontSize: 11, fontWeight: 600, cursor: "pointer", fontFamily: "monospace", textAlign: "left", display: "flex", justifyContent: "space-between", marginBottom: 3 }}>
                {hourLabel(h)}<span style={{ fontSize: 10, color: active ? "#60A5FA" : "#CBD5E1" }}>{count}件</span>
              </button>
            );
          })}
        </div>

      </aside>

      {/* メイン */}
      <main style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
        {/* プレーヤーバー */}
        <div style={{ padding: "8px 18px", borderBottom: "1px solid #E2E8F0", display: "flex", alignItems: "center", gap: 12, background: "#fff", flexShrink: 0 }}>
          <button onClick={() => nudgePlayback(-10)}
            style={{ height: 28, borderRadius: 999, border: "1px solid #CBD5E1", background: "#fff", color: "#334155", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, fontSize: 11, padding: "0 8px", fontFamily: "monospace" }}>
            -10s
          </button>
          <button onClick={() => moveSegment(-1)}
            disabled={activeSegmentIndex <= 0}
            style={{ width: 28, height: 28, borderRadius: "50%", border: "1px solid #CBD5E1", background: "#fff", color: "#334155", cursor: activeSegmentIndex <= 0 ? "not-allowed" : "pointer", opacity: activeSegmentIndex <= 0 ? 0.35 : 1, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, fontSize: 12 }}>
            {"<<"}
          </button>
          <button onClick={togglePlay}
            style={{ width: 30, height: 30, borderRadius: "50%", border: "none", background: isPlaying ? "#2563EB" : "#EFF6FF", color: isPlaying ? "#fff" : "#2563EB", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
            {isPlaying ? <IconPause /> : <IconPlay />}
          </button>
          <button onClick={() => moveSegment(1)}
            disabled={sortedHourSegments.length === 0 || activeSegmentIndex >= sortedHourSegments.length - 1}
            style={{ width: 28, height: 28, borderRadius: "50%", border: "1px solid #CBD5E1", background: "#fff", color: "#334155", cursor: (sortedHourSegments.length === 0 || activeSegmentIndex >= sortedHourSegments.length - 1) ? "not-allowed" : "pointer", opacity: (sortedHourSegments.length === 0 || activeSegmentIndex >= sortedHourSegments.length - 1) ? 0.35 : 1, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, fontSize: 12 }}>
            {">>"}
          </button>
          <button onClick={() => nudgePlayback(10)}
            style={{ height: 28, borderRadius: 999, border: "1px solid #CBD5E1", background: "#fff", color: "#334155", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, fontSize: 11, padding: "0 8px", fontFamily: "monospace" }}>
            +10s
          </button>
          <div style={{ fontFamily: "monospace", fontSize: 12, color: "#334155", width: 44, flexShrink: 0, fontWeight: 600 }}>
            {fmt(toRelativeSec(playbackTime))}
          </div>
          <div style={{ flex: 1, position: "relative", height: 40, background: "#F1F5F9", borderRadius: 4, overflow: "hidden", border: "1px solid #E2E8F0" }}>
            {SEGMENTS.filter((s) => matchesSelectedHour(s)).map(seg => {
              const left = (toRelativeSec(seg.start) / 3600) * 100;
              const width = Math.max(((toRelativeSec(seg.end) - toRelativeSec(seg.start)) / 3600) * 100, 0.4);
              const isActive = seg.id === activeSegment;
              return (
                <div key={seg.id} onClick={() => handleSegmentClick(seg)}
                  style={{ position: "absolute", left: `${left}%`, width: `${width}%`, top: 10, height: 20, background: isActive ? "#6366F1" : "#6366F170", borderRadius: 2, cursor: "pointer", transition: "all 0.15s", border: isActive ? "1px solid #6366F1" : "none" }} />
              );
            })}
            <div style={{ position: "absolute", left: `${(toRelativeSec(playbackTime) / 3600) * 100}%`, top: 0, bottom: 0, width: 2, background: "#2563EB", zIndex: 10 }} />
          </div>
          <div style={{ fontSize: 10, color: "#94A3B8", fontFamily: "monospace", flexShrink: 0 }}>{currentBlock?.file}</div>
        </div>
        {seekWarning ? (
          <div style={{ padding: "6px 18px", background: "#FEF2F2", borderBottom: "1px solid #FECACA", color: "#991B1B", fontSize: 12 }}>
            {seekWarning}
          </div>
        ) : null}
        {hasMultiSourceInHour ? (
          <div style={{ padding: "6px 18px", background: "#FFFBEB", borderBottom: "1px solid #FDE68A", color: "#92400E", fontSize: 12 }}>
            Multiple source files detected in this hour ({knownHourSourceStats.length} files).
            <div style={{ marginTop: 6, fontSize: 11, color: "#A16207" }}>
              Recommended: keep only the latest sourceFile for this hour, then re-analyze for stable counts and summaries.
            </div>
            <details style={{ marginTop: 6 }}>
              <summary style={{ cursor: "pointer", fontSize: 11, color: "#92400E" }}>Show sourceFile breakdown</summary>
              <ul style={{ margin: "6px 0 0 18px", padding: 0, color: "#78350F", fontSize: 11 }}>
                {knownHourSourceStats.slice(0, 10).map((row) => (
                  <li key={row.source} style={{ marginBottom: 2 }}>{row.source} ({row.count} segments)</li>
                ))}
                {knownHourSourceStats.length > 10 ? (
                  <li key="more">... and {knownHourSourceStats.length - 10} more</li>
                ) : null}
              </ul>
            </details>
          </div>
        ) : null}

        {/* セグメント */}
        <div style={{ flex: 1, minHeight: 0, background: "#fff", display: "flex", flexDirection: "column" }}>
          {/* 概要：D案 背景なし・左1pxインジゴライン */}
          <div style={{ padding: "14px 14px 10px", borderBottom: "1px solid #EEF2FF", flexShrink: 0 }}>
            <section style={{ border: "1px solid #C7D2FE", borderRadius: 12, background: "#F8FAFF", padding: "12px 14px" }}>
              <div style={{ marginTop: 2, marginBottom: 4, fontSize: 11, fontWeight: 800, color: "#4F46E5" }}>{"\u767a\u751f\u4e8b\u9805"}</div>
              {visibleIssues.length === 0 ? (
                <div style={{ fontSize: 12, color: "#94A3B8", padding: "8px 2px" }}>{"\u3053\u306e\u6642\u9593\u5e2f\u306e\u767a\u751f\u4e8b\u9805\u306f\u3042\u308a\u307e\u305b\u3093"}</div>
              ) : visibleIssues.map((it, i) => {
                const plan = getSummarySeekPlan(it);
                return (
                  <div key={`issue-${i}`} onClick={() => executeSummarySeek(it, plan)}
                    style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 4px", borderBottom: i < visibleIssues.length - 1 ? "1px solid #F1F5F9" : "none", cursor: plan.playable ? "pointer" : "not-allowed", opacity: plan.playable ? 1 : 0.72 }}
                    onMouseEnter={e => { if (plan.playable) e.currentTarget.style.background = "#FFF5F5"; }}
                    onMouseLeave={e => { e.currentTarget.style.background = "transparent"; }}
                  >
                    <span style={{ fontSize: 10, color: "#DC2626", fontWeight: 700, flexShrink: 0, padding: "2px 8px", borderRadius: 999, border: "1px solid #FECACA", background: "#FEF2F2" }}>{it.type || it.severity || "発生事項"}</span>
                    <span style={{ fontSize: 12, color: "#334155", flex: 1 }}>{it.summary || ""}</span>
                    <span style={{ color: plan.playable ? "#F87171" : "#CBD5E1", display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}><IconLink size={12} /></span>
                  </div>
                );
              })}

              <div style={{ marginTop: 10, fontSize: 11, fontWeight: 800, color: "#4F46E5", marginBottom: 6 }}>{"\u4e3b\u8981\u306a\u5224\u65ad"}</div>
              {visibleDecisions.length === 0 ? (
                <div style={{ fontSize: 12, color: "#94A3B8", padding: "8px 2px" }}>{"\u3053\u306e\u6642\u9593\u5e2f\u306e\u4e3b\u8981\u306a\u5224\u65ad\u306f\u3042\u308a\u307e\u305b\u3093"}</div>
              ) : visibleDecisions.map((d, i) => {
                const plan = getSummarySeekPlan(d);
                return (
                  <div key={i} onClick={() => executeSummarySeek(d, plan)}
                    style={{ display: "flex", alignItems: "center", gap: 10, padding: "7px 6px", borderBottom: "1px solid #E5E7EB", cursor: plan.playable ? "pointer" : "not-allowed", opacity: plan.playable ? 1 : 0.72 }}
                    onMouseEnter={e => { if (plan.playable) e.currentTarget.style.background = "#EEF2FF"; }}
                    onMouseLeave={e => { e.currentTarget.style.background = "transparent"; }}
                  >
                    <span style={{ fontFamily: "monospace", fontSize: 12, color: "#4F46E5", fontWeight: 700, width: 40, flexShrink: 0 }}>{formatDecisionTime(d)}</span>
                    <span style={{ fontSize: 12, color: "#334155", flex: 1 }}>{d.summary || ""}</span>
                    <span style={{ color: plan.playable ? "#818CF8" : "#CBD5E1", display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}><IconLink size={12} /></span>
                  </div>
                );
              })}
            </section>
          </div>

          <div style={{ padding: "14px 20px 10px", flexShrink: 0 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
              <div style={{ fontSize: 10, fontWeight: 700, color: "#94A3B8", textTransform: "uppercase", letterSpacing: 1.4, whiteSpace: "nowrap" }}>無線ログ</div>
              <div style={{ flex: 1, height: 1, background: "#E2E8F0" }} />
              <div style={{ fontSize: 10, color: "#CBD5E1", fontFamily: "monospace", whiteSpace: "nowrap" }}>
                {filteredSegments.length}件{searchQuery && ` · "${searchQuery}"`}
              </div>
            </div>
          </div>

          {/* 無線ログ一覧（ここだけスクロール） */}
          <div ref={scrollRef} style={{ flex: 1, minHeight: 0, overflowY: "auto", padding: "0 20px 14px" }}>
          <div style={{ display: "flex", flexDirection: "column", gap: 1 }}>
            {filteredSegments.map(seg => {
              const isActive = seg.id === activeSegment;
              let textContent = seg.text;
              if (searchQuery.trim()) {
                const regex = new RegExp(`(${searchQuery.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
                const parts = textContent.split(regex);
                textContent = parts.map((p, i) => regex.test(p) ? <mark key={i} style={{ background: "#FDE68A", color: "#92400E", borderRadius: 2, padding: "0 2px" }}>{p}</mark> : p);
              }
              return (
                <div key={seg.id} ref={el => segmentRefs.current[seg.id] = el}
                  onClick={() => handleSegmentClick(seg)}
                  style={{ display: "flex", gap: 10, padding: "10px 12px", borderRadius: 4, cursor: "pointer", transition: "all 0.12s", background: isActive ? "#EEF2FF" : "transparent", borderLeft: `2px solid ${isActive ? "#6366F1" : "transparent"}` }}
                  onMouseEnter={e => { if (!isActive) e.currentTarget.style.background = "#F8FAFC"; }}
                  onMouseLeave={e => { if (!isActive) e.currentTarget.style.background = "transparent"; }}
                >
                  <div style={{ flexShrink: 0, width: 44, textAlign: "right", paddingTop: 2 }}>
                    <span style={{ fontFamily: "monospace", fontSize: 10, color: "#94A3B8" }}>{fmtHourMinute(seg.start)}</span>
                  </div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 13, lineHeight: 1.75, color: "#1E293B" }}>{textContent}</div>
                    {isActive && (
                      <div style={{ marginTop: 4, display: "flex", alignItems: "center", gap: 5 }}>
                        <span style={{ color: "#6366F1", opacity: 0.7, display: "flex" }}><IconWave /></span>
                        <span style={{ fontSize: 10, color: "#6366F1", fontFamily: "monospace" }}>{fmtHourMinute(seg.start)} – {fmtHourMinute(seg.end)}</span>
                      </div>
                    )}
                  </div>
                </div>
              );
            })}
          </div>
          </div>
        </div>
      </main>
    </div>
  );
}

// ─── App ───
function App() {
  const [page, setPage] = useState({ view: "dashboard" });
  const [selectedEvent, setSelectedEvent] = useState(EVENTS[0]?.id || "");
  const [selectedDate, setSelectedDate] = useState(EVENTS[0]?.dates?.[0] || "");
  const [initialHourParam, setInitialHourParam] = useState(null);
  const [initialFileParam, setInitialFileParam] = useState("");
  const [initialChannelParam, setInitialChannelParam] = useState("");
  const [dataVersion, setDataVersion] = useState(0);

  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const qEvent = params.get("event_id");
    const qDate = params.get("date");
    const qHour = params.get("hour");
    const qFile = params.get("file");
    const qChannel = params.get("channel");
    if (qEvent) setSelectedEvent(qEvent);
    if (qDate) setSelectedDate(qDate);
    if (qHour !== null && qHour !== "") setInitialHourParam(Number(qHour));
    if (qFile) setInitialFileParam(qFile);
    if (qChannel) setInitialChannelParam(qChannel);
    if (qEvent || qDate || qHour || qFile || qChannel) {
      setPage({ view: "viewer" });
    }
  }, []);

  useEffect(() => {
    fetch("/api/events")
      .then((r) => r.json())
      .then((payload) => {
        const list = Array.isArray(payload) ? payload : (payload?.value || []);
        if (!Array.isArray(list) || list.length === 0) return;
        const baseEvents = list.map((e) => ({
          ...e,
          status: e.status || "completed",
          totalSegments: Number(e.totalSegments || 0),
          totalIssues: Number(e.totalIssues || 0),
        }));
        Promise.all(
          baseEvents.map(async (e) => {
            const dates = Array.isArray(e.dates) ? e.dates : [];
            let segmentsCount = 0;
            let issuesCount = 0;
            for (const d of dates) {
              try {
                const snapRes = await fetch(`/api/events/${encodeURIComponent(e.id)}/${encodeURIComponent(d)}`);
                if (!snapRes.ok) continue;
                const snap = await snapRes.json();
                segmentsCount += Array.isArray(snap?.segments) ? snap.segments.length : 0;
                const issues = snap?.summary?.issues;
                issuesCount += Array.isArray(issues) ? issues.length : 0;
              } catch (_err) {
                // continue
              }
            }
            return {
              ...e,
              totalSegments: e.totalSegments > 0 ? e.totalSegments : segmentsCount,
              totalIssues: e.totalIssues > 0 ? e.totalIssues : issuesCount,
            };
          })
        )
          .then((enrichedEvents) => {
            EVENTS = enrichedEvents;
            const first = EVENTS[0];
            const exists = EVENTS.some((ev) => ev.id === selectedEvent);
            if (!exists && first?.id) setSelectedEvent(first.id);
            const targetEvent = EVENTS.find((ev) => ev.id === selectedEvent) || first;
            const dateExists = (targetEvent?.dates || []).includes(selectedDate);
            if (!dateExists && targetEvent?.dates?.[0]) setSelectedDate(targetEvent.dates[0]);
            setDataVersion((v) => v + 1);
          })
          .catch(() => {});
      })
      .catch(() => {});
  }, []);

  useEffect(() => {
    if (!selectedEvent || !selectedDate) return;
    fetch(`/api/events/${selectedEvent}/${selectedDate}`)
      .then((r) => {
        if (!r.ok) throw new Error(`snapshot fetch failed: ${r.status}`);
        return r.json();
      })
      .then((snapshot) => {
        applySnapshot(snapshot);
        setDataVersion((v) => v + 1);
      })
      .catch(() => {});
  }, [selectedEvent, selectedDate]);

  // イベント切り替え時にpageも更新
  const handleSetPage = (newPage) => {
    if (newPage.event) {
      setSelectedEvent(newPage.event.id);
      if (newPage.date) setSelectedDate(newPage.date);
    }
    setPage(newPage);
  };

  const currentEvent = EVENTS.find(e => e.id === selectedEvent);

  return (
    <div style={{ fontFamily: "'Noto Sans JP', -apple-system, sans-serif", background: "#FBFCFE", color: "#1E293B", minHeight: "100vh" }}>
      <style>{`
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;600;700;800&display=swap');
        @keyframes fadeIn { from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:translateY(0)} }
        @keyframes blink { 0%,100%{opacity:1}50%{opacity:0.3} }
        * { box-sizing:border-box; margin:0; padding:0; }
        ::-webkit-scrollbar{width:4px}
        ::-webkit-scrollbar-thumb{background:#CBD5E1;border-radius:2px}
        button{font-family:inherit}
      `}</style>
      <Header
        page={page}
        setPage={handleSetPage}
        selectedEvent={selectedEvent}
        setSelectedEvent={setSelectedEvent}
        selectedDate={selectedDate}
        setSelectedDate={setSelectedDate}
      />
      <div style={{ animation: "fadeIn 0.15s" }}>
        {page.view === "dashboard" && <Dashboard setPage={handleSetPage} />}
        {page.view === "live" && <Live event={currentEvent} setPage={handleSetPage} />}
        {page.view === "report" && <Report event={currentEvent} setPage={handleSetPage} selectedDate={selectedDate} setSelectedDate={setSelectedDate} />}
        {page.view === "viewer" && (
          <Viewer
            event={currentEvent}
            date={selectedDate !== "all" ? selectedDate : currentEvent?.dates[0]}
            setPage={handleSetPage}
            initialHour={initialHourParam}
            initialFile={initialFileParam}
            initialChannel={initialChannelParam}
            dataVersion={dataVersion}
          />
        )}
      </div>
    </div>
  );
}


const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
