/* ============================================================
   CHAI ZEBRA — Console (members-only) PAGE logic
   Gated by a (non-remembered) login, then renders the Fav
   Tracker, Word Analyzer, Fan Tracker, Audio Analytics and
   Notes — each section sorts by clicking its column headers.
   Comments / notes / reminders share one Notes store; reminders
   also surface in a floating widget.
   ============================================================ */
(() => {
  'use strict';
  const $  = (s, r = document) => r.querySelector(s);
  const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
  const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c]));
  const fmtDate = (iso) => { const d = new Date(iso + 'T00:00:00'); return isNaN(d) ? (iso || '—') : d.toLocaleDateString('en-US', { month:'short', day:'numeric', year:'numeric' }); };
  const z = n => String(n).padStart(2, '0');
  const todayISO = () => { const d = new Date(); return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())}`; };
  const addDays = (iso, n) => { const d = new Date(iso + 'T00:00:00'); d.setDate(d.getDate() + n); return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())}`; };
  const cmpStr = (a, b) => String(a).localeCompare(String(b));
  const cmpNum = (a, b) => a - b;

  /* generic column sorting: click a [data-key] header to sort, click again to flip */
  function bindSort(container, state, render){
    if (!container) return;
    container.addEventListener('click', (e) => {
      const h = e.target.closest('[data-key]'); if (!h || !container.contains(h)) return;
      const k = h.dataset.key;
      if (state.key === k) state.dir = -state.dir;
      else { state.key = k; state.dir = h.dataset.defdir === 'asc' ? 1 : -1; }
      render();
    });
  }
  function paintSort(container, state){
    if (!container) return;
    $$('[data-key]', container).forEach(h => {
      const on = h.dataset.key === state.key;
      h.classList.toggle('sort-active', on);
      h.classList.toggle('asc', on && state.dir === 1);
      h.classList.toggle('desc', on && state.dir === -1);
    });
  }

  const SONGS = window.CHAI_SONGS || [];
  const POSTS = window.CHAI_POSTS || [];

  /* ---------- favs (shared with the album page via localStorage) ---------- */
  const FAV_KEY = 'chaiZebraFavs';
  const loadFavs = () => { try { return JSON.parse(localStorage.getItem(FAV_KEY)) || {}; } catch { return {}; } };
  const favs = loadFavs();
  const VOTE_SEED = window.CHAI_VOTE_SEED || {};
  const votesFor = (s) => (favs[s] || 0) + (VOTE_SEED[s] || 0);

  /* =========================================================
     NOTES STORE — one place for comments, notes & reminders
     ========================================================= */
  const NOTES_KEY = 'chaiNotes_v1';
  function seedNotes(){
    const t = todayISO();
    return [
      { id:'n1', type:'reminder', text:'Reply to Savanna Cole re: the pre-order refund', date:t, done:false, fanId:'fan-07', fanName:'Savanna Cole', at:5 },
      { id:'n2', type:'reminder', text:'DM the super fans about the Brooklyn show', date:addDays(t, 3), done:false, at:4 },
      { id:'n3', type:'comment', text:'Chargeback on the pre-order — needs a personal reply.', date:'2025-02-15', fanId:'fan-07', fanName:'Savanna Cole', at:3 },
      { id:'n4', type:'note', text:'Vinyl repress lands next week — update the drop page.', date:t, at:2 },
      { id:'n5', type:'comment', text:'Front row every tour — sort her a Brooklyn VIP.', date:'2023-08-20', fanId:'fan-01', fanName:'Marisol Vega', at:1 },
    ];
  }
  const Notes = {
    all: (() => { try { const r = localStorage.getItem(NOTES_KEY); if (r) return JSON.parse(r); } catch {} const s = seedNotes(); try { localStorage.setItem(NOTES_KEY, JSON.stringify(s)); } catch {} return s; })(),
    save(){ try { localStorage.setItem(NOTES_KEY, JSON.stringify(this.all)); } catch {} },
    add(n){ n.id = 'n' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); n.at = Date.now(); this.all.unshift(n); this.save(); return n; },
    remove(id){ this.all = this.all.filter(x => x.id !== id); this.save(); },
    toggleDone(id){ const n = this.all.find(x => x.id === id); if (n){ n.done = !n.done; this.save(); } },
    forFan(id){ return this.all.filter(n => n.type === 'comment' && n.fanId === id).sort((a, b) => (b.at || 0) - (a.at || 0)); },
    commentCount(id){ return this.all.filter(n => n.type === 'comment' && n.fanId === id).length; },
    reminders(){ return this.all.filter(n => n.type === 'reminder'); },
  };
  // re-render everything that depends on the Notes store
  function refreshNotesUI(){ renderNotes(); renderFloatReminders(); renderFans(); }

  /* =========================================================
     FAV TRACKER  (filter + column sort)
     ========================================================= */
  const favBars = $('#favBars'), favTotal = $('#favTotal'), topFav = $('#topFav'), favHead = $('#favHead');
  let favFilter = '';
  const favSort = { key:'votes', dir:-1 };

  function renderFavBars(){
    if (!favBars) return;
    let rows = SONGS.map(s => ({ name:s, votes:votesFor(s) }));
    const q = favFilter.trim().toLowerCase();
    if (q) rows = rows.filter(r => r.name.toLowerCase().includes(q));
    rows.sort((a, b) => (favSort.key === 'name' ? cmpStr(a.name, b.name) : cmpNum(a.votes, b.votes)) * favSort.dir);
    const max = Math.max(1, ...SONGS.map(s => votesFor(s)));
    const total = SONGS.reduce((a, s) => a + votesFor(s), 0);
    favBars.innerHTML = rows.length ? '' : '<p class="bars-empty">No tracks match.</p>';
    rows.forEach((r, i) => {
      const lead = favSort.key === 'votes' && favSort.dir === -1 && i === 0 && r.votes > 0;
      const row = document.createElement('div');
      row.className = 'bar-row' + (lead ? ' lead' : '');
      row.innerHTML = `<div class="bar-label"><span>${esc(r.name)}</span><span class="val">${r.votes}</span></div><div class="bar-track"><div class="bar-fill"></div></div>`;
      favBars.appendChild(row);
      requestAnimationFrame(() => { row.querySelector('.bar-fill').style.width = (r.votes / max * 100) + '%'; });
    });
    favTotal.textContent = `${total} VOTE${total === 1 ? '' : 'S'}`;
    const top = SONGS.map(s => [s, votesFor(s)]).sort((a, b) => b[1] - a[1])[0];
    topFav.textContent = total > 0 ? top[0] : '— awaiting votes —';
    paintSort(favHead, favSort);
  }

  /* =========================================================
     FEED WORD ANALYZER  (filter + column sort)
     ========================================================= */
  const STOP = new Set(('a an the and or of to in on is are was im i my me you we it this that for at be as ' +
    'with just so all her his your our they them he she do did does get got up no not now rn really ' +
    'every single had has have like me only us out their them then there what when who why').split(' '));
  function analyzeFeed(){
    const wordCount = {}, tagCount = {};
    POSTS.forEach(p => {
      (p.text.match(/#(\w+)/g) || []).forEach(t => { const k = t.toLowerCase(); tagCount[k] = (tagCount[k] || 0) + 1; });
      p.text.replace(/#\w+/g, '').replace(/[^\w\s]|_/g, ' ').toLowerCase().split(/\s+/).forEach(w => {
        if (w.length < 3 || STOP.has(w) || /^\d+$/.test(w)) return;
        wordCount[w] = (wordCount[w] || 0) + 1;
      });
    });
    const boost = window.CHAI_WORD_BOOST || {};
    for (const w in boost) wordCount[w] = (wordCount[w] || 0) + boost[w];
    return { wordCount, tagCount };
  }
  const wordHead = $('#wordHead');
  let wordFilter = '';
  const wordSort = { key:'count', dir:-1 };
  function renderWordAnalyzer(){
    const { wordCount, tagCount } = analyzeFeed();
    let words = Object.entries(wordCount);
    const q = wordFilter.trim().toLowerCase();
    if (q) words = words.filter(([w]) => w.includes(q));
    words = words.sort((a, b) => b[1] - a[1]).slice(0, 12);   // top 12 by count...
    words.sort((a, b) => (wordSort.key === 'word' ? cmpStr(a[0], b[0]) : cmpNum(a[1], b[1])) * wordSort.dir); // ...then by chosen column
    const max = Math.max(1, ...words.map(w => w[1]));
    const totalWords = Object.values(wordCount).reduce((a, b) => a + b, 0);
    const wordBars = $('#wordBars');
    wordBars.innerHTML = words.length ? '' : '<p class="bars-empty">No words match.</p>';
    words.forEach(([w, c], i) => {
      const lead = wordSort.key === 'count' && wordSort.dir === -1 && i === 0;
      const row = document.createElement('div');
      row.className = 'bar-row' + (lead ? ' lead' : '');
      row.innerHTML = `<div class="bar-label"><span>${esc(w)}</span><span class="val">×${c}</span></div><div class="bar-track"><div class="bar-fill"></div></div>`;
      wordBars.appendChild(row);
      requestAnimationFrame(() => { row.querySelector('.bar-fill').style.width = (c / max * 100) + '%'; });
    });
    $('#wordTotal').textContent = `${totalWords} WORDS`;
    const cloud = $('#tagCloud');
    cloud.innerHTML = '';
    Object.entries(tagCount).sort((a, b) => b[1] - a[1]).forEach(([t, c]) => { const span = document.createElement('span'); span.textContent = `${t} ·${c}`; cloud.appendChild(span); });
    paintSort(wordHead, wordSort);
  }

  /* =========================================================
     FAN TRACKER  (labels + comment + filter + column sort)
     ========================================================= */
  const fansBody = $('#fansBody'), fansCount = $('#fansCount'), fansHead = $('#fansHead');
  let fans = (window.CHAI_FANS && window.CHAI_FANS.load()) || [];
  let fanFilter = '', fanLabelFilter = '';
  const fanSort = { key:'added', dir:-1 };

  function renderFans(){
    if (!fansBody) return;
    let list = fans.slice();
    const q = fanFilter.trim().toLowerCase();
    if (q) list = list.filter(f => (f.name || '').toLowerCase().includes(q) || (f.email || '').toLowerCase().includes(q) || (f.x || '').toLowerCase().includes(q));
    if (fanLabelFilter) list = list.filter(f => f.labels && f.labels[fanLabelFilter]);
    list.sort((a, b) => {
      const k = fanSort.key;
      const r = (k === 'added') ? cmpStr(a.added || '', b.added || '') : cmpStr(a[k] || '', b[k] || '');
      return r * fanSort.dir;
    });
    fansBody.innerHTML = list.length ? '' : '<tr><td colspan="6" class="fans-empty">No fans match your filters.</td></tr>';
    list.forEach(f => {
      const L = f.labels || {};
      const cc = Notes.commentCount(f.id);
      const tr = document.createElement('tr');
      tr.className = 'fan-row' + (L.concerning ? ' concerning' : '');
      tr.dataset.id = f.id;
      tr.innerHTML = `
        <td class="fan-name">${esc(f.name) || '<span class="muted">—</span>'}</td>
        <td class="fan-email">${esc(f.email)}</td>
        <td class="fan-x">${f.x ? esc(f.x) : '<span class="muted">—</span>'}</td>
        <td class="fan-since">${fmtDate(f.added)}</td>
        <td>
          <div class="fan-labels">
            <button class="lbl lbl-great${L.great ? ' on' : ''}" data-k="great">great fan</button>
            <button class="lbl lbl-super${L.super ? ' on' : ''}" data-k="super">super fan</button>
            <button class="lbl lbl-concerning${L.concerning ? ' on' : ''}" data-k="concerning">concerning</button>
          </div>
        </td>
        <td class="fan-cmt-cell"><button class="fan-cmt${cc ? ' has' : ''}" data-id="${f.id}">${cc ? '💬 ' + cc : '+ note'}</button></td>`;
      fansBody.appendChild(tr);
    });
    const total = fans.length, shown = list.length;
    fansCount.textContent = (shown !== total) ? `${shown}/${total} FANS` : `${total} FAN${total === 1 ? '' : 'S'}`;
    paintSort(fansHead, fanSort);
  }

  if (fansBody){
    fansBody.addEventListener('click', (e) => {
      const tr = e.target.closest('.fan-row'); if (!tr) return;
      if (e.target.classList.contains('fan-cmt')){ openFanCmt(tr.dataset.id); return; }
      if (e.target.classList.contains('lbl')){
        const f = fans.find(x => x.id === tr.dataset.id); if (!f) return;
        f.labels = f.labels || {};
        const k = e.target.dataset.k; f.labels[k] = !f.labels[k];
        if (window.CHAI_FANS) window.CHAI_FANS.save(fans);
        renderFans();
      }
    });
  }

  /* =========================================================
     AUDIO ANALYTICS  (live-ish; filter + column sort)
     ========================================================= */
  const AUDIO_BASE = {
    'Chai Why I Do Dis':            { clicks:1240, avg:42,  comp:58, trend:6  },
    'Reset the Table':              { clicks:980,  avg:51,  comp:63, trend:-3 },
    'A Revolution ft. Goala Girls': { clicks:4820, avg:96,  comp:78, trend:14 },
    'Never Wanted to be Queen':     { clicks:5310, avg:88,  comp:81, trend:9  },
    'Time in a Jungle':             { clicks:1530, avg:64,  comp:66, trend:2  },
    'Sticking Her Neck Out':        { clicks:3680, avg:102, comp:74, trend:11 },
    'Days for the Girls':           { clicks:2110, avg:58,  comp:69, trend:-5 },
    'Head Above the Claudes':       { clicks:2740, avg:71,  comp:72, trend:7  },
    'Wear Out Your Words':          { clicks:870,  avg:47,  comp:55, trend:-2 },
    '27 Zebras':                    { clicks:1980, avg:60,  comp:64, trend:4  },
  };
  let audio = SONGS.map(s => { const b = AUDIO_BASE[s] || { clicks:500, avg:50, comp:50, trend:0 }; return { song:s, clicks:b.clicks, tpt:b.clicks * b.avg, comp:b.comp, trend:b.trend, hot:false }; });
  const audioHead = $('#audioHead');
  let audioFilter = '';
  const audioSort = { key:'clicks', dir:-1 };

  const fmtNum = n => Math.round(n).toLocaleString('en-US');
  const fmtDur = (sec) => { sec = Math.round(sec); const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60; if (h) return `${h}h ${m}m`; if (m) return `${m}m ${s}s`; return `${s}s`; };
  const audioBody = $('#audioBody'), audioSummary = $('#audioSummary');

  function renderAudioSummary(){
    if (!audioSummary) return;
    const totalClicks = audio.reduce((a, x) => a + x.clicks, 0);
    const totalTpt = audio.reduce((a, x) => a + x.tpt, 0);
    const avgTpc = totalClicks ? totalTpt / totalClicks : 0;
    const avgComp = Math.round(audio.reduce((a, x) => a + x.comp, 0) / audio.length);
    const topFavS = SONGS.map(s => [s, votesFor(s)]).sort((a, b) => b[1] - a[1])[0];
    const cells = [
      ['TOTAL CLICKS', fmtNum(totalClicks)],
      ['TOTAL PLAY TIME (TPT)', fmtDur(totalTpt)],
      ['AVG TIME / CLICK (TPC)', fmtDur(avgTpc)],
      ['AVG COMPLETION', avgComp + '%'],
      ['TOP FAV', (topFavS && topFavS[1] > 0) ? topFavS[0] : '—'],
    ];
    audioSummary.innerHTML = cells.map(([l, v]) => `<div class="astat"><span class="astat-l">${l}</span><strong class="astat-v">${esc(v)}</strong></div>`).join('');
  }
  function renderAudio(){
    if (!audioBody) return;
    renderAudioSummary();
    let list = audio.map(x => ({ ...x, tpc: x.clicks ? x.tpt / x.clicks : 0, favs: votesFor(x.song) }));
    const q = audioFilter.trim().toLowerCase();
    if (q) list = list.filter(x => x.song.toLowerCase().includes(q));
    list.sort((a, b) => { const k = audioSort.key; const r = (k === 'song') ? cmpStr(a.song, b.song) : cmpNum(a[k] || 0, b[k] || 0); return r * audioSort.dir; });
    const topClicks = Math.max(...audio.map(x => x.clicks));
    audioBody.innerHTML = list.length ? '' : '<tr><td colspan="8" class="fans-empty">No tracks match.</td></tr>';
    list.forEach(x => {
      const tr = document.createElement('tr');
      tr.className = 'fan-row audio-row' + (x.hot ? ' hot' : '') + (x.clicks === topClicks ? ' top' : '');
      const tCls = x.trend > 0 ? 'up' : x.trend < 0 ? 'down' : 'flat';
      const tTxt = x.trend > 0 ? `▲ ${x.trend}%` : x.trend < 0 ? `▼ ${Math.abs(x.trend)}%` : '—';
      tr.innerHTML = `
        <td class="fan-name">${esc(x.song)}</td>
        <td class="au-num">${fmtNum(x.clicks)}</td>
        <td class="au-num">${fmtDur(x.tpt)}</td>
        <td class="au-num">${fmtDur(x.tpc)}</td>
        <td class="au-fav">${x.favs}</td>
        <td><div class="au-comp"><div class="au-bar"><span style="width:${x.comp}%"></span></div><em>${x.comp}%</em></div></td>
        <td class="au-trend ${tCls}">${tTxt}</td>
        <td><span class="au-live" title="Active now">● LIVE</span></td>`;
      audioBody.appendChild(tr);
    });
    paintSort(audioHead, audioSort);
  }
  let audioTimer = null;
  function startAudioLive(){
    if (audioTimer) return;
    audioTimer = setInterval(() => {
      audio.forEach(x => x.hot = false);
      const n = 1 + Math.floor(Math.random() * 3);
      for (let i = 0; i < n; i++){
        const x = audio[Math.floor(Math.random() * audio.length)];
        const tpc = x.tpt / Math.max(1, x.clicks);
        const add = 1 + Math.floor(Math.random() * 4);
        x.clicks += add; x.tpt += add * tpc * (0.7 + Math.random() * 0.6);
        x.comp = Math.max(42, Math.min(92, x.comp + (Math.random() < 0.5 ? -1 : 1)));
        x.hot = true;
      }
      renderAudio();
    }, 3500);
  }

  /* =========================================================
     NOTES SECTION  (comments + notes + reminders, by date)
     ========================================================= */
  const notesBody = $('#notesBody'), notesCount = $('#notesCount'), notesHead = $('#notesHead');
  let notesFilter = '', notesType = '';
  const notesSort = { key:'date', dir:-1 };
  const TYPE_LABEL = { note:'NOTE', comment:'COMMENT', reminder:'REMINDER' };

  function renderNotes(){
    if (!notesBody) return;
    let list = Notes.all.slice();
    const q = notesFilter.trim().toLowerCase();
    if (q) list = list.filter(n => (n.text || '').toLowerCase().includes(q) || (n.fanName || '').toLowerCase().includes(q));
    if (notesType) list = list.filter(n => n.type === notesType);
    list.sort((a, b) => {
      let r;
      if (notesSort.key === 'type') r = cmpStr(a.type, b.type);
      else if (notesSort.key === 'text') r = cmpStr(a.text, b.text);
      else r = cmpStr(a.date, b.date) || ((a.at || 0) - (b.at || 0));
      return r * notesSort.dir;
    });
    notesBody.innerHTML = list.length ? '' : '<tr><td colspan="4" class="fans-empty">No notes yet.</td></tr>';
    list.forEach(n => {
      const tr = document.createElement('tr');
      tr.className = 'note-row note-' + n.type + (n.type === 'reminder' && n.done ? ' done' : '');
      tr.dataset.id = n.id;
      const fanTag = n.fanName ? ` <span class="note-fan">${esc(n.fanName)}</span>` : '';
      const doneBtn = n.type === 'reminder' ? `<button class="note-done" title="Toggle done">${n.done ? '✓' : '○'}</button>` : '';
      tr.innerHTML = `
        <td><span class="note-badge nb-${n.type}">${TYPE_LABEL[n.type] || 'NOTE'}</span></td>
        <td class="note-text">${esc(n.text)}${fanTag}</td>
        <td class="fan-since">${fmtDate(n.date)}</td>
        <td class="note-act">${doneBtn}<button class="note-del" title="Delete">✕</button></td>`;
      notesBody.appendChild(tr);
    });
    notesCount.textContent = `${Notes.all.length} NOTE${Notes.all.length === 1 ? '' : 'S'}`;
    paintSort(notesHead, notesSort);
  }
  if (notesBody){
    notesBody.addEventListener('click', (e) => {
      const tr = e.target.closest('.note-row'); if (!tr) return;
      const id = tr.dataset.id;
      if (e.target.classList.contains('note-done')){ Notes.toggleDone(id); refreshNotesUI(); }
      else if (e.target.classList.contains('note-del')){ Notes.remove(id); refreshNotesUI(); }
    });
  }
  const noteForm = $('#noteForm'), noteText = $('#noteText');
  if (noteForm) noteForm.addEventListener('submit', (e) => { e.preventDefault(); const t = noteText.value.trim(); if (!t) return; Notes.add({ type:'note', text:t, date: todayISO() }); noteForm.reset(); refreshNotesUI(); });
  const remForm = $('#remForm'), remText = $('#remText'), remDate = $('#remDate');
  if (remForm){
    if (remDate && !remDate.value) remDate.value = todayISO();
    remForm.addEventListener('submit', (e) => { e.preventDefault(); const t = remText.value.trim(); if (!t) return; Notes.add({ type:'reminder', text:t, date: remDate.value || todayISO(), done:false }); remForm.reset(); if (remDate) remDate.value = todayISO(); refreshNotesUI(); });
  }

  /* =========================================================
     FLOATING REMINDERS  (always-on widget over the console)
     ========================================================= */
  const remFab = $('#remFab'), remPop = $('#remPop'), remBadge = $('#remBadge'), remPopList = $('#remPopList');
  function renderFloatReminders(){
    if (!remPopList) return;
    const today = todayISO();
    const rems = Notes.reminders().slice().sort((a, b) => (Number(a.done) - Number(b.done)) || cmpStr(a.date, b.date));
    remPopList.innerHTML = rems.length ? '' : '<li class="rem-empty">No reminders. You’re clear. 🦓</li>';
    rems.forEach(n => {
      const overdue = !n.done && n.date <= today;
      const li = document.createElement('li');
      li.className = 'rem-li' + (n.done ? ' done' : '');
      li.dataset.id = n.id;
      li.innerHTML = `<button class="rem-check" title="Done">${n.done ? '✓' : ''}</button><span class="rem-tx">${esc(n.text)}</span><span class="rem-dt${overdue ? ' overdue' : ''}">${overdue ? 'Due · ' : ''}${fmtDate(n.date)}</span><button class="rem-x" title="Delete">✕</button>`;
      remPopList.appendChild(li);
    });
    const due = Notes.reminders().filter(n => !n.done).length;
    if (remBadge){ remBadge.textContent = due; remBadge.classList.toggle('hide', due === 0); }
  }
  if (remPopList){
    remPopList.addEventListener('click', (e) => {
      const li = e.target.closest('.rem-li'); if (!li) return;
      const id = li.dataset.id;
      if (e.target.classList.contains('rem-check')){ Notes.toggleDone(id); refreshNotesUI(); }
      else if (e.target.classList.contains('rem-x')){ Notes.remove(id); refreshNotesUI(); }
    });
  }
  if (remFab) remFab.addEventListener('click', () => { if (remPop) remPop.hidden = !remPop.hidden; });
  const remPopClose = $('#remPopClose'); if (remPopClose) remPopClose.addEventListener('click', () => { remPop.hidden = true; });
  const remPopForm = $('#remPopForm'), remPopText = $('#remPopText'), remPopDate = $('#remPopDate');
  if (remPopForm){
    if (remPopDate && !remPopDate.value) remPopDate.value = todayISO();
    remPopForm.addEventListener('submit', (e) => { e.preventDefault(); const t = remPopText.value.trim(); if (!t) return; Notes.add({ type:'reminder', text:t, date: (remPopDate && remPopDate.value) || todayISO(), done:false }); remPopForm.reset(); if (remPopDate) remPopDate.value = todayISO(); refreshNotesUI(); });
  }

  /* =========================================================
     FAN COMMENT MODAL  (per fan; comments flow into Notes)
     ========================================================= */
  const fanCmtModal = $('#fanCmtModal');
  let cmtFanId = null, cmtFanName = '';
  function renderFanCmtList(){
    const el = $('#fanCmtList'); if (!el) return;
    const list = Notes.forFan(cmtFanId);
    el.innerHTML = list.length ? '' : '<p class="cmt-empty">No comments yet — add the first.</p>';
    list.forEach(n => {
      const d = document.createElement('div'); d.className = 'cmt-item'; d.dataset.id = n.id;
      d.innerHTML = `<p>${esc(n.text)}</p><div class="cmt-meta"><span>${fmtDate(n.date)}</span><button class="cmt-del" title="Delete">✕</button></div>`;
      el.appendChild(d);
    });
  }
  function openFanCmt(fanId){
    const f = fans.find(x => x.id === fanId); if (!f) return;
    cmtFanId = fanId; cmtFanName = f.name || f.email;
    $('#fanCmtName').textContent = f.name || '(no name)';
    $('#fanCmtMeta').textContent = [f.email, f.x].filter(Boolean).join('  ·  ');
    renderFanCmtList();
    fanCmtModal.classList.add('show'); document.body.style.overflow = 'hidden';
  }
  function closeFanCmt(){ fanCmtModal.classList.remove('show'); document.body.style.overflow = ''; cmtFanId = null; }
  if (fanCmtModal){
    const c = $('#fanCmtClose'); if (c) c.addEventListener('click', closeFanCmt);
    fanCmtModal.addEventListener('click', (e) => { if (e.target === fanCmtModal) closeFanCmt(); });
    const listEl = $('#fanCmtList');
    if (listEl) listEl.addEventListener('click', (e) => { if (!e.target.classList.contains('cmt-del')) return; const it = e.target.closest('.cmt-item'); Notes.remove(it.dataset.id); renderFanCmtList(); refreshNotesUI(); });
    const fForm = $('#fanCmtForm'), fText = $('#fanCmtText');
    if (fForm) fForm.addEventListener('submit', (e) => { e.preventDefault(); const t = fText.value.trim(); if (!t || !cmtFanId) return; Notes.add({ type:'comment', text:t, fanId:cmtFanId, fanName:cmtFanName, date: todayISO() }); fForm.reset(); renderFanCmtList(); refreshNotesUI(); });
  }

  /* =========================================================
     CONTROLS — filters + column sorters
     ========================================================= */
  const on = (id, evt, fn) => { const el = $('#' + id); if (el) el.addEventListener(evt, fn); };
  on('favFilter',  'input', e => { favFilter = e.target.value; renderFavBars(); });
  on('wordFilter', 'input', e => { wordFilter = e.target.value; renderWordAnalyzer(); });
  on('fanFilter',  'input', e => { fanFilter = e.target.value; renderFans(); });
  on('fanLabelFilter', 'change', e => { fanLabelFilter = e.target.value; renderFans(); });
  on('audioFilter','input', e => { audioFilter = e.target.value; renderAudio(); });
  on('notesFilter','input', e => { notesFilter = e.target.value; renderNotes(); });
  on('notesTypeFilter', 'change', e => { notesType = e.target.value; renderNotes(); });

  bindSort(favHead, favSort, renderFavBars);
  bindSort(wordHead, wordSort, renderWordAnalyzer);
  bindSort(fansHead, fanSort, renderFans);
  bindSort(audioHead, audioSort, renderAudio);
  bindSort(notesHead, notesSort, renderNotes);

  /* =========================================================
     SECTION SUBNAV — scroll-spy active state
     ========================================================= */
  const subnav = $('#sectionNav');
  if (subnav && 'IntersectionObserver' in window){
    const links = $$('a', subnav);
    const map = {}; links.forEach(a => { map[a.getAttribute('href').slice(1)] = a; });
    const obs = new IntersectionObserver((ents) => {
      ents.forEach(en => { if (en.isIntersecting){ links.forEach(a => a.classList.remove('active')); const a = map[en.target.id]; if (a) a.classList.add('active'); } });
    }, { rootMargin: '-42% 0px -53% 0px', threshold: 0 });
    ['sec-fav', 'sec-word', 'sec-fans', 'sec-audio', 'sec-notes'].forEach(id => { const el = $('#' + id); if (el) obs.observe(el); });
  }

  /* =========================================================
     COLLAPSE — a ▾ toggle on every section's header
     ========================================================= */
  $$('#console .panel').forEach(p => {
    const head = p.querySelector('.panel-head');
    if (head && !head.querySelector('.panel-toggle')){
      const btn = document.createElement('button');
      btn.type = 'button'; btn.className = 'panel-toggle';
      btn.setAttribute('aria-label', 'Collapse / expand section');
      btn.title = 'Collapse / expand';
      btn.textContent = '▾';
      head.appendChild(btn);
    }
  });
  const _consoleEl = $('#console');
  if (_consoleEl) _consoleEl.addEventListener('click', (e) => {
    const t = e.target.closest('.panel-toggle'); if (!t) return;
    t.closest('.panel').classList.toggle('collapsed');
  });

  /* =========================================================
     LOGIN GATE  (members-only; NOT remembered between visits)
     ========================================================= */
  const LOGIN_KEY = 'chaiLoggedIn';
  const consoleSection = $('#console');
  const loginModal = $('#loginModal');

  function showConsole(){
    loginModal.classList.remove('show');
    consoleSection.classList.remove('locked');
    document.body.classList.add('authed');
    document.body.style.overflow = '';
    renderFavBars(); renderWordAnalyzer(); renderFans(); renderAudio(); renderNotes(); renderFloatReminders();
    startAudioLive();
  }
  function showLogin(){
    consoleSection.classList.add('locked');
    loginModal.classList.add('show');
    document.body.classList.remove('authed');
    document.body.style.overflow = 'hidden';
    setTimeout(() => { const u = $('#loginUser'); if (u) u.focus(); }, 50);
  }
  try { localStorage.removeItem(LOGIN_KEY); } catch {}
  showLogin();

  $('#loginForm').addEventListener('submit', (e) => { e.preventDefault(); const u = $('#loginUser').value.trim(), p = $('#loginPass').value.trim(); if (!u || !p) return; showConsole(); });
  $('#loginClose').addEventListener('click', () => { window.location.href = 'index.html'; });

  /* =========================================================
     FOOTER + SIGN OUT + PRIVACY
     ========================================================= */
  const signOut = () => { window.location.href = 'index.html'; };
  $('#logoutBtn').addEventListener('click', signOut);
  const signOutLink = $('#signOutLink'); if (signOutLink) signOutLink.addEventListener('click', (e) => { e.preventDefault(); signOut(); });

  const privacyModal = $('#privacyModal');
  const closePrivacy = () => privacyModal.classList.remove('show');
  const privacyLink = $('#privacyLink'); if (privacyLink) privacyLink.addEventListener('click', (e) => { e.preventDefault(); privacyModal.classList.add('show'); });
  $('#privacyClose').addEventListener('click', closePrivacy);
  $('#privacyOk').addEventListener('click', closePrivacy);
  privacyModal.addEventListener('click', (e) => { if (e.target === privacyModal) closePrivacy(); });
  document.addEventListener('keydown', (e) => { if (e.key === 'Escape'){ closePrivacy(); if (fanCmtModal) closeFanCmt(); } });

  /* nav solidifies on scroll */
  const nav = $('#top');
  if (nav){ const onScroll = () => nav.classList.toggle('scrolled', window.scrollY > 40); window.addEventListener('scroll', onScroll, { passive:true }); onScroll(); }
})();
