// detail-view.jsx — composer detail page

function DetailView({ composerId, onBack, onSelectComposer, onSelectWork }) {
  // Unified works catalogue
  const [worksSort, setWorksSort] = React.useState('genre');   // genre | cat
  const [worksFilter, setWorksFilter] = React.useState('all'); // all | popular | recommended
  const [activeGenre, setActiveGenre] = React.useState(null);  // scroll-spy highlight for the ToC
  const [tocOpen, setTocOpen] = React.useState(false);
  const dtRef = React.useRef(null);
  const tocRef = React.useRef(null);
  React.useEffect(() => { setWorksSort('genre'); setWorksFilter('all'); setTocOpen(false); }, [composerId]);
  React.useEffect(() => { setTocOpen(false); }, [worksSort, worksFilter]);

  // Close the ToC dropdown on outside click.
  React.useEffect(() => {
    if (!tocOpen) return;
    const onDown = (e) => { if (tocRef.current && !tocRef.current.contains(e.target)) setTocOpen(false); };
    document.addEventListener('mousedown', onDown);
    return () => document.removeEventListener('mousedown', onDown);
  }, [tocOpen]);

  const scrollToId = (id) => {
    const el = document.getElementById(id);
    if (el) el.scrollIntoView({ block: 'start' });
  };

  // Scroll-spy: highlight the genre currently under the sticky controls.
  React.useEffect(() => {
    const root = dtRef.current;
    if (!root) return;
    let raf = 0;
    const onScroll = () => {
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => {
        const line = root.getBoundingClientRect().top + 150;
        let cur = null;
        root.querySelectorAll('.dt-genre-group').forEach(g => {
          if (g.getBoundingClientRect().top <= line) cur = g.id;
        });
        setActiveGenre(cur);
      });
    };
    root.addEventListener('scroll', onScroll, { passive: true });
    onScroll();
    return () => { root.removeEventListener('scroll', onScroll); cancelAnimationFrame(raf); };
  }, [composerId, worksSort, worksFilter]);

  const composers = window.COMPOSERS;
  const c = composers.find(x => x.id === composerId);
  if (!c) return <div className="view"><p>Composer not found.</p></div>;

  const allWorks = (window.ALL_WORKS && window.ALL_WORKS[composerId]) || null;
  const portrait = (window.PORTRAITS && window.PORTRAITS[composerId]) || null;
  const guideSet = (window.GUIDES && window.GUIDES[composerId]) || null; // { workSlug: guide }

  const era = window.ERAS.find(e => e.id === c.era);
  const initials = c.name.split(' ').map(w => w[0]).join('').slice(0, 2);
  const lifespan = c.died - c.born;
  const styleVars = { '--era-accent': `var(--era-${c.era})` };

  // Contemporaries: composers whose lifespan overlaps c's mid-life ±15
  const peak = c.born + Math.floor(lifespan / 2);
  const contemporaries = composers
    .filter(x => x.id !== c.id && x.born <= peak + 15 && x.died >= peak - 15)
    .sort((a, b) => Math.abs(a.born - c.born) - Math.abs(b.born - c.born))
    .slice(0, 5);

  return (
    <div className="dt" style={styleVars} ref={dtRef}>
      <header className="dt-hero" style={{ paddingTop: 24 }}>
        <div>
          <div className="dt-meta-row">
            <span className="era-tag">{era.name}</span>
            <span style={{ color: 'var(--ink-faint)' }}>·</span>
            <span>{c.nation}</span>
            <span style={{ color: 'var(--ink-faint)' }}>·</span>
            <span>{lifespan} years</span>
          </div>
          <h1 className="dt-name">{renderNameLines(c.name)}</h1>
          <div className="dt-dates">
            <span><em>b.</em> {c.born}</span>
            <span><em>d.</em> {c.died}</span>
            <span className="lifespan">{era.name.toUpperCase()}</span>
          </div>
        </div>
        <div className={'dt-portrait' + (portrait ? ' has-img' : '')}>
          {portrait ? (
            <React.Fragment>
              <img className="dt-portrait-img" src={portrait.src} alt={c.name} loading="lazy" />
              <a className="dt-portrait-credit" href={portrait.page} target="_blank" rel="noopener noreferrer">
                Wikimedia Commons
              </a>
            </React.Fragment>
          ) : (
            <React.Fragment>
              <div className="dt-portrait-init">{initials}</div>
              <div className="dt-portrait-label">portrait</div>
            </React.Fragment>
          )}
        </div>
      </header>

      <div className="dt-body">
        <section className="dt-bio">
          <h3>Life</h3>
          <p style={{ fontStyle: 'italic', fontSize: 22, color: 'var(--ink)', marginBottom: 28 }}>
            {c.epitaph}
          </p>
          <p>{c.bio}</p>

          <div style={{ marginTop: 40 }}>
            <h3>Era</h3>
            <p style={{ marginBottom: 8 }}>
              <span style={{
                fontFamily: 'var(--f-display)', fontStyle: 'italic',
                fontSize: 22, color: 'var(--era-accent)'
              }}>{era.name}</span>
              <span className="mono" style={{ marginLeft: 14, fontSize: 12, color: 'var(--ink-mute)', letterSpacing: '0.1em' }}>
                {era.start}–{era.end}
              </span>
            </p>
            <p style={{ marginTop: 4 }}>{era.note}</p>
          </div>

          {contemporaries.length > 0 && (
            <div style={{ marginTop: 40 }}>
              <h3>Contemporaries</h3>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
                {contemporaries.map(x => (
                  <button
                    key={x.id}
                    className="filter-chip"
                    onClick={() => onSelectComposer(x.id)}
                    style={{
                      borderColor: 'var(--border)',
                      color: `var(--era-${x.era})`,
                      fontFamily: 'var(--f-display)',
                      fontSize: 14,
                      letterSpacing: 0,
                      textTransform: 'none',
                      fontStyle: 'italic',
                      padding: '6px 12px',
                    }}
                  >
                    {x.name.split(' ').slice(-1)[0]}
                    <span className="mono" style={{ marginLeft: 8, fontSize: 9.5, color: 'var(--ink-faint)', letterSpacing: '0.08em', fontStyle: 'normal' }}>
                      {x.born}–{x.died}
                    </span>
                  </button>
                ))}
              </div>
            </div>
          )}
        </section>

        <section className="dt-works">
          {allWorks && (() => {
            const FILTERS = { all: () => true, popular: w => w.popular, recommended: w => w.recommended };
            const filtered = allWorks.filter(FILTERS[worksFilter] || FILTERS.all);
            const groups = buildGroups(filtered, worksSort);
            const total = filtered.length;
            const showToc = worksSort === 'genre' && total > 0;

            const activeGenreZh = activeGenre ? genreLabel(activeGenre.replace(/^gx-/, '')).zh : null;

            return (
              <div className="dt-cat">
                <div className="dt-cat-main">
                  <div className="dt-all-ctrl">
                    {showToc && (
                      <div className="dt-toc-dd" ref={tocRef}>
                        <button
                          className={'dt-sort-chip dt-toc-ddbtn' + (tocOpen ? ' is-open' : '')}
                          onClick={() => setTocOpen(o => !o)}
                        >
                          ☰ {activeGenreZh || '目錄'} <span className="dt-dd-caret">▾</span>
                        </button>
                        {tocOpen && (
                          <div className="dt-toc-menu">
                            {groups.map((g, gi) => (
                              <div key={gi} className={'dt-toc-genre' + (activeGenre === `gx-${g.genre}` ? ' is-active' : '')}>
                                <a className="dt-toc-glink" onClick={() => { scrollToId(`gx-${g.genre}`); setTocOpen(false); }}>
                                  {genreLabel(g.genre).zh} <span className="en">{genreLabel(g.genre).en}</span><span className="n">{g.count}</span>
                                </a>
                                <ul className="dt-toc-subs">
                                  {g.subs.map((sg, si) => (
                                    <li key={si}>
                                      <a onClick={() => { scrollToId(`sx-${g.genre}-${si}`); setTocOpen(false); }}>
                                        <span className="zh">{sg.sub.zh}</span>
                                        <span className="en">{sg.sub.en}</span>
                                        <span className="n">{sg.items.length}</span>
                                      </a>
                                    </li>
                                  ))}
                                </ul>
                              </div>
                            ))}
                          </div>
                        )}
                      </div>
                    )}
                    <span className="dt-all-ctrl-label">分類</span>
                    {[['genre', '曲種', 'Genre'], ['cat', '編號', 'Number']].map(([v, zh, en]) => (
                      <button key={v}
                        className={'dt-sort-chip' + (worksSort === v ? ' is-active' : '')}
                        onClick={() => setWorksSort(v)}>
                        {zh} <span className="en">{en}</span>
                      </button>
                    ))}
                    <span className="dt-ctrl-sep" />
                    <span className="dt-all-ctrl-label">顯示</span>
                    {[['all', '全部', 'All'], ['popular', '♥ 熱門', 'Popular'], ['recommended', '★ 精選', 'Essential']].map(([v, zh, en]) => (
                      <button key={v}
                        className={'dt-sort-chip dt-filter-chip' + (worksFilter === v ? ' is-active' : '')}
                        onClick={() => setWorksFilter(v)}>
                        {zh} <span className="en">{en}</span>
                      </button>
                    ))}
                    <span className="dt-all-total">{total} 首</span>
                  </div>

                  <div className="dt-all">
                    {groups.map((g, gi) => (
                      <div key={gi} id={g.genre ? `gx-${g.genre}` : undefined} className="dt-genre-group">
                        {g.genre && (
                          <div className="dt-genre-h">
                            <span className="dt-genre-name">{genreLabel(g.genre).zh}</span>
                            <span className="dt-genre-en">{genreLabel(g.genre).en}</span>
                            <span className="dt-genre-count">{g.count}</span>
                          </div>
                        )}
                        {g.subs.map((sg, si) => (
                          <div key={si} id={g.genre ? `sx-${g.genre}-${si}` : undefined} className="dt-sub-group">
                            {sg.sub && (
                              <div className="dt-sub-h">
                                <span className="dt-sub-name">{sg.sub.zh}</span>
                                <span className="dt-sub-en">{sg.sub.en}</span>
                                <span className="dt-sub-count">{sg.items.length}</span>
                              </div>
                            )}
                            <ul className="dt-all-list">
                              {sg.items.map((w, i) => {
                                const gKey = window.SLUGIFY(w.title);
                                const hasDetail = !!(guideSet && guideSet[gKey]);
                                return (
                                  <li
                                    key={i}
                                    className={'dt-all-row' + (hasDetail ? ' has-detail' : '')}
                                    onClick={hasDetail ? () => onSelectWork && onSelectWork(c.id, gKey) : undefined}
                                    title={hasDetail ? '查看樂曲導聆' : undefined}
                                  >
                                    {!g.genre && (
                                      <span className="dt-all-genre">
                                        <span className="zh">{genreLabel(w.genre).zh}</span>
                                        <span className="en">{genreLabel(w.genre).en}</span>
                                      </span>
                                    )}
                                    <span className="dt-all-title">
                                      {hasDetail && <span className="dt-detail-mark" title="有樂曲導聆">♪</span>}
                                      {w.title}
                                      {w.subtitle ? <span className="dt-all-sub">{w.subtitle}</span> : null}
                                      {hasDetail && <span className="dt-detail-go">導聆 →</span>}
                                    </span>
                                    <span className="dt-all-badges">
                                      {w.popular && <span className="dt-badge pop">熱門</span>}
                                      {w.recommended && <span className="dt-badge rec">精選</span>}
                                    </span>
                                    <CopyBtn text={`${c.name} ${w.title}`} />
                                  </li>
                                );
                              })}
                            </ul>
                          </div>
                        ))}
                      </div>
                    ))}

                    {total === 0 && (
                      <p className="muted" style={{ fontStyle: 'italic', padding: '20px 0' }}>沒有符合的作品。</p>
                    )}
                  </div>
                </div>
              </div>
            );
          })()}

          <div style={{ marginTop: 48 }}>
            <h3>Recordings</h3>
            <div style={{
              border: '1px solid var(--border)',
              padding: '32px 24px',
              display: 'flex',
              flexDirection: 'column',
              gap: 12,
              alignItems: 'center',
              justifyContent: 'center',
              background: 'repeating-linear-gradient(135deg, var(--surface) 0 12px, var(--surface-2) 12px 24px)',
              minHeight: 160,
            }}>
              <div className="mono" style={{ fontSize: 10, color: 'var(--ink-mute)', letterSpacing: '0.2em', textTransform: 'uppercase' }}>
                Recommended recording
              </div>
              <div style={{ fontFamily: 'var(--f-display)', fontStyle: 'italic', fontSize: 18, color: 'var(--ink-dim)', textAlign: 'center' }}>
                Catalogue cover & performer credits<br />to be linked
              </div>
            </div>
          </div>
        </section>
      </div>
    </div>
  );
}

function CopyBtn({ text }) {
  const [done, setDone] = React.useState(false);
  const onClick = (e) => {
    e.stopPropagation();
    const finish = () => { setDone(true); setTimeout(() => setDone(false), 1300); };
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(text).then(finish, finish);
    } else {
      const ta = document.createElement('textarea');
      ta.value = text; document.body.appendChild(ta); ta.select();
      try { document.execCommand('copy'); } catch (_) {}
      document.body.removeChild(ta); finish();
    }
  };
  return (
    <button
      className={'dt-copy' + (done ? ' is-done' : '')}
      onClick={onClick}
      title={'複製以便搜尋 YouTube：' + text}
      aria-label="Copy composer + work"
    >
      {done ? '✓ 已複製' : '⧉ 複製'}
    </button>
  );
}

function PlayIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M3 1.5L12 7L3 12.5V1.5Z" fill="currentColor"/>
    </svg>
  );
}

function renderNameLines(name) {
  const parts = name.split(' ');
  const surname = parts[parts.length - 1];
  const given = parts.slice(0, -1).join(' ');
  if (!given) return surname;
  return (
    <React.Fragment>
      <em>{given}</em>
      <br />
      {surname}
    </React.Fragment>
  );
}

// ─── All-works sorting / grouping ───────────────────────────
const GENRE_LABEL = {
  Orchestral: { zh: '管弦樂', en: 'Orchestral' },
  Stage:      { zh: '舞台',   en: 'Stage' },
  Vocal:      { zh: '聲樂',   en: 'Vocal' },
  Chamber:    { zh: '室內樂', en: 'Chamber' },
  Keyboard:   { zh: '鍵盤',   en: 'Keyboard' },
};
const GENRE_ORDER = ['Orchestral', 'Stage', 'Vocal', 'Chamber', 'Keyboard'];
function genreLabel(key) { return GENRE_LABEL[key] || { zh: key, en: '' }; }

// ── Sub-category classifier — finer category derived from the work title ──
const SUB_INSTR = [
  [/\bdouble bass\b/i, '低音提琴', 'Double Bass'],
  [/\b(piano|klavier)\b/i, '鋼琴', 'Piano'],
  [/\bkeyboard\b/i, '鍵盤', 'Keyboard'],
  [/\b(harpsichord|cembalo)\b/i, '大鍵琴', 'Harpsichord'],
  [/\bviolin/i, '小提琴', 'Violin'],
  [/\bviola\b/i, '中提琴', 'Viola'],
  [/\b(cello|violoncello)\b/i, '大提琴', 'Cello'],
  [/\bflute\b/i, '長笛', 'Flute'],
  [/\b(oboe|hautbois)\b/i, '雙簧管', 'Oboe'],
  [/\bclarinet\b/i, '單簧管', 'Clarinet'],
  [/\bbassoon\b/i, '低音管', 'Bassoon'],
  [/\bhorn\b/i, '法國號', 'Horn'],
  [/\btrumpet\b/i, '小號', 'Trumpet'],
  [/\bharp\b/i, '豎琴', 'Harp'],
  [/\bguitar\b/i, '吉他', 'Guitar'],
  [/\borgan\b/i, '管風琴', 'Organ'],
  [/\bmandolin/i, '曼陀林', 'Mandolin'],
];
const SUB_CN_NUM = { '2': '雙', 'two': '雙', '3': '三', 'three': '三' };
function _sub(key, zh, en) { return { key, zh, en }; }

function _instrumentsIn(title) {
  const found = [];
  for (const [re, zh, en] of SUB_INSTR) {
    const m = title.match(re);
    if (m) found.push({ idx: m.index, zh, en });
  }
  found.sort((a, b) => a.idx - b.idx);
  const seen = new Set();
  return found.filter(f => (seen.has(f.en) ? false : (seen.add(f.en), true)));
}

function _classifyConcerto(t) {
  if (/brandenburg/i.test(t)) return _sub('Brandenburg Concerto', '布蘭登堡協奏曲', 'Brandenburg Concerto');
  if (/concerto grosso/i.test(t)) return _sub('Concerto Grosso', '大協奏曲', 'Concerto Grosso');
  if (/sinfonia concertante/i.test(t)) return _sub('Sinfonia Concertante', '交響協奏曲', 'Sinfonia Concertante');
  const instrs = _instrumentsIn(t);
  const cm = t.match(/\b(2|3|two|three)\s+(harpsichords|pianos|violins|cellos|flutes|horns|oboes)\b/i);
  if (cm && instrs.length) {
    const pre = SUB_CN_NUM[cm[1].toLowerCase()] || '';
    const i = instrs[0];
    return _sub(`${cm[1]} ${i.en} Concerto`, `${pre}${i.zh}協奏曲`, `Concerto for ${cm[1]} ${i.en}s`);
  }
  if (instrs.length >= 2) {
    return _sub(instrs.map(i => i.en).join(' & ') + ' Concerto', instrs.map(i => i.zh).join('與') + '協奏曲', instrs.map(i => i.en).join(' & ') + ' Concerto');
  }
  if (instrs.length === 1) return _sub(instrs[0].en + ' Concerto', instrs[0].zh + '協奏曲', instrs[0].en + ' Concerto');
  return _sub('Concerto', '協奏曲（其他）', 'Concerto');
}

function classifySub(genre, t) {
  if (genre === 'Orchestral') {
    if (/concert(o|i|one|os)\b|brandenburg/i.test(t)) return _classifyConcerto(t);
    if (/\b(symphon|sinfoni)/i.test(t)) return _sub('Symphony', '交響曲', 'Symphony');
    if (/\b(overture|ouverture|vorspiel)\b/i.test(t)) return _sub('Overture', '序曲', 'Overture');
    if (/\bserenade\b/i.test(t)) return _sub('Serenade', '小夜曲', 'Serenade');
    if (/\bdivertimento\b/i.test(t)) return _sub('Divertimento', '嬉遊曲', 'Divertimento');
    if (/\bsuite\b/i.test(t)) return _sub('Suite', '組曲', 'Suite');
    if (/\b(dance|dances|waltz|valse|minuet|menuet|ländler|march|marches|polonaise|ecossaise|contredans|german dance)/i.test(t)) return _sub('Dances & Marches', '舞曲與進行曲', 'Dances & Marches');
    if (/\b(symphonic poem|tone poem|fantasy|fantasia|rhapsody|capriccio|variations)\b/i.test(t)) return _sub('Symphonic / Other', '交響詩與其他', 'Symphonic & Other');
    return _sub('Other Orchestral', '其他管弦', 'Other Orchestral');
  }
  if (genre === 'Chamber') {
    if (/string quartet/i.test(t)) return _sub('String Quartet', '弦樂四重奏', 'String Quartet');
    if (/string quintet/i.test(t)) return _sub('String Quintet', '弦樂五重奏', 'String Quintet');
    if (/string trio/i.test(t)) return _sub('String Trio', '弦樂三重奏', 'String Trio');
    if (/piano quartet/i.test(t)) return _sub('Piano Quartet', '鋼琴四重奏', 'Piano Quartet');
    if (/piano quintet/i.test(t)) return _sub('Piano Quintet', '鋼琴五重奏', 'Piano Quintet');
    if (/piano trio/i.test(t)) return _sub('Piano Trio', '鋼琴三重奏', 'Piano Trio');
    if (/(violin sonata|sonata for violin)/i.test(t)) return _sub('Violin Sonata', '小提琴奏鳴曲', 'Violin Sonata');
    if (/(cello sonata|sonata for cello|violoncello sonata)/i.test(t)) return _sub('Cello Sonata', '大提琴奏鳴曲', 'Cello Sonata');
    if (/(flute|clarinet|oboe|horn|bassoon|trumpet) (sonata|quintet|quartet|trio)/i.test(t)) return _sub('Wind Chamber', '管樂室內樂', 'Wind Chamber');
    if (/quintet/i.test(t)) return _sub('Quintet', '五重奏', 'Quintet');
    if (/quartet/i.test(t)) return _sub('Quartet', '四重奏', 'Quartet');
    if (/trio/i.test(t)) return _sub('Trio', '三重奏', 'Trio');
    if (/(duo|duet)/i.test(t)) return _sub('Duo', '二重奏', 'Duo');
    if (/sonata/i.test(t)) return _sub('Sonata', '奏鳴曲', 'Sonata');
    if (/serenade|divertimento|notturno/i.test(t)) return _sub('Serenade / Divertimento', '小夜曲與嬉遊曲', 'Serenade & Divertimento');
    return _sub('Other Chamber', '其他室內樂', 'Other Chamber');
  }
  if (genre === 'Keyboard') {
    if (/sonata/i.test(t)) return _sub('Sonata', '奏鳴曲', 'Sonata');
    if (/variation/i.test(t)) return _sub('Variations', '變奏曲', 'Variations');
    if (/(prelude|präludium|prélude)/i.test(t)) return _sub('Prelude', '前奏曲', 'Prelude');
    if (/fugue|fuga/i.test(t)) return _sub('Fugue', '賦格', 'Fugue');
    if (/(etude|étude)/i.test(t)) return _sub('Étude', '練習曲', 'Étude');
    if (/nocturne/i.test(t)) return _sub('Nocturne', '夜曲', 'Nocturne');
    if (/ballade/i.test(t)) return _sub('Ballade', '敘事曲', 'Ballade');
    if (/scherzo/i.test(t)) return _sub('Scherzo', '詼諧曲', 'Scherzo');
    if (/mazurka/i.test(t)) return _sub('Mazurka', '馬厝卡舞曲', 'Mazurka');
    if (/(waltz|valse)/i.test(t)) return _sub('Waltz', '圓舞曲', 'Waltz');
    if (/polonaise/i.test(t)) return _sub('Polonaise', '波蘭舞曲', 'Polonaise');
    if (/impromptu/i.test(t)) return _sub('Impromptu', '即興曲', 'Impromptu');
    if (/(fantasy|fantasia|fantasie|phantasie)/i.test(t)) return _sub('Fantasy', '幻想曲', 'Fantasy');
    if (/rondo/i.test(t)) return _sub('Rondo', '迴旋曲', 'Rondo');
    if (/(partita|suite)/i.test(t)) return _sub('Suite / Partita', '組曲與帕蒂塔', 'Suite & Partita');
    if (/toccata/i.test(t)) return _sub('Toccata', '觸技曲', 'Toccata');
    if (/invention/i.test(t)) return _sub('Invention', '創意曲', 'Invention');
    if (/(minuet|menuet|dance|écossaise|ecossaise|ländler|german dance)/i.test(t)) return _sub('Dances', '舞曲', 'Dances');
    if (/(moment|bagatelle|klavierstück|piece|pieces|album)/i.test(t)) return _sub('Character Pieces', '小品', 'Character Pieces');
    return _sub('Other Keyboard', '其他鍵盤', 'Other Keyboard');
  }
  if (genre === 'Vocal') {
    if (/requiem/i.test(t)) return _sub('Requiem', '安魂曲', 'Requiem');
    if (/\bmass\b|missa|messe/i.test(t)) return _sub('Mass', '彌撒', 'Mass');
    if (/passion/i.test(t)) return _sub('Passion', '受難曲', 'Passion');
    if (/oratorio/i.test(t)) return _sub('Oratorio', '神劇', 'Oratorio');
    if (/cantata|kantate/i.test(t)) return _sub('Cantata', '清唱劇', 'Cantata');
    if (/motet/i.test(t)) return _sub('Motet', '經文歌', 'Motet');
    if (/(magnificat|te deum|stabat mater|psalm|vespers|chorale|choral|hymn|gloria|kyrie)/i.test(t)) return _sub('Sacred Choral', '宗教合唱', 'Sacred Choral');
    if (/\b(aria|arie)\b/i.test(t)) return _sub('Aria', '詠嘆調', 'Aria');
    if (/(lied|lieder|gesang|gesänge|song|songs|canzon|romance)/i.test(t)) return _sub('Song / Lied', '藝術歌曲', 'Song / Lied');
    return _sub('Other Vocal', '其他聲樂', 'Other Vocal');
  }
  if (genre === 'Stage') {
    if (/ballet/i.test(t)) return _sub('Ballet', '芭蕾', 'Ballet');
    if (/(incidental|music to|music for the|schauspiel|bühnenmusik)/i.test(t)) return _sub('Incidental Music', '戲劇配樂', 'Incidental Music');
    if (/singspiel/i.test(t)) return _sub('Singspiel', '歌唱劇', 'Singspiel');
    return _sub('Opera', '歌劇', 'Opera');
  }
  return _sub('Other', '其他', 'Other');
}

const byCat = (a, b) => a.cat - b.cat || a.title.localeCompare(b.title);
const isOtherSub = key => /^Other\b/.test(key) || key === 'Concerto';

// Build render groups. Returns [{ genre, count, subs:[{ sub, items }] }].
// mode 'cat': single flat list (genre=null, sub=null). mode 'genre': genre → sub-category.
function buildGroups(works, mode) {
  const arr = works.slice();

  if (mode === 'cat') {
    arr.sort(byCat);
    return [{ genre: null, count: arr.length, subs: [{ sub: null, items: arr }] }];
  }

  // genre → sub
  const byGenre = {};
  for (const w of arr) (byGenre[w.genre] = byGenre[w.genre] || []).push(w);
  const genreKeys = GENRE_ORDER.filter(g => byGenre[g])
    .concat(Object.keys(byGenre).filter(g => !GENRE_ORDER.includes(g)));

  return genreKeys.map(genre => {
    const items = byGenre[genre];
    const bySub = {};
    for (const w of items) {
      const s = classifySub(genre, w.title);
      if (!bySub[s.key]) bySub[s.key] = { sub: s, items: [] };
      bySub[s.key].items.push(w);
    }
    const subs = Object.values(bySub);
    // order: by count desc, but "Other/misc" buckets last
    subs.sort((a, b) =>
      (isOtherSub(a.sub.key) - isOtherSub(b.sub.key)) ||
      (b.items.length - a.items.length) ||
      a.sub.en.localeCompare(b.sub.en)
    );
    subs.forEach(sg => sg.items.sort(byCat));
    return { genre, count: items.length, subs };
  });
}

window.DetailView = DetailView;
window.PlayIcon = PlayIcon;