/* teams.jsx — 団体一覧（大会別＋五十音の行見出し）・検索・お気に入り・詳細
   TEAM_YOMI は並べ替え用の読み（インカレ出場校＋新人戦2026の全92団体ぶん）。長音符は入れず、拗音・促音は小書きにしない。 */
// 五十音ソート用の読み（baseTeam正規化後の団体名→ひらがな）
const TEAM_YOMI = {
 "茨城大学":"いばらき", "都立小松川高校":"とりつこまつがわ", "諏訪清陵高校":"すわせいりよう", "岡谷南高校":"おかやみなみ", "美方高校":"みかた", "若狭東高校":"わかさひがし", "浜松BC佐鳴会":"はままつびいしいさなるかい", "愛知東郷ボートクラブ":"あいちとうごうぼうとくらぶ", "清風高校":"せいふう", "宮島工業高校":"みやじまこうぎよう", "銀河学院高等学校":"ぎんががくいん", "福山BC":"ふくやまびいしい", "成立学園高等学校":"せいりつがくえん", "太陽工業":"たいようこうぎよう", "関西電力":"かんさいでんりよく", "米子漕艇クラブ":"よなごそうていくらぶ", "東京農大網走":"とうきようのうだいあばしり", "本荘高校":"ほんじよう", "佐野東高校":"さのひがし", "慶應義塾大学医学部":"けいおうぎじゆくいがくぶ", "今治工業高校":"いまばりこうぎよう", "新田高校":"にった", "松山大学今治南混成":"まつやまいまばりみなみこんせい", "川内高校OB混成":"せんだいおおびいこんせい", "浦和岡谷南混成":"うらわおかやみなみこんせい", "北大小樽商大混成":"ほくだいおたるしようだいこんせい", "早大理工秋田大学混成":"そうだいりこうあきたこんせい", "今治工業宇和島東混成":"いまばりこうぎようわじまひがしこんせい", "京都大学医学部":"きようといがくぶ", "慶應法政混成":"けいおうほうせいこんせい", "明治大・南稜高校混成":"めいじなんりようこんせい", "早稲田・中央混成":"わせだちゆうおうこんせい", "科学大理工筑波大混成":"かがくだいりこうつくばこんせい", "中央大学混成":"ちゆうおうこんせい", "仙台大学混成":"せんだいこんせい", "富山国際大美方高混成":"とやまこくさいみかたこんせい",
 "青山学院大学":"あおやまがくいん", "NTT東日本":"えぬていていひがしにほん", "愛媛大学":"えひめ", "追手門学院大学":"おうてもんがくいん", "大阪大学":"おおさか",
 "大阪・大阪公立混成":"おおさかおおさかこうりつこんせい", "大阪工業大学":"おおさかこうぎよう", "大阪公立大学":"おおさかこうりつ", "岡山大学":"おかやま", "小樽商科大学":"おたるしようか",
 "鹿児島大学":"かごしま", "金沢大学":"かなざわ", "鹿屋体育大学":"かのやたいいく", "関西大学":"かんさい", "関西学院大学":"かんせいがくいん",
 "関西学院・関西混成":"かんせいがくいんかんさいこんせい", "学習院大学":"がくしゆういん", "北里大学医学部":"きたさといがくぶ", "九州大学":"きゆうしゆう", "京都大学":"きようと",
 "京都米子JDS混成":"きようとよなごじえいでいえすこんせい", "岐阜大学":"ぎふ", "熊本大学":"くまもと", "熊本学園大学":"くまもとがくえん", "慶應義塾大学":"けいおうぎじゆく",
 "慶應一橋混成":"けいおうひとつばしこんせい", "警視庁":"けいしちよう", "高知大学":"こうち", "神戸大学":"こうべ", "滋賀大学":"しが", "滋賀医科大学":"しがいか",
 "滋賀大学教育学部":"しがきよういくがくぶ", "志水会混成":"しみずかいこんせい", "周南公立大学":"しゆうなんこうりつ", "自治医科大学":"じちいか", "成蹊大学":"せいけい", "成城大学":"せいじよう",
 "清風「葛」会":"せいふうかつらかい", "瀬田漕艇クラブ混成":"せたそうていくらぶこんせい", "仙台大学":"せんだい", "総合研究大学院大学":"そうごうけんきゆうだいがくいん",
 "WRインビクタス":"だぶりゆあるいんびくたす", "千葉大学":"ちば", "中央大学":"ちゆうおう", "中央大学理工学部":"ちゆうおうりこうがくぶ", "中部電力":"ちゆうぶでんりよく",
 "中部電力混成":"ちゆうぶでんりよくこんせい", "直進行軍":"ちよくしんこうぐん", "筑波大学":"つくば", "DRC混成":"でいあるしいこんせい", "デンソーオルカリス":"でんそおるかりす",
 "東海大学":"とうかい", "東京大学":"とうきよう", "東京科学大学医歯学系":"とうきようかがくいしがくけい", "東京科学大学理工学系":"とうきようかがくりこうがくけい",
 "東京外国語大学":"とうきようがいこくご", "東京経済大学":"とうきようけいざい", "東京都立大学":"とうきようとりつ", "東経大・明治大混成":"とうけいだいめいじだいこんせい", "同志社大学":"とうししや",
 "東大滋賀東北北大混成":"とうだいしがとうほくほくだいこんせい", "東大立教成城混成":"とうだいりつきようせいじようこんせい", "東北大学":"とうほく", "東北学院大学":"とうほくがくいん",
 "東北大学混成":"とうほくこんせい", "東北太陽愛知混成":"とうほくたいようあいちこんせい", "富山国際大学":"とやまこくさい", "トヨタ自動車RC":"とよたじどうしや", "名古屋大学":"なごや",
 "名古屋工業大学":"なごやこうぎよう", "名古屋大南山大混成":"なごやだいなんざんだいこんせい", "南山大学":"なんざん", "新潟大学":"にいがた", "新潟RC混成":"にいがたあるしいこんせい",
 "日本大学":"にほん", "日本体育大学":"にほんたいいく", "葉月RC混成":"はづきあるしいこんせい", "一橋大学":"ひとつばし", "広島大学":"ひろしま", "福岡教育大学":"ふくおかきよういく",
 "福岡女子大学":"ふくおかじよし", "福島県立医科大学":"ふくしまけんりついか", "法政大学":"ほうせい", "北海道大学":"ほつかいどう", "北海道大学医学部":"ほつかいどういがくぶ",
 "北海道大学水産学部":"ほつかいどうすいさんがくぶ", "防衛大学校":"ぼうえい", "松江工業高等専門学校":"まつえこうぎよう", "松山大学":"まつやま", "みちのくRC混成":"みちのくあるしいこんせい",
 "明治大学":"めいじ", "山茨福日医早理広混成":"やまいばらふくにちいそうりひろこんせい", "山形大学混成":"やまがたこんせい", "山口大学":"やまぐち", "山梨大学医学部":"やまなしいがくぶ",
 "横浜今治愛知東郷混成":"よこはまいまばりあいちとうごうこんせい", "立教大学":"りつきよう", "立命館大学":"りつめいかん", "龍谷大学":"りゆうこく", "早稲田大学":"わせだ",
 "早稲田大学理工漕艇部":"わせだりこうそうていぶ", "早稲田理工碧水会混成":"わせだりこうへきすいかいこんせい",
};
const byYomi = (a, b) => (TEAM_YOMI[a] || a).localeCompare(TEAM_YOMI[b] || b, "ja");
// ラウンド種別→色分けキー。決勝に近いほど鮮やかな配色になるよう段階づけ
const roundKind = (r) => r.isFinalA ? "afinal" : r.round === "B決勝" ? "bfinal" : r.round === "準決" ? "semi" : r.round === "敗者戦" ? "cr" : "heat";
// 進出チップの色は「行き先」に合わせる（準決=紫 / A決勝=金 / B決勝=黄 / 敗者戦=グレー / それ以外=緑）。日程セルの配色と統一。
// karal.jpは「→Ａ決勝」など全角英字で配信するため、判定前に半角へ正規化する。
const zenQ = (s) => String(s || "").replace(/[Ａ-Ｚａ-ｚ０-９]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xFEE0));
const progDest = (qRaw) => { const q = zenQ(qRaw); return !q ? "" : /A決勝/.test(q) ? "prog-afinal" : /B決勝/.test(q) ? "prog-bfinal" : /準決/.test(q) ? "prog-semi" : /敗者|敗復/.test(q) ? "prog-cr" : /DNS|棄権/.test(q) ? "" : "go"; };
function TeamDetail({ team, ord, isFav, toggleFav, onClose, onOpenEvent }) {
  const RW = window.RW;
  const ROS = window.KANSAI.ROSTERS || {};
  const minorTeam = RW.isMinorTeam && RW.isMinorTeam(team);  // 高校（未成年）は氏名を出さない
  const t = RW.TEAMS[team];
  const [openK, setOpenK] = React.useState(null);
  React.useEffect(() => { setOpenK(null); }, [team]);
  if (!t) return null;
  // チーム詳細内では基準チーム名は自明なので、クルー表記から団体名を落として A/B/C だけ見せる
  // （例：「同志社大学A」→「A」）。接頭辞が一致しない稀なケースは元の表記をそのまま返す。
  const crewSuffix = (label) => {
    const s = String(label || "").trim();
    if (team && s.startsWith(team)) { const rest = s.slice(team.length).replace(/^[\s　・]+/, ""); return rest || s; }
    return s;
  };
  // group crews by event
  const byEvent = {};
  t.crews.forEach((c) => { (byEvent[c.evKey] = byEvent[c.evKey] || []).push(c); });
  const evKeys = Object.keys(byEvent).sort((a, b) => RW.evOf[a].div.localeCompare(RW.evOf[b].div));
  const finalsWon = evKeys.filter((k) => RW.evOf[k].finalA && ord >= RW.evOf[k].finalA.endOrd);
  const prog = RW.favProgress(team);
  const isAfter = ord >= RW.LAST; // 大会後：日程マスをやめ「最終結果」を出す
  const zenR = (s) => String(s || "").replace(/[Ａ-Ｚａ-ｚ０-９]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xFEE0));
  // クルーの到達最終ラウンド＋着順から「最終結果」を作る。決勝は着順を“総合順位”に変換して表示。
  //   新人戦は A決勝6艇（総合1〜6位）・B決勝4艇（7〜10位）・C決勝以下は5艇ずつ（11〜15位、16〜20位…）
  //   （JARAプログレッション：5レーン運用・A決勝のみ6レーン、準決勝4・5着がB決勝）。
  //   A決勝: 1/2/3=メダル、4〜6=そのまま「N位」。B〜L決勝は「総合N位」。
  const FINAL_OFFSET = (L) => L === "A" ? 0 : L === "B" ? 6 : L === "C" ? 10 : 10 + 5 * ("ABCDEFGHIJKL".indexOf(L) - 2);
  const resultOf = (p) => {
    const q = zenR(p.qualify), r = p.rank, R = p.lastRound;
    if (r == null && /DNS|棄権/.test(q)) return { text: "棄権", cls: "out" };
    if (R === "A決勝") {
      if (r === 1) return { medal: "🥇", text: "優勝", cls: "gold" };
      if (r === 2) return { medal: "🥈", text: "2位", cls: "silver" };
      if (r === 3) return { medal: "🥉", text: "3位", cls: "bronze" };
      return { text: r != null ? r + "位" : "決勝進出", cls: "af" };
    }
    const fm = /^([B-L])決勝$/.exec(R || "");
    if (fm) return { text: r != null ? "総合" + (FINAL_OFFSET(fm[1]) + r) + "位" : "出場", cls: "bf" };
    if (R === "準決") return { text: "準決敗退", cls: "sf" };
    if (R === "敗者戦") return { text: "敗者戦敗退", cls: "cr" };
    return { text: (R || "予選") + "敗退", cls: "out" };
  };
  // 種目×開催日のマトリクス（1種目=1行、列=大会の全日程）。
  // 時刻は「その種目のそのラウンド枠の発艇時間帯」：複数組なら 7:00–8:20 の幅で示す
  // （どの組で漕ぐかは組み合わせ発表まで確定しないため、確定時刻に見せない）。1レースだけの枠は時刻そのまま。
  const shortRound = (r) => r.isFinalA ? "A決勝" : r.round === "準々決勝" ? "準々" : r.round;
  const isLetterFinal = (r) => /^[C-L]決勝$/.test(r.round) && !r.isFinalA;   // 順位別決勝（C〜L）
  // ── 組み合わせ発表後：そのクルーの確定レース（時刻・組・レーン）を返す ──
  // draw.js の表記は crew 種目「同志社大学」／スカル「氏名(同志社大学)」。団体名で突き合わせる。
  const DRAW = window.KANSAI.DRAW || {};
  const drawTeamOf = (s) => {
    const m = String(s).match(/[（(]([^（()）]+)[)）]\s*$/);
    return RW.baseTeam(m ? m[1] : s);
  };
  const drawRoundLabel = (r) => (r || "")
    .replace(/^Heat(\d+)組?$/, "予選$1組").replace(/^Heat$/, "予選")
    .replace(/^QF(\d+)組?$/, "準々$1組")
    .replace(/^SemiF(\d*)組?$/, (m0, n) => "準決" + (n ? n + "組" : ""))
    .replace(/^Rep(\d+)組?$/, "敗復$1組").replace(/^Repechage.*$/, "敗復")
    .replace(/^Semi(?:Final)?([A-Z]?)(\d*)組?$/, (m0, a, n) => "準決" + (a || "") + (n || "") + (n ? "組" : ""))
    .replace(/^Quarter(?:Final)?(\d*)組?$/, (m0, n) => "準々" + (n ? n + "組" : ""))
    .replace(/^Final\s*([A-Z])$/, "$1決勝").replace(/^Final$/, "決勝");
  // その種目・その団体が出る確定レース一覧（日付インデックスつき）
  const dayIdxOfMMDD = (mmdd) => {
    const norm = (x) => String(x).replace(/\b0(\d)/g, "$1");
    return RW.DAYS.findIndex((d) => norm(d.date) === norm(mmdd));
  };
  const drawFor = (evKey, teamName) => {
    const races = DRAW[evKey];
    if (!races) return null;
    const base = RW.baseTeam(teamName);
    const hits = [];
    races.forEach((r) => {
      (r.lanes || []).forEach((l) => {
        if (drawTeamOf(l.team) !== base) return;
        hits.push({ di: dayIdxOfMMDD(r.mmdd), t: r.t.replace(/^0/, ""),
                    round: drawRoundLabel(r.round), lane: l.lane, no: r.no });
      });
    });
    return hits;
  };
  const dayCells = (evKey, teamName) => {
    // 組み合わせが出ている日は確定レース（時刻・組・レーン）、
    // まだの日（準決・決勝など）は従来どおり予定の時間帯を出す。
    const fixed = teamName ? (drawFor(evKey, teamName) || []) : [];
    const fixedDays = new Set(fixed.map((x) => x.di));
    return RW.DAYS.map((d, di) => {
      if (fixedDays.has(di)) {
        return { rows: fixed.filter((x) => x.di === di).sort((a, b) => a.t.localeCompare(b.t))
          .map((x) => ({ kind: /^A決勝/.test(x.round) ? "afinal"
                           : /^B決勝/.test(x.round) ? "bfinal"
                           : /準決/.test(x.round) ? "semi"
                           : /決勝/.test(x.round) ? "cr" : "heat",
                         label: x.round, time: x.t, lane: x.lane, fixed: true })) };
      }
      // 組み合わせ済みの日に出番が無いなら、その日は空（予定の重複表示を防ぐ）
      if (fixed.length && DRAW[evKey] && DRAW[evKey].some((r) => dayIdxOfMMDD(r.mmdd) === di)) return { rows: [] };
      return (() => {
    const items = RW.FLAT.filter((x) => x.ev.key === evKey && x.dayIndex === di);
    const rows = [];
    // 順位別決勝が同日に複数並ぶ日（例 D〜I決勝）は1チップにまとめる：「D–I決勝 9:22–10:12」
    const lf = items.filter(isLetterFinal);
    const rest = items.filter((r) => !isLetterFinal(r));
    if (lf.length >= 2) {
      const letters = lf.map((r) => r.round.charAt(0)).sort();
      rows.push({ kind: "cr", label: letters[0] + "–" + letters[letters.length - 1] + "決勝",
        time: lf[0].t + "–" + (lf[lf.length - 1].tEnd || lf[lf.length - 1].t) });
    } else lf.forEach((r) => rows.push({ kind: "cr", label: r.round,
      time: (r.n > 1 && r.tEnd && r.tEnd !== r.t) ? r.t + "–" + r.tEnd : r.t }));
    rest.forEach((r) => rows.push({ kind: roundKind(r), label: shortRound(r),
      time: (r.n > 1 && r.tEnd && r.tEnd !== r.t) ? r.t + "–" + r.tEnd : r.t }));
    rows.sort((a, b) => a.time.localeCompare(b.time));
    return { rows };
      })();
    });
  };

  return (
    <div className="tcard">
      <div className="th" onClick={() => onClose && onClose()} style={{ cursor: onClose ? "pointer" : "default" }}>
        <span className="lanes" />
        <div className="thbtns">
          <StarBtn on={isFav} onClick={() => toggleFav(team)} size={24} />
          {onClose && <button className="closebtn" aria-label="閉じる" onClick={(e) => { e.stopPropagation(); onClose(); }}><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg></button>}
        </div>
        <h3>{team}</h3>
        <div className="meta"><span>{evKeys.length}種目 · {t.crews.length}エントリー</span></div>
      </div>
      <div className="tbody">
        {!isAfter && (
          <React.Fragment>
            <div className="tmx-hdr">
              <span className="tmx-hlbl" />
              {RW.DAYS.map((d) => (
                <span className={"tmx-hd" + (d.dow === "土" ? " sat" : d.dow === "日" ? " sun" : "")} key={d.key}>
                  <b>{d.dow}</b><i>{d.date}</i>
                </span>
              ))}
            </div>
            <div className="tmx-leg">
              {[["#0fb5cf", "予選・準々"], ["#6238cf", "準決"], ["#d99a16", "A決勝"], ["#caa53d", "B決勝"], ["#9aa1ad", "順位決定"]].map(([c, lb]) => (
                <span key={lb}><i style={{ background: c }} />{lb}</span>
              ))}
              {window.KANSAI.DRAW && <span className="tmx-leg-lane"><em className="tmx-lane">4<span>レーン</span></em>＝組み合わせ確定</span>}
            </div>
          </React.Fragment>
        )}
        {evKeys.map((k) => {
          const ev = RW.evOf[k];
          const crews = byEvent[k];
          const ros = ROS[k] || {};
          const op = openK === k;
          const cells = dayCells(k, team);
          const evProg = prog.filter((p) => p.evKey === k);
          return (
            <React.Fragment key={k}>
              <div className="trow clk" onClick={() => setOpenK(op ? null : k)} style={{ cursor: "pointer" }}>
                <span className="tcode"><Code ev={ev} /></span>
                <span className="tnm">{ev.name}{crews.length > 1 ? `（${crews.length}）` : ""}{RW.multiDiv ? " · " + RW.DIV[ev.div].label : ""}</span>
                <IconChevron dir={op ? "up" : "down"} size={12} />
              </div>
              {isAfter ? (
                /* 大会後：種目ごとの最終結果（決勝はメダル・着順、他は「◯◯敗退」） */
                <div className="tresrow">
                  <span className="treslbl">最終結果</span>
                  {evProg.length ? evProg.map((p, i) => { const R = resultOf(p); return (
                    <span className={"tres " + R.cls} key={i}>
                      {crews.length > 1 && <b className="trescrew">{crewSuffix(p.crewLabel)}</b>}
                      {R.medal && <em className="tresmedal">{R.medal}</em>}{R.text}
                    </span>
                  ); }) : <span className="tres out">結果は結果タブへ</span>}
                </div>
              ) : (
                <React.Fragment>
                  {evProg.length > 0 && (
                    <div style={{ display: "flex", flexWrap: "wrap", gap: 5, margin: "-2px 0 8px" }}>
                      {evProg.map((p, i) => (
                        <span className={"progchip " + progDest(p.qualify)} key={i}>
                          {crews.length > 1 ? crewSuffix(p.crewLabel) + " " : ""}{p.lastRound}{p.grp ? " " + p.grp : ""}{p.rank != null ? " " + p.rank + "位" : ""}{p.qualify ? " " + p.qualify : ""}
                        </span>
                      ))}
                    </div>
                  )}
                  <div className="tmx">
                    <span className="tmx-hlbl" />
                    {cells.map((c, i) => (
                      <div className={"tmx-c" + (c.rows.length ? "" : " off")} key={i}>
                        {c.rows.length
                          ? c.rows.map((it, j) => (
                              <div className={"tmx-r " + it.kind} key={j}>
                                <i>{it.label}</i><b>{it.time}</b>
                                {it.lane && <em className="tmx-lane">{it.lane}<span>レーン</span></em>}
                              </div>
                            ))
                          : <span className="tmx-none">–</span>}
                      </div>
                    ))}
                  </div>
                </React.Fragment>
              )}
              {op && (
                <div className="trow-ros">
                  {crews.map((c) => {
                    const ms = ros[c.n] || [];
                    return (
                      <div className={"trow-crew" + (c.x ? " scratch" : "")} key={c.n}>
                        {crews.length > 1 && <div className="trow-crewlbl">{crewSuffix(c.label)}{c.x && <span className="rc-dnf" style={{ marginLeft: 6 }}>棄権</span>}</div>}
                        {crews.length === 1 && c.x && <div className="trow-crewlbl"><span className="rc-dnf">棄権</span></div>}
                        {(ms.length && !minorTeam) ? ms.map((m, i) => (
                          <div className="trow-seat" key={i}>
                            {ev.type !== "scull" && <span className="trow-s">{m[0]}</span>}
                            <span className="trow-nm"><RowerName name={m[1]} kana={m[2]} /></span>
                          </div>
                        )) : <div className="trow-seat"><span className="trow-nm">{minorTeam ? c.label : (c.name || c.label)}</span></div>}
                      </div>
                    );
                  })}
                  <button type="button" className="trow-evbtn" onClick={(e) => { e.stopPropagation(); onOpenEvent(k); }}>種目の全クルーを見る →</button>
                </div>
              )}
            </React.Fragment>
          );
        })}
        {!isAfter && (
          <p className="tmx-note">
            時間は<b>その種目の発艇時間帯</b>（複数組の枠は最初〜最後の発艇）。どの組で漕ぐかは
            組み合わせ発表後に確定します。予選より後の枠は<b>勝ち上がった場合</b>のレース時間です。
          </p>
        )}
      </div>
    </div>
  );
}

function TeamsView({ ord, favTeams, toggleFav, selectedTeam, setSelectedTeam, onOpenEvent, q, setQ }) {
  const RW = window.RW;
  const ROS = window.KANSAI.ROSTERS || {};
  // 団体の総人数（各クルーの名簿人数、無ければ艇の定員で概算）
  const teamHeads = (t) => RW.TEAMS[t].crews.reduce((n, c) => {
    const r = (ROS[c.evKey] || {})[c.n];
    return n + (r && r.length ? r.length : RW.seatsOf(RW.evOf[c.evKey]));
  }, 0);
  // 名簿が未公表の間は「艇の定員×クルー数」で数える。新人戦は1人1種目なので実人数と同じ。
  const HAS_ROSTER = Object.keys(ROS).length > 0;
  const [sortMode, setSortMode] = React.useState("yomi"); // "yomi"=五十音順 / "heads"=出場者数（多い順）
  const [divFilter, setDivFilter] = React.useState("all"); // "all"=すべて / 各グループ（部門が複数なら大会別、1つなら団体の種類別）
  const cmp = sortMode === "heads" ? (a, b) => teamHeads(b) - teamHeads(a) || byYomi(a, b) : byYomi;
  const all = RW.TEAM_NAMES;
  const favList = all.filter((t) => favTeams[t]).slice().sort(byYomi);
  const filtered = all.filter((t) => !q || t.includes(q));
  const athleteHits = q ? RW.searchAthletes(q).slice(0, 6) : [];
  const sorted = filtered.slice().sort(cmp);

  // ── 団体を「出場する大会（部門）」で分ける。1団体が複数大会に出ることもある（両方に載せる）。
  const DIV_ORDER = Object.keys(RW.DIV).sort((a, b) => RW.DIV[a].order - RW.DIV[b].order);
  const divsOfTeam = (t) => {
    const set = new Set();
    (RW.TEAMS[t] ? RW.TEAMS[t].crews : []).forEach((c) => { const e = RW.evOf[c.evKey]; if (e) set.add(e.div); });
    return set;
  };
  // 表示グループ：部門（大会）が複数ある大会は大会別、1つだけの大会（新人戦）は団体の種類別。
  const GROUPS = RW.multiDiv
    ? DIV_ORDER.map((dk) => ({ key: dk, label: RW.DIV[dk].label, test: (t) => divsOfTeam(t).has(dk) }))
    : [["univ", "大学"], ["hs", "高校"], ["club", "社会人・クラブ"]].map(([k, l]) => ({ key: k, label: l, test: (t) => RW.teamKind(t) === k }));
  // ── 五十音の「行」（あ/か/さ…）。濁音・半濁音は清音の行に寄せる。
  const KANA_ROWS = [
    ["あ", "あいうえおぁぃぅぇぉ"],
    ["か", "かきくけこがぎぐげご"],
    ["さ", "さしすせそざじずぜぞ"],
    ["た", "たちつてとだぢづでどっ"],
    ["な", "なにぬねの"],
    ["は", "はひふへほばびぶべぼぱぴぷぺぽ"],
    ["ま", "まみむめも"],
    ["や", "やゆよゃゅょ"],
    ["ら", "らりるれろ"],
    ["わ", "わをん"],
  ];
  const rowOf = (t) => {
    const y = TEAM_YOMI[t] || t;
    const c = y.charAt(0);
    const hit = KANA_ROWS.find(([, chars]) => chars.indexOf(c) >= 0);
    return hit ? hit[0] : "他";
  };

  const TeamBtn = ({ t }) => {
    const T = RW.TEAMS[t];
    return (
      <button className="tbtn" onClick={() => setSelectedTeam(t === selectedTeam ? null : t)}>
        {favTeams[t] && <span className="fav-s"><Star size={11} /></span>}
        <span className="tbtn-nm">{t}</span>
        <span className="tbtn-stats">
          <span className="ts-x ev"><b>{T.evKeys.size}</b>種目</span>
          <span className="ts-x cr"><b>{T.crews.length}</b>クルー</span>
          <span className="ts-x pp"><b>{teamHeads(t)}</b>名</span>
        </span>
      </button>
    );
  };
  // 部門ブロック：中を「あ行/か行…」で小見出し分け（五十音順のときだけ）。
  const DivGroup = ({ divKey, label, list }) => {
    if (!list.length) return null;
    const rows = [];
    if (sortMode === "yomi") {
      const order = KANA_ROWS.map(([r]) => r).concat("他");
      order.forEach((r) => {
        const items = list.filter((t) => rowOf(t) === r);
        if (items.length) rows.push([r, items]);
      });
    } else {
      rows.push([null, list]);
    }
    return (
      <div className="tgroup">
        <div className="tgroup-h">{label || divKey}<span className="c">{list.length}</span></div>
        {rows.map(([r, items]) => (
          <div key={r || "all"}>
            {r && <div className="krow-h">{r}</div>}
            <div className="tlist">{items.map((t) => <TeamBtn key={divKey + t} t={t} />)}</div>
          </div>
        ))}
      </div>
    );
  };

  return (
    <section className="view">
      <div className="shead"><span className="tag"><span>TEAMS</span></span><span className="jp">チームから探す</span><span className="sp" /><span className="meta">{all.length}団体</span></div>
      <div className="search"><IconSearch size={20} /><input value={q} onChange={(e) => setQ(e.target.value)} placeholder="団体名で検索（例：同志社、清風高校、トヨタ…）" autoComplete="off" /></div>
      {selectedTeam && <TeamDetail team={selectedTeam} ord={ord} isFav={!!favTeams[selectedTeam]} toggleFav={toggleFav} onClose={() => setSelectedTeam(null)} onOpenEvent={onOpenEvent} />}
      {!q && favList.length > 0 && (
        <div className="favsec" style={{ marginTop: 14 }}>
          <div className="favtitle"><Star size={14} color="var(--red)" />お気に入り</div>
          <div className="tlist">{favList.map((t) => <TeamBtn key={t} t={t} />)}</div>
        </div>
      )}
      {q && athleteHits.length > 0 && (
        <div className="favsec" style={{ marginTop: 14 }}>
          <div className="favtitle">選手</div>
          <div className="nextlist">
            {athleteHits.map((a, i) => (
              <div className="nextrow" key={i} style={{ cursor: "pointer" }} onClick={() => setSelectedTeam(RW.baseTeam(a.team))}>
                <div className="nx" style={{ display: "flex", alignItems: "center", gap: 8 }}>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontWeight: 800, fontSize: 13 }}>{a.name}{a.kana && <i className="rn-kana" style={{ marginLeft: 6 }}>{a.kana}</i>}</div>
                    <div className="nm">{a.team}・{a.evs.join(" / ")}</div>
                  </div>
                  <IconArrow size={16} />
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
      {filtered.length > 0 && (
        <div className="team-sort">
          <span className="team-sort-lbl">並び替え</span>
          <button type="button" className={"chip" + (sortMode === "yomi" ? " on" : "")} onClick={() => setSortMode("yomi")}>五十音順</button>
          <button type="button" className={"chip" + (sortMode === "heads" ? " on" : "")} onClick={() => setSortMode("heads")}>出場者数</button>
        </div>
      )}
      {filtered.length > 0 && (
        <div className="chips team-div" style={{ margin: "10px 0 0" }}>
          <button type="button" className={"chip" + (divFilter === "all" ? " on" : "")} onClick={() => setDivFilter("all")}>すべて</button>
          {GROUPS.map((g, di) => (
            <button type="button" key={g.key} className={"chip" + (divFilter === g.key ? " on" : "")} onClick={() => setDivFilter(g.key)}>
              <i className={"entsw d" + di} style={{ marginRight: 6, verticalAlign: -1 }} />{g.label}
            </button>
          ))}
        </div>
      )}
      <div style={{ marginTop: 10 }}>
        {filtered.length
          ? GROUPS.filter((g) => divFilter === "all" || divFilter === g.key)
              .map((g) => <DivGroup key={g.key} divKey={g.key} label={g.label} list={sorted.filter(g.test)} />)
          : <div className="empty">「{q}」に一致する団体はありません</div>}
      </div>
    </section>
  );
}
Object.assign(window, { TeamsView, TEAM_YOMI });
