/* race-card.jsx — 発艇表ビッグカード（紺地×白の高コントラスト全画面）。
   2モード：
   ・raceId … 1レース（発艇時刻ドン＋エントリー）。日程/レーダーから。
   ・eventKey … 1種目（予選〜A決勝のラウンド一覧＋エントリー）。種目タブから。
   結果・タイム・レーンは静岡県ローイング協会の公式結果をそのまま出す（こちらで作らない）。
   高校生の大会なので、氏名は公式結果に載っているシングルスカルだけを文字で出す。
   クルー種目の選手名・名簿・プロフィールは扱わない（そのデータを置かない設計）。 */

// 公式結果（RW.realRaces / RW.realRacesFor）をラウンド別にまとめる補助。
// この大会のラウンドは 予選 → 敗者戦 → 準決 → B決勝（7〜12位）／A決勝（1〜6位）の5つ。
const KARAL_ROUND_ORDER = ["予選", "敗者戦", "準決", "B決勝", "A決勝", "オープン", "レース"];
const karalRoundLabel = (r) =>
  r === "敗復" ? "敗者戦"
    : r === "決勝" ? "A決勝"
      : (r === "順位決定" || r === "順決") ? "B決勝" : r;
function groupKaralByRound(races) {
  const byRound = {};
  (races || []).forEach((r) => { const l = karalRoundLabel(r.round); (byRound[l] = byRound[l] || []).push(r); });
  return Object.keys(byRound)
    .sort((a, b) => KARAL_ROUND_ORDER.indexOf(a) - KARAL_ROUND_ORDER.indexOf(b))
    .map((round) => ({ round, races: byRound[round] }));
}

// 進出条件チップ。タップすると「各組の上位3艇が準決勝へ…」の解説が開く
function ProgChip({ evKey, round }) {
  const [open, setOpen] = React.useState(false);
  const pg = (evKey && window.KANSAI.PROG_OF) ? window.KANSAI.PROG_OF(evKey, round) : null;
  if (!pg) return null;
  return (
    <React.Fragment>
      <button type="button" className={"rc-progchip" + (open ? " on" : "")}
              onClick={(e) => { e.stopPropagation(); setOpen((o) => !o); }}>{pg.chip}</button>
      {open && <span className="rc-prognote">{pg.note}</span>}
    </React.Fragment>
  );
}

// 1組ぶんの結果（または発艇前の組み合わせ）。行をタップすると、その学校のページへ。
// エントリー番号 → 登録メンバー（rosters.js）。クルー種目の名前を出すために使う。
// 公式結果には氏名が無いので、大会エントリー（クルー登録）から引いている。
// シート順は分からず、補漕（控え）も含む — 表示のときは必ずその旨を添える。
function crewMembers(evKey, team) {
  const RW = window.RW;
  const ros = (window.KANSAI.ROSTERS || {})[evKey];
  if (!ros || !team) return null;
  const ev = RW.EVENTS.find((e) => e.key === evKey);
  if (!ev) return null;
  const nrm = (s) => String(s || "").replace(/[\s　]/g, "");
  const base = RW.baseTeam(team);
  const en = ev.entries.find((x) => nrm(x.team) === nrm(team))
    || ev.entries.find((x) => RW.baseTeam(x.team) === base);
  const list = en ? ros[String(en.n)] : null;
  return (list && list.length) ? list : null;
}

// 登録メンバーの1行（クルー種目のときだけ開く）
function CrewMemberList({ members }) {
  const rowers = members.filter((m) => !m[1]);
  const spares = members.filter((m) => m[1]);
  return (
    <div className="rc-crew">
      <div className="rc-crewnames">
        {rowers.map((m, i) => <span className="rc-cm" key={i}>{m[0]}</span>)}
        {spares.map((m, i) => <span className="rc-cm sp" key={"s" + i}>{m[0]}<i>補漕</i></span>)}
      </div>
      <div className="rc-crewnote">大会エントリーの登録メンバーです。並び順は乗艇順ではありません。</div>
    </div>
  );
}

function KaralLaneRows({ races, favTeams, onOpenTeam, cat, evKey }) {
  const RW = window.RW;
  const [openKey, setOpenKey] = React.useState(null);
  return races.map((kr, gi) => {
    const lanes = kr.lanes.slice().sort((a, b) => {
      if (a.rank == null && b.rank == null) return (a.lane || 0) - (b.lane || 0);
      if (a.rank == null) return 1;
      if (b.rank == null) return -1;
      return a.rank - b.rank;
    });
    return (
      <div className="rc-res-grp" key={gi}>
        <div className="rc-res-grphd">{kr.grp || ""}{kr.time ? "　" + kr.time : ""}{kr.raceNo ? <b className="rc-raceno">レースNo.{kr.raceNo}</b> : null}<ProgChip evKey={evKey} round={kr.round} /></div>
        {lanes.map((l, i) => {
          const isFav = !!(favTeams && l.team && favTeams[RW.baseTeam(l.team)]);
          const num = l.rank != null ? l.rank : (l.status || (l.lane != null ? l.lane : "―"));
          // 公式結果に氏名が載るのはシングルスカルだけ。クルー種目は下の登録メンバーで出す。
          const showName = !!l.name;
          // 進出先（→準決勝 など）だけを出す。DNS・棄権は左の順位欄に出るので重ねて出さない。
          const qual = l.qualify && /^→/.test(l.qualify) ? l.qualify : null;
          // クルー種目は行をタップで登録メンバーを開く。シングルは氏名が行に出ているので開かない。
          const members = showName ? null : crewMembers(evKey, l.team);
          const rowKey = gi + "-" + i;
          const open = openKey === rowKey;
          const canOpen = !!(l.team && (members || onOpenTeam));
          return (
            <React.Fragment key={i}>
              <div className={"rc-row" + (canOpen ? " clk" : "") + (cat === "w" ? " w" : "") + (isFav ? " fav" : "")}
                   onClick={canOpen ? () => (members ? setOpenKey(open ? null : rowKey) : onOpenTeam(l.team)) : undefined}>
                <span className="rc-n">{num}</span>
                <span className="rc-ident">
                  <span className="rc-team">{showName ? l.name : (l.team || "")}{isFav && <Star size={11} color="var(--gold-2)" />}</span>
                  {showName && <span className="rc-sub">{l.team}</span>}
                </span>
                <span className="rc-res-t">{l.t2000 || ""}</span>
                {qual && <span className="rc-res-q">{qual}</span>}
                {canOpen && (members ? <IconChevron dir={open ? "up" : "down"} size={12} /> : <IconArrow size={12} />)}
              </div>
              {open && members && (
                <React.Fragment>
                  <CrewMemberList members={members} />
                  {onOpenTeam && <button type="button" className="rc-teambtn" onClick={() => onOpenTeam(l.team)}>この学校のページへ →</button>}
                </React.Fragment>
              )}
            </React.Fragment>
          );
        })}
      </div>
    );
  });
}

// ペース比較（ヒートマップ／レース再現）用の補助。
// 公式結果は 500m / 1000m / 1500m / 2000m の4つの通過タイムを載せている。
// ただし計測できていない区間は空欄で来ることがあるので、欠測は前提として扱う。
function karalTimeToSec(t) {
  const m = String(t || "").match(/(\d+):(\d{2}(?:\.\d+)?)/);
  return m ? (+m[1]) * 60 + parseFloat(m[2]) : null;
}
function karalSecToTime(x) {
  if (x == null) return "";
  const mm = Math.floor(x / 60), ss = x - mm * 60;
  return mm + ":" + ss.toFixed(2).padStart(5, "0");
}
function karalLapStr(x) { const mm = Math.floor(x / 60); return mm + ":" + (x - mm * 60).toFixed(1).padStart(4, "0"); }
function karalPaceColor(frac) {
  const a = [40, 116, 240], b = [255, 72, 88];
  const c = a.map((v, i) => Math.round(v + (b[i] - v) * frac));
  return "rgba(" + c[0] + "," + c[1] + "," + c[2] + ",0.28)";
}
// 距離0mを起点に、実測できている通過点だけを繋いだ折れ線（区間欠測は前後の実測点から補間する）
function karalWaypoints(cum) {
  const wp = [{ d: 0, t: 0 }];
  for (let i = 0; i < 4; i++) { if (cum[i] != null) wp.push({ d: (i + 1) * 500, t: cum[i] }); }
  return wp;
}
function karalDistAt(wp, tt) {
  const last = wp[wp.length - 1];
  if (tt <= 0) return 0;
  if (tt >= last.t) return last.d;
  for (let i = 1; i < wp.length; i++) {
    if (tt < wp[i].t) { const a = wp[i - 1], b = wp[i]; return a.d + (tt - a.t) / (b.t - a.t) * (b.d - a.d); }
  }
  return last.d;
}
function karalTimeAtDist(wp, d) {
  const last = wp[wp.length - 1];
  if (d <= 0) return 0;
  if (d >= last.d) return last.t;
  for (let i = 1; i < wp.length; i++) {
    if (d < wp[i].d) { const a = wp[i - 1], b = wp[i]; return a.t + (d - a.d) / (b.d - a.d) * (b.t - a.t); }
  }
  return last.t;
}
// 種目の全レースから、2000mのゴールタイムがある艇だけを取り出す（DNS/DNF等は除外）
const KARAL_RND_ORD = { "予選": 0, "敗者戦": 1, "準決": 2, "B決勝": 3, "A決勝": 4 };
function karalPaceRows(karalList) {
  const out = [];
  const zen = (s) => String(s || "").replace(/[Ａ-Ｚａ-ｚ０-９]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xFEE0));
  const nrm = (s) => String(s || "").replace(/[\s　]/g, "");
  // ラウンドごとの出場クルー集合（学校＋氏名）。「後のラウンドの発艇表に載っている＝進出」を判定するため。
  const inRound = {};
  (karalList || []).forEach((race) => {
    const R = karalRoundLabel(race.round);
    (race.lanes || []).forEach((l) => { if (l.team) (inRound[R] = inRound[R] || new Set()).add(nrm(l.team) + "|" + nrm(l.name)); });
  });
  const destOf = (R, l) => {
    const q = zen(l.qualify);
    if (/DNS|DNF|棄権|失格/.test(q)) return null;
    if (/A決勝/.test(q)) return "A決勝";
    if (/B決勝|順位/.test(q)) return "B決勝";
    if (/準決/.test(q)) return "準決";
    if (/敗者|敗復/.test(q)) return "敗者戦";
    // 進出先の記載が無い行は、後のラウンドの発艇表に載っているかで補う
    const key = nrm(l.team) + "|" + nrm(l.name);
    let best = null;
    Object.keys(inRound).forEach((R2) => {
      if ((KARAL_RND_ORD[R2] ?? 9) > (KARAL_RND_ORD[R] ?? 9) && inRound[R2].has(key)) {
        if (best == null || (KARAL_RND_ORD[R2] ?? 9) < (KARAL_RND_ORD[best] ?? 9)) best = R2;
      }
    });
    return best;
  };
  (karalList || []).forEach((race) => {
    const round = karalRoundLabel(race.round);
    (race.lanes || []).forEach((l) => {
      const cum = [karalTimeToSec(l.t500), karalTimeToSec(l.t1000), karalTimeToSec(l.t1500), karalTimeToSec(l.t2000)];
      if (cum[3] == null) return;
      const laps = [0, 1, 2, 3].map((i) => (cum[i] != null && (i === 0 || cum[i - 1] != null)) ? cum[i] - (i === 0 ? 0 : cum[i - 1]) : null);
      out.push({
        name: l.name, team: l.team, heat: race.grp || round, round, next: destOf(round, l), rank: l.rank, lane: l.lane,
        cum, laps, total: cum[3], wp: karalWaypoints(cum),
      });
    });
  });
  return out;
}

// 全組横断ペース・ヒートマップ（500mごとの速さを色で。欠測区間は「－」）
function KaralPaceHeatmap({ rows }) {
  if (!rows || !rows.length) return null;
  const labels = ["0–500", "500–1000", "1000–1500", "1500–2000"];
  const colMM = [0, 1, 2, 3].map((i) => {
    const v = rows.map((r) => r.laps[i]).filter((x) => x != null);
    return v.length ? [Math.min(...v), Math.max(...v)] : [0, 0];
  });
  const tMM = [Math.min(...rows.map((r) => r.total)), Math.max(...rows.map((r) => r.total))];
  const frac = (x, mn, mx) => (mx > mn ? (x - mn) / (mx - mn) : 0);
  const sorted = [...rows].sort((a, b) => a.total - b.total);
  return (
    <div className="pcwrap">
      <div className="pchint">500mごとのタイムを色分け（<b style={{ color: "#2b8cff" }}>青＝速い</b>／<b style={{ color: "#ff4858" }}>赤＝遅い</b>、同じ区間の中で比べています）。計測できていない区間は「－」。左が青で右が赤なら、前半が速く後半で落ちたということ。</div>
      <div className="hmscroll">
        <div className="hmtable">
          <div className="hmrow hmhead"><span className="hmnm" />{labels.map((l, i) => <span className="hmc" key={i}>{l}</span>)}<span className="hmtot">合計</span></div>
          {sorted.map((r, ri) => (
            <div className="hmrow" key={ri}>
              <span className="hmnm"><b>{r.name || r.team}</b>{r.name && <i className="hmteam">{r.team}</i>}<span className="hmsub">{r.heat}{r.rank ? "・" + r.rank + "着" : ""}</span></span>
              {r.laps.map((lp, i) => (
                <span className="hmc" key={i} style={lp != null ? { background: karalPaceColor(frac(lp, colMM[i][0], colMM[i][1])) } : undefined}>{lp != null ? karalLapStr(lp) : "－"}</span>
              ))}
              <span className="hmtot" style={{ background: karalPaceColor(frac(r.total, tMM[0], tMM[1])) }}>{karalSecToTime(r.total)}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// レース再現（ヨーイドン）：2000mのゴールタイムがある艇を、一斉スタートに見立てて左→右へ進める
// 欠測区間があっても前後の実測通過点から距離を補間するので、500m/1000m/1500mのどれが欠けていても動く
function KaralRaceReplay({ rows, initialRound, lockRound, favTeams }) {
  const all = (rows || []).filter((r) => r.total != null);
  // ラウンド（予選/敗者戦/準決/…）で分ける。「全組」は同じラウンド内の全組だけ（予選と決勝を混ぜて一斉スタートしない）
  // initialRound＝親（種目カードのラウンドタブ）から指定された初期ラウンド。lockRound時は内部のラウンド切替を出さない。
  const rounds = [...new Set(all.map((r) => r.round))].sort((a, b) => (KARAL_RND_ORD[a] ?? 9) - (KARAL_RND_ORD[b] ?? 9));
  const [roundSel, setRoundSel] = React.useState(initialRound || null);
  const curRound = (roundSel && rounds.indexOf(roundSel) >= 0) ? roundSel : rounds[rounds.length - 1]; // 既定＝最新ラウンド
  const inRound = all.filter((r) => r.round === curRound);
  const heats = [...new Set(inRound.map((r) => r.heat))];
  const [heatSel, setHeatSel] = React.useState(null);
  const curHeat = (heatSel && heats.indexOf(heatSel) >= 0) ? heatSel : null;
  const [t, setT] = React.useState(0);
  const [playing, setPlaying] = React.useState(true);
  const [speed, setSpeed] = React.useState(4);   // 既定4倍（本人指示）
  const PALETTE = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#a855f7", "#06b6d4"];
  const crews = (curHeat ? inRound.filter((r) => r.heat === curHeat) : inRound).slice().sort((a, b) => a.total - b.total);
  const maxT = crews.length ? Math.max(...crews.map((c) => c.total)) : 0;
  const singleHeat = heats.length <= 1;
  const heatColor = (h) => PALETTE[Math.max(0, heats.indexOf(h)) % PALETTE.length];
  const colorOf = (c, idx) => ((curHeat || singleHeat) ? PALETTE[idx % PALETTE.length] : heatColor(c.heat));
  // 開始時のみ約1秒スタートラインで静止（ヨーイ…ドン）。組/ラウンド切替でリセット。
  const startHold = React.useRef(1);
  React.useEffect(() => { startHold.current = 1; }, [curRound, curHeat]);
  React.useEffect(() => {
    if (!playing) return;
    let raf, last = null;
    const step = (ts) => {
      if (last == null) last = ts;
      const dt = (ts - last) / 1000; last = ts;
      if (startHold.current > 0) { startHold.current -= dt; raf = requestAnimationFrame(step); return; } // ヨーイ…（1秒静止）
      setT((prev) => { const nx = prev + dt * speed * 18; if (nx >= maxT) { setPlaying(false); return maxT; } return nx; });
      raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [playing, speed, maxT]);
  if (!crews.length) return null;
  const lead = Math.max(...crews.map((c) => karalDistAt(c.wp, t)));
  const xPct = (d) => 5 + (d / 2000) * 93; // 左=スタート(5%)→右=ゴール(98%)
  const ROWH = 23;
  const order = crews.map((c, i) => ({ i, d: karalDistAt(c.wp, t) }))
    .sort((a, b) => (b.d - a.d) || (crews[a.i].total - crews[b.i].total));
  const rankOf = {}; order.forEach((o, pos) => { rankOf[o.i] = pos; });
  const leadI = order[0].i;
  const leaderWp = crews[leadI].wp;
  const bowOrder = crews.map((c, i) => ({ i, b: +c.lane || 99 })).sort((a, b) => a.b - b.b);
  const bowPos = {}; bowOrder.forEach((o, p) => { bowPos[o.i] = p; });
  return (
    <div className="pcwrap">
      <div className="pchint">{curHeat
        ? (<>{RW.roundText(curRound)}{curHeat !== curRound ? " " + curHeat : ""}の実際のレース。行は<b>レーン番号順</b>に固定。左＝スタート→右＝ゴール。</>)
        : (<>{RW.roundText(curRound)}の全組を「一斉スタート」に見立てた再現。左＝スタート→右＝ゴール。<b>順位が変わると行も入れ替わります</b>。ゴール後は<b>進んだ先</b>（→準決 など）を表示。</>)}</div>
      {!lockRound && rounds.length > 1 && (
        <div className="rrheats rrrounds">
          {rounds.map((rd) => <button key={rd} className={"rrhb" + (curRound === rd ? " on" : "")} onClick={() => { setRoundSel(rd); setHeatSel(null); setT(0); setPlaying(true); }}>{rd}</button>)}
        </div>
      )}
      {heats.length > 1 && (
        <div className="rrheats">
          <button className={"rrhb" + (curHeat === null ? " on" : "")} onClick={() => { setHeatSel(null); setT(0); setPlaying(true); }}>全組</button>
          {heats.map((h) => <button key={h} className={"rrhb" + (curHeat === h ? " on" : "")} onClick={() => { setHeatSel(h); setT(0); setPlaying(true); }}>{h}</button>)}
        </div>
      )}
      {!curHeat && heats.length > 1 && <div className="rrlegend">{heats.map((h) => <span className="rrlg" key={h}><i style={{ background: heatColor(h) }} />{h}</span>)}</div>}
      <div className="rrctrl">
        <button className="rrplay" onClick={() => { if (t >= maxT) setT(0); setPlaying((p) => !p); }}>{playing ? "⏸" : "▶"}</button>
        <input className="rrscrub" type="range" min="0" max={maxT || 1} step="0.05" value={t} onChange={(e) => { setPlaying(false); setT(+e.target.value); }} />
        <span className="rrtime">{karalSecToTime(t)}</span>
        {[2, 4, 8].map((s) => <button key={s} className={"rrspd" + (speed === s ? " on" : "")} onClick={() => setSpeed(s)}>{s}X</button>)}
      </div>
      <div className="rrtrack" style={{ height: 16 + crews.length * ROWH }}>
        <div className="rrgrid">
          {[0, 500, 1000, 1500, 2000].map((d) => <span className={"rrgl" + (d === 2000 ? " goal" : "") + (d === 0 ? " start" : "")} key={d} style={{ left: xPct(d) + "%" }}><i>{d === 2000 ? "GOAL" : d === 0 ? "START" : d}</i></span>)}
          <span className="rrlead" style={{ left: xPct(lead) + "%" }} />
        </div>
        {crews.map((c, i) => {
          const di = karalDistAt(c.wp, t); const x = xPct(di);
          const fin = t >= c.total;
          const pos = curHeat ? bowPos[i] : rankOf[i];
          const isLead = i === leadI;
          const gap = isLead ? 0 : (t - karalTimeAtDist(leaderWp, di));
          const col = colorOf(c, i);
          // お気に入りは金色の帯＋★＋艇の光で一目で追えるように（本人指示）
          const isFav = !!(favTeams && c.team && window.RW && favTeams[window.RW.baseTeam(c.team)]);
          return (
            <div className={"rrlane" + (isFav ? " fav" : "")} key={i} style={{ top: 16 + pos * ROWH, zIndex: isFav ? 120 : 100 - pos }}>
              <span className={"rrrank" + (isLead ? " top" : "")}>{curHeat ? (c.lane || "") : pos + 1}</span>
              <span className="rrnm" style={{ color: isFav ? "var(--gold-2)" : col }}>{isFav ? "★" : ""}{c.name || c.team}</span>
              <span className="rrln">
                <span className={"rrboat" + (fin ? " fin" : "") + (isFav ? " fav" : "")} style={{ left: x + "%", color: isFav ? "var(--gold-2)" : col }}>
                  <svg className="rrhull" viewBox="0 0 48 18" aria-hidden="true">
                    <g opacity="0.85">
                      <g>
                        <animateTransform attributeName="transform" type="rotate" dur="0.9s" repeatCount="indefinite" calcMode="spline" keyTimes="0;0.5;1" keySplines="0.4 0 0.6 1;0.4 0 0.6 1" values="-19 17 7.3;19 17 7.3;-19 17 7.3" />
                        <line x1="17" y1="7.3" x2="17" y2="3" stroke="currentColor" strokeWidth="1" strokeLinecap="round" /><ellipse cx="17" cy="2.4" rx="1.3" ry="2" fill="currentColor" />
                      </g>
                      <g>
                        <animateTransform attributeName="transform" type="rotate" dur="0.9s" repeatCount="indefinite" calcMode="spline" keyTimes="0;0.5;1" keySplines="0.4 0 0.6 1;0.4 0 0.6 1" values="-19 29 7.3;19 29 7.3;-19 29 7.3" />
                        <line x1="29" y1="7.3" x2="29" y2="3" stroke="currentColor" strokeWidth="1" strokeLinecap="round" /><ellipse cx="29" cy="2.4" rx="1.3" ry="2" fill="currentColor" />
                      </g>
                      <g>
                        <animateTransform attributeName="transform" type="rotate" dur="0.9s" repeatCount="indefinite" calcMode="spline" keyTimes="0;0.5;1" keySplines="0.4 0 0.6 1;0.4 0 0.6 1" values="19 17 10.7;-19 17 10.7;19 17 10.7" />
                        <line x1="17" y1="10.7" x2="17" y2="15" stroke="currentColor" strokeWidth="1" strokeLinecap="round" /><ellipse cx="17" cy="15.6" rx="1.3" ry="2" fill="currentColor" />
                      </g>
                      <g>
                        <animateTransform attributeName="transform" type="rotate" dur="0.9s" repeatCount="indefinite" calcMode="spline" keyTimes="0;0.5;1" keySplines="0.4 0 0.6 1;0.4 0 0.6 1" values="19 29 10.7;-19 29 10.7;19 29 10.7" />
                        <line x1="29" y1="10.7" x2="29" y2="15" stroke="currentColor" strokeWidth="1" strokeLinecap="round" /><ellipse cx="29" cy="15.6" rx="1.3" ry="2" fill="currentColor" />
                      </g>
                    </g>
                    <path d="M3 9 C16 6.4 32 6.4 44 8.2 C44.9 8.45 44.9 9.55 44 9.8 C32 11.6 16 11.6 3 9 Z" fill="currentColor" />
                    <circle cx="17.5" cy="9" r="1.05" fill="#fff" opacity="0.5" /><circle cx="28.5" cy="9" r="1.05" fill="#fff" opacity="0.5" />
                  </svg>
                  {fin ? (
                    <i className="rrfintime">
                      {pos + 1}着 {karalSecToTime(c.total)}
                      {c.next && <em className={"rrq " + (c.next === "A決勝" ? "af" : c.next === "B決勝" ? "bf" : c.next === "準決" ? "sf" : "cr")}>→{RW.roundText(c.next)}</em>}
                    </i>
                  ) : (gap > 0.05 && <i className={"rrgap" + (di < 300 ? " ahead" : "")}>+{gap.toFixed(1)}</i>)}
                </span>
              </span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function RaceCardOverlay({ raceId, eventKey, ord, onClose, onOpenTeam, onOpenRace, onOpenEvent, favTeams }) {
  const RW = window.RW;
  const isEvent = !!eventKey;
  const r = !isEvent ? RW.FLAT.find((x) => x.id === raceId) : null;
  const ev = isEvent ? RW.evOf[eventKey] : (r ? r.ev : null);
  const [copied, setCopied] = React.useState(false);
  const [resMode, setResMode] = React.useState("結果");
  const [openN, setOpenN] = React.useState(null);   // エントリー一覧で開いているクルー
  const [selRound, setSelRound] = React.useState("A決勝"); // 種目モードのラウンドタブ（既定＝A決勝）
  // 種目カード（isEvent）を開いた時の初期表示：予選（最初のラウンド）のレース再現を自動スタート。個別レースは従来どおり結果。
  React.useEffect(() => {
    if (eventKey) {
      const evRounds = RW.FLAT.filter((x) => x.ev.key === eventKey).sort((a, b) => a.ord - b.ord);
      setSelRound(evRounds[0] ? (evRounds[0].isFinalA ? "A決勝" : evRounds[0].round) : "A決勝");
      setResMode("レース");
    } else {
      setSelRound("A決勝"); setResMode("結果");
    }
  }, [raceId, eventKey]);
  // 既定モード（本人指示）：終わったラウンド＝レース再現／まだのラウンド＝組み合わせ一覧。
  // 実施済みかは描画のたびに計算し、ラウンド切替だけでなく「結果が後から届いた」時も追従させる
  // （開いた直後は結果が未読込のことがあり、切替時だけの判定では常に「まだ」扱いになるため）。
  const modeRound = eventKey ? selRound : (r ? r.round : null);
  const modeRoundRaced = !!(ev && modeRound && (RW.realRaces(ev.key) || []).some((kr) =>
    karalRoundLabel(kr.round) === modeRound &&
    (kr.lanes || []).some((l) => l.rank != null || l.t2000 || l.status)));
  React.useEffect(() => {
    if (!modeRound) return;
    setResMode(modeRoundRaced ? "レース" : "結果");
  }, [modeRound, modeRoundRaced]);
  if (!ev) return null;
  const st = r ? RW.statusOf(r, ord) : null;
  const finalPassed = ev.finalA && ord >= ev.finalA.endOrd;
  const podium = finalPassed && RW.realDecided(ev.key) ? RW.PODIUMS[ev.key] : null;
  const podiumWaiting = finalPassed && !RW.realDecided(ev.key);
  const multi = r ? (r.n || 1) > 1 : true;
  const openTeam = (team) => { onClose(); onOpenTeam(RW.baseTeam(team)); };
  const shareUrl = location.origin + location.pathname + (isEvent ? "#event=" + encodeURIComponent(ev.key) : "#race=" + r.id);
  const meetLabel = (RW.META.title || "") + (RW.META.year || "");
  const shareTitle = (isEvent ? ev.code + " " + ev.name : r.ev.code + " " + r.ev.name + " " + r.round) + "｜" + meetLabel;
  const onShare = () => {
    if (navigator.share) navigator.share({ title: shareTitle, url: shareUrl }).catch(() => {});
    else { navigator.clipboard.writeText(shareUrl); setCopied(true); setTimeout(() => setCopied(false), 2000); }
  };
  const rounds = isEvent ? RW.FLAT.filter((x) => x.ev.key === ev.key).sort((a, b) => a.ord - b.ord) : null;
  const karalList = isEvent ? (RW.realRaces(ev.key) || []) : RW.realRacesFor(r);
  // 種目モードのヒートマップ／レース再現は、上のラウンドタブで選んだ段階（予選/準決/…）の全組だけに絞る
  const resGroups = groupKaralByRound(karalList);
  const awaitingResult = !isEvent && st === "done" && karalList.length === 0;
  const paceRows = karalPaceRows(karalList);
  const canViz = paceRows.length >= 2;
  const heatRows = isEvent ? paceRows.filter((pr) => pr.round === selRound) : paceRows;
  const effMode = canViz ? resMode : "結果"; // 通過タイムが無い種目は既定がレースでも結果を出す

  // 前後ナビ：種目モード＝前後の種目 / レースモード＝前後のレース。遷移先の種目コードも補足表示。
  let prevNav = null, nextNav = null, prevHint = null, nextHint = null, prevCat = null, nextCat = null, navLbl = isEvent ? "種目" : "レース";
  if (isEvent) {
    const i = RW.EVENTS.findIndex((e) => e.key === ev.key);
    const p = RW.EVENTS[i - 1], n = RW.EVENTS[i + 1];
    if (p) { prevNav = () => onOpenEvent(p.key); prevHint = p.code; prevCat = p.cat; }
    if (n) { nextNav = () => onOpenEvent(n.key); nextHint = n.code; nextCat = n.cat; }
  } else {
    const i = RW.FLAT.indexOf(r);
    const p = RW.FLAT[i - 1], n = RW.FLAT[i + 1];
    if (p) { prevNav = () => onOpenRace(p.id); prevHint = p.ev.code; prevCat = p.ev.cat; }
    if (n) { nextNav = () => onOpenRace(n.id); nextHint = n.ev.code; nextCat = n.ev.cat; }
  }

  return (
    <div className="rcwrap" onClick={onClose}>
      <div className={"rcard" + (isEvent ? " evmode" : "")} onClick={(e) => e.stopPropagation()}>
        <span className="lanes" />
        {copied && <span className="rc-share-toast">コピーしました</span>}
        <button type="button" className="rc-share" aria-label="共有" onClick={onShare}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M12 15V4" /><path d="M7 9l5-5 5 5" /><path d="M6 13v5a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-5" />
          </svg>
        </button>
        <button type="button" className="rc-x" aria-label="閉じる" onClick={onClose}>
          <svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
        </button>
        <div className="rc-hd">
          <div className="rc-badges">
            {r && r.isFinalA && <span className="finchip">決勝</span>}
            {!isEvent && (st === "live" ? <span className="livetag">LIVE</span> : st === "done" ? <span className="rc-st done">終了</span> : <span className="rc-st">NEXT</span>)}
          </div>
          <div className={"rc-codeband" + (ev.cat === "w" ? " w" : "")}>
            <span className="rc-codebig">{evCodeLabel(ev)}</span>
            <span className="rc-name">{ev.name}</span>
          </div>
          {isEvent ? (
            <div className="rc-rounds">
              {/* ラウンドタブ：予選／敗者戦／準決／B決勝／A決勝。タップでそのラウンドのレース再現が即スタート */}
              {rounds.map((rd) => {
                // 内部の round 文字列（A決勝/B決勝…）で選び、画面には大会の言い方（決勝/順位決定）を出す
                const key = rd.isFinalA ? "A決勝" : rd.round;
                const lbl = RW.roundText(key);
                return (
                  <button type="button" key={rd.id} className={"rc-rnd" + (key === selRound ? " fin" : "")}
                          onClick={() => { setSelRound(key); setResMode("レース"); }}>
                    <b>{lbl}</b>
                    <i>{rd.dow} {rd.t}</i>
                  </button>
                );
              })}
            </div>
          ) : (
            <React.Fragment>
              <div className="rc-round">{RW.roundText(r.round)}{r.grp ? "・" + r.grp : ""}</div>
              <div className="rc-time">{r.dow} {r.t}{r.tEnd && r.tEnd !== r.t ? "–" + r.tEnd : ""}</div>
            </React.Fragment>
          )}
        </div>

        <div className="rc-scroll">
          {/* 種目モードではラウンドタブ＋結果一覧で完結するため、表彰台ブロックは出さない（レースモードのみ表示） */}
          {!isEvent && podium && (
            <div className="rc-blk">
              <div className="rc-sec">RESULT・決勝</div>
              <div className="rc-podium"><PodiumRows podium={podium} onOpenTeam={openTeam} /></div>
            </div>
          )}

          {!isEvent && podiumWaiting && (
            <div className="rc-blk">
              <div className="rc-sec">RESULT・決勝</div>
              <div className="rc-res-wait">結果待ち（公式結果の反映まで数分かかることがあります）</div>
            </div>
          )}

          {resGroups.length > 0 && (() => {
            // 選択中ラウンドに結果（着順・タイム・DNS等）が1件でもあるか。無ければ発艇前＝「組み合わせ」表記
            const roundRaced = resGroups.filter((g) => !isEvent || g.round === selRound)
              .some((g) => g.races.some((kr) => (kr.lanes || []).some((l) => l.rank != null || l.t2000 || l.status)));
            const resLabel = roundRaced ? "結果" : "組み合わせ";
            return (
            <div className="rc-blk">
              <div className="rc-sec">{resLabel}</div>
              {canViz && (
                <div className="pcmode">
                  {["結果", "レース", "ヒートマップ"].map((m) => (
                    <button type="button" key={m} className={"pcmbtn" + (resMode === m ? " on" : "")} onClick={() => setResMode(m)}>
                      {m === "レース" ? "レース再現" : m === "結果" ? resLabel : m}
                    </button>
                  ))}
                </div>
              )}
              {effMode === "ヒートマップ" ? (heatRows.length ? <KaralPaceHeatmap rows={heatRows} key={"hm" + selRound} /> : <div className="rc-res-wait">{RW.roundText(selRound)}は通過タイムがありません</div>) : effMode === "レース" ? ((isEvent && !paceRows.some((pr) => pr.round === selRound))
                ? <div className="rc-res-wait">{RW.roundText(selRound)}はまだ行われていません。結果が入るとここで再現できます</div>
                : <KaralRaceReplay rows={isEvent ? paceRows : (karalPaceRows(RW.realRaces(r.evKey) || []) )} key={"rr" + selRound} initialRound={isEvent ? selRound : r.round} lockRound={true} favTeams={favTeams} />) : (
                <React.Fragment>
                  <div className="rc-res-note">{roundRaced ? "着順・学校・ゴールタイム（行をタップすると学校のページへ）" : "数字はレーン。結果はレース後にここへ反映"}</div>
                  <div className="rc-res">
                    {/* 種目モード：上のラウンドタブで選んだラウンドだけを直接表示（予選/B決勝…の折りたたみ見出しは出さない） */}
                    {resGroups.filter((g) => !isEvent || g.round === selRound).map((g) => (
                      <div className="rc-res-rnd" key={g.round}>
                        <KaralLaneRows races={g.races} favTeams={favTeams} onOpenTeam={openTeam} cat={ev.cat} evKey={ev.key} />
                      </div>
                    ))}
                  </div>
                </React.Fragment>
              )}
              <div className="rc-res-note">出典：静岡県ローイング協会 公式結果</div>
            </div>
            );
          })()}

          {awaitingResult && (
            <div className="rc-blk">
              <div className="rc-res-wait">結果待ち（公式結果の反映まで数分かかることがあります）</div>
            </div>
          )}

          {/* 組み合わせがまだ出ていない種目だけ、エントリー一覧をフォールバック表示。
              組み合わせが来た種目は上の「結果/組み合わせ」表示があるので二重表示にしない。 */}
          {resGroups.length === 0 && (
          <div className="rc-blk">
            <div className="rc-sec">{multi ? "エントリー全クルー（組分け・レーンは大会当日に発表）" : "エントリー"}</div>
            <div className={"rc-list" + (ev.cat === "w" ? " w" : "")}>
              {ev.entries.map((en, i) => {
                const isFav = !!(favTeams && favTeams[RW.baseTeam(en.team)]);
                const rowCls = (i % 2 ? " alt" : "") + (isFav ? " fav" : "");
                const members = crewMembers(ev.key, en.team);
                const open = openN === en.n;
                return (
                  <React.Fragment key={en.n}>
                    <div className={"rc-row clk" + rowCls + (en.x ? " scratch" : "")}
                         onClick={() => (members ? setOpenN(open ? null : en.n) : openTeam(en.team))}>
                      <span className="rc-n">{en.n}</span>
                      <span className="rc-team">{members && members.length === 1 ? members[0][0] : en.team}</span>
                      {members && members.length === 1 && <span className="rc-sub">{en.team}</span>}
                      {en.x && <span className="rc-dnf">棄権</span>}
                      {isFav && !en.x && <Star size={11} color="var(--gold-2)" />}
                      {members && members.length > 1
                        ? <IconChevron dir={open ? "up" : "down"} size={12} />
                        : <IconArrow size={12} />}
                    </div>
                    {open && members && members.length > 1 && (
                      <React.Fragment>
                        <CrewMemberList members={members} />
                        <button type="button" className="rc-teambtn" onClick={() => openTeam(en.team)}>この学校のページへ →</button>
                      </React.Fragment>
                    )}
                  </React.Fragment>
                );
              })}
            </div>
          </div>
          )}

        </div>

        <div className="rc-nav">
          <button type="button" className={prevCat === "w" ? "w" : "m"} disabled={!prevNav} onClick={() => prevNav && prevNav()}>‹ 前の{navLbl}{prevHint && <em className="rc-navhint">{prevHint}</em>}</button>
          <button type="button" className={nextCat === "w" ? "w" : "m"} disabled={!nextNav} onClick={() => nextNav && nextNav()}>次の{navLbl}{nextHint && <em className="rc-navhint">{nextHint}</em>} ›</button>
        </div>
      </div>
    </div>
  );
}

// 日程タブ用インライン展開：1レース(=予選など1ラウンド)を、その場でダークパネルに展開して表示する。
// 結果が来たら KaralLaneRows が組ごとに着順(1位順)へ自動整列。結果前は組み合わせ／エントリー一覧を表示。
function RaceInline({ r, favTeams, onOpenTeam }) {
  const RW = window.RW;
  const ev = r.ev;
  const karalList = RW.realRacesFor(r);
  const resGroups = groupKaralByRound(karalList);
  // このラウンドに結果が1件でもあるか（無ければ発艇前＝「組み合わせ」表記）
  const inlineRaced = karalList.some((kr) => (kr.lanes || []).some((l) => l.rank != null || l.t2000 || l.status));
  // 既定：終わったラウンド＝レース再現／まだ＝組み合わせ（本人指示）
  const [resMode, setResMode] = React.useState(inlineRaced ? "レース" : "結果");
  const paceRows = karalPaceRows(karalList);
  const canViz = paceRows.length >= 2;
  const multi = (r.n || 1) > 1;
  const openTeam = onOpenTeam || null;
  const pgRow = window.KANSAI.PROG_OF ? window.KANSAI.PROG_OF(ev.key, r.round) : null;
  return (
    <div className="rc-inline">
      {pgRow && <div className="rc-prog"><b>進出条件</b>{pgRow.note}</div>}
      {resGroups.length > 0 ? (
        <React.Fragment>
          {canViz && (
            <div className="pcmode">
              {["結果", "レース", "ヒートマップ"].map((m) => (
                <button type="button" key={m} className={"pcmbtn" + (resMode === m ? " on" : "")} onClick={() => setResMode(m)}>
                  {m === "レース" ? "レース再現" : m === "結果" ? (inlineRaced ? "結果" : "組み合わせ") : m}
                </button>
              ))}
            </div>
          )}
          {resMode === "ヒートマップ" ? <KaralPaceHeatmap rows={paceRows} /> : resMode === "レース" ? <KaralRaceReplay rows={karalPaceRows(RW.realRaces(ev.key) || [])} initialRound={r.round} lockRound={true} favTeams={favTeams} /> : (
            <React.Fragment>
              <div className="rc-res-note">{inlineRaced ? "着順・学校・ゴールタイム" : "数字はレーン。結果はレース後にここへ反映"}</div>
              <div className="rc-res">
                {resGroups.map((g) => (
                  <KaralLaneRows key={g.round} races={g.races} favTeams={favTeams} onOpenTeam={openTeam} cat={ev.cat} evKey={ev.key} />
                ))}
              </div>
            </React.Fragment>
          )}
        </React.Fragment>
      ) : (
        <React.Fragment>
          <div className="rc-res-note">{multi ? "エントリー全クルー（組分け・レーンは大会当日に発表）" : "エントリー（着順は結果が入ると1位順で表示）"}</div>
          <div className={"rc-list" + (ev.cat === "w" ? " w" : "")}>
            {ev.entries.map((en) => (
              <div className={"rc-row" + (en.x ? " scratch" : "")} key={en.n}>
                <span className="rc-n">{en.n}</span>
                <span className="rc-team">{en.team}</span>
                {en.x && <span className="rc-dnf">棄権</span>}
              </div>
            ))}
          </div>
        </React.Fragment>
      )}
    </div>
  );
}
Object.assign(window, { RaceCardOverlay, RaceInline, KaralLaneRows, ProgChip, KaralRaceReplay });
