'use strict';
// ═══════════════════════════════════════════════
// js/timeline.js — timeline with all metrics stacked as charts
// ═══════════════════════════════════════════════

const TL_METRICS = [
  { key: 'ci',             label: 'Coverage Index (CI)',    unit: '',  max: 10,  color: '#0057ff' },
  { key: 'pctTugged',      label: '% Days Tugged',          unit: '%', max: 100, color: '#00a86b' },
  { key: 'pctRetained',    label: '% Days Retained',        unit: '%', max: 100, color: '#6d28d9' },
  { key: 'avgHrsTugged',   label: 'Hrs/day Tugged',         unit: 'h', max: 24,  color: '#d4600a' },
  { key: 'avgHrsRetained', label: 'Hrs/day Retained',       unit: 'h', max: 24,  color: '#be185d' },
];

let _tlSelectedUser = 'me'; // 'me' or pubKeyB64
let _tlDateRange = 'all';   // '30d' | '90d' | 'all'

function tlSelectUser(val) {
  _tlSelectedUser = val;
  renderTimeline();
}

function tlSetRange(range) {
  _tlDateRange = range;
  renderTimeline();
}

function renderTimeline() {
  const el = document.getElementById('timeline-content');
  if (!el) return;

  const contacts = (typeof getContacts === 'function') ? getContacts() : [];
  const buddiesWithData = contacts.filter(c => {
    const j = typeof getPeerJournal === 'function' ? getPeerJournal(c.pubKeyB64) : null;
    return j && (j.entries || []).length > 0;
  });

  if (_tlSelectedUser !== 'me' && !buddiesWithData.some(c => c.pubKeyB64 === _tlSelectedUser)) {
    _tlSelectedUser = 'me';
  }

  // Dropdown selector
  const selectorHtml = buddiesWithData.length ? `
    <div style="display:flex;align-items:center;gap:10px;margin-bottom:1.25rem">
      <label style="font-family:var(--mono);font-size:10px;color:var(--muted2);flex-shrink:0;text-transform:uppercase;letter-spacing:.06em">Viewing</label>
      <select class="input tl-user-select" style="max-width:220px;font-size:13px">
        <option value="me" ${_tlSelectedUser === 'me' ? 'selected' : ''}>Me</option>
        ${buddiesWithData.map(c => `<option value="${esc(c.pubKeyB64)}" ${_tlSelectedUser === c.pubKeyB64 ? 'selected' : ''}>${esc(c.nickname || 'Buddy')}</option>`).join('')}
      </select>
    </div>` : '';

  let entries, isBuddy = false, buddyContact = null;
  if (_tlSelectedUser === 'me') {
    entries = getEntries().filter(e => e.date).sort((a, b) => a.date.localeCompare(b.date));
  } else {
    isBuddy = true;
    buddyContact = buddiesWithData.find(c => c.pubKeyB64 === _tlSelectedUser);
    const journal = typeof getPeerJournal === 'function' ? getPeerJournal(_tlSelectedUser) : null;
    entries = (journal?.entries || []).filter(e => e.date).sort((a, b) => a.date.localeCompare(b.date));
  }

  if (!entries.length) {
    el.innerHTML = selectorHtml + `<div class="empty"><div class="icon">◷</div><p>No entries yet${isBuddy ? ' for this buddy' : ' - start journaling to see your timeline'}</p></div>`;
    return;
  }

  // Date range filter
  const rangeTabsHtml = `<div style="display:flex;gap:4px;margin-bottom:1.25rem">
    ${['30d','90d','all'].map(r => {
      const label = r === 'all' ? 'All time' : r;
      const active = _tlDateRange === r;
      return `<button class="btn btn-sm ${active ? 'btn-primary' : 'btn-ghost'}" data-action="tlSetRange" data-a0="${r}">${label}</button>`;
    }).join('')}
  </div>`;

  let chartEntries = entries;
  if (_tlDateRange !== 'all') {
    const days = _tlDateRange === '30d' ? 30 : 90;
    const cutoff = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
    chartEntries = entries.filter(e => e.date >= cutoff);
  }

  // ── Photo gallery strip (oldest → newest) ──
  // Entries with a photo are eligible for the slideshow ("Play"). We only show
  // the Play button when there's something to scrub through.
  const playableEntries = entries.filter(e => {
    const k = isBuddy ? `c_${_tlSelectedUser.slice(0, 16)}_${e.id}_front` : e.imgFront;
    return !!k;
  });
  const playBtn = playableEntries.length > 1
    ? `<button class="btn btn-primary btn-sm" data-action="tlOpenSlideshow" style="font-size:12px;margin-left:auto">▶ Play</button>`
    : '';
  const photoStrip = `
    <div class="panel" style="margin-bottom:1.5rem">
      <div class="ph"><h2>Progress Photos</h2><span style="font-family:var(--mono);font-size:11px;color:var(--muted2)">${entries.length} entr${entries.length !== 1 ? 'ies' : 'y'}</span>${playBtn}</div>
      <div style="overflow-x:auto;padding:10px 12px 12px">
        <div style="display:flex;gap:8px;width:max-content">
          ${entries.map(e => {
            const imgKey = isBuddy
              ? `c_${_tlSelectedUser.slice(0, 16)}_${e.id}_front`
              : e.imgFront;
            const src = imgURL(imgKey);
            const buddyName = isBuddy ? (buddyContact?.nickname || 'Buddy') : null;
            return `<div style="flex-shrink:0;cursor:pointer;text-align:center;width:80px" data-action="tlRowClick" data-entry-id="${e.id}" data-buddy-pub="${isBuddy ? _tlSelectedUser : ''}">
              ${src
                ? (isBuddy && typeof photoCanvas === 'function'
                    ? photoCanvas(imgKey, { watermarkName: buddyName, style: 'width:80px;height:107px;object-fit:cover;border-radius:6px;border:1px solid var(--border2);display:block', lightbox: false })
                    : `<img class="thumb-hover" src="${src}" style="width:80px;height:107px;object-fit:cover;border-radius:6px;border:1px solid var(--border2);display:block">`)
                : `<div style="width:80px;height:107px;border-radius:6px;background:var(--surface2);border:1px solid var(--border2);display:flex;align-items:center;justify-content:center;color:var(--muted2);font-size:22px">◳</div>`}
              <div style="font-family:var(--mono);font-size:10px;color:var(--muted2);margin-top:4px">${esc(e.date.slice(5))}</div>
              <div style="margin-top:2px"><span class="ci-badge ${ciBadgeClass(e.ci)}" style="font-size:10px;padding:2px 6px">CI-${e.ci}</span></div>
            </div>`;
          }).join('')}
        </div>
      </div>
    </div>`;

  const charts = TL_METRICS.map(metric => {
    const pts = chartEntries.filter(e => e[metric.key] != null);
    if (pts.length < 2) return `
      <div style="margin-bottom:1.5rem">
        <div style="font-family:var(--mono);font-size:10px;font-weight:600;color:var(--muted2);letter-spacing:.06em;text-transform:uppercase;margin-bottom:6px;display:flex;align-items:center;gap:8px">
          <span style="width:10px;height:10px;border-radius:2px;background:${metric.color};display:inline-block;flex-shrink:0"></span>
          ${esc(metric.label)}
        </div>
        <div style="height:40px;background:var(--surface2);border-radius:5px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:11px;color:var(--muted2)">
          Need at least 2 entries with this metric
        </div>
      </div>`;
    return `
      <div style="margin-bottom:1.5rem">
        <div style="font-family:var(--mono);font-size:10px;font-weight:600;color:var(--muted);letter-spacing:.06em;text-transform:uppercase;margin-bottom:6px;display:flex;align-items:center;gap:8px">
          <span style="width:10px;height:10px;border-radius:2px;background:${metric.color};display:inline-block;flex-shrink:0"></span>
          ${esc(metric.label)}
          <span style="font-weight:400;color:var(--muted2)">${pts.length} pts</span>
        </div>
        <div class="tl-chart-wrap">${renderTlSvg(pts, metric)}</div>
      </div>`;
  }).join('');

  // Lineups -- only shown for the user's own timeline (lineups reference local
  // gallery items by id, which won't resolve on a buddy's view).
  const lineupsStrip = !isBuddy ? renderLineupsStrip() : '';

  el.innerHTML = selectorHtml + photoStrip + lineupsStrip + `
    <div style="margin-bottom:1.5rem">
      ${rangeTabsHtml}
      ${charts}
    </div>
    <div class="panel">
      <div class="ph"><h2>${isBuddy ? esc(buddyContact?.nickname || 'Buddy') + "'s " : ''}All Entries</h2></div>
      ${renderTlEntryRows(entries, isBuddy ? _tlSelectedUser : null)}
    </div>`;
}

// ─────────────────────────────────────────────────────────────────────────────
// Fullscreen slideshow: scrub the user's progress photos with a thumb-friendly
// timeline slider. Opens from the Play button on the Progress Photos panel.
// ─────────────────────────────────────────────────────────────────────────────

// Set lazily on open: the chronologically-sorted list of entries that have a
// front photo (for the currently-selected timeline user).
let _tlSlideshowEntries = null;
let _tlSlideshowIsBuddy = false;
let _tlSlideshowBuddyKey = null;

function tlOpenSlideshow() {
  const contacts = (typeof getContacts === 'function') ? getContacts() : [];
  const isBuddy = _tlSelectedUser !== 'me';
  let entries;
  if (isBuddy) {
    const journal = typeof getPeerJournal === 'function' ? getPeerJournal(_tlSelectedUser) : null;
    entries = (journal?.entries || []).slice();
  } else {
    entries = (typeof getEntries === 'function' ? getEntries() : []).slice();
  }
  entries = entries
    .filter(e => e.date)
    .filter(e => {
      const k = isBuddy ? `c_${_tlSelectedUser.slice(0, 16)}_${e.id}_front` : e.imgFront;
      return !!k;
    })
    .sort((a, b) => a.date.localeCompare(b.date));
  if (!entries.length) { toast && toast('No photos to play', true); return; }

  _tlSlideshowEntries = entries;
  _tlSlideshowIsBuddy = isBuddy;
  _tlSlideshowBuddyKey = isBuddy ? _tlSelectedUser : null;

  const overlay = document.getElementById('tlSlideshow');
  const slider  = document.getElementById('tlSlideshow-slider');
  const first   = document.getElementById('tlSlideshow-firstDate');
  const last    = document.getElementById('tlSlideshow-lastDate');
  if (!overlay || !slider) return;
  slider.min = 0;
  slider.max = entries.length - 1;
  slider.value = 0;
  if (first) first.textContent = entries[0].date.slice(2);
  if (last)  last.textContent  = entries[entries.length - 1].date.slice(2);
  _tlSlideshowShow(0);
  overlay.style.display = 'flex';
  document.body.style.overflow = 'hidden';
  _tlSlideshowWireSlider();
}

function tlCloseSlideshow() {
  const overlay = document.getElementById('tlSlideshow');
  if (overlay) overlay.style.display = 'none';
  document.body.style.overflow = '';
  _tlSlideshowEntries = null;
}

function _tlSlideshowShow(idx) {
  if (!_tlSlideshowEntries || !_tlSlideshowEntries.length) return;
  const i = Math.max(0, Math.min(_tlSlideshowEntries.length - 1, parseInt(idx, 10) || 0));
  const e = _tlSlideshowEntries[i];
  const imgEl = document.getElementById('tlSlideshow-img');
  const dateEl = document.getElementById('tlSlideshow-date');
  const progEl = document.getElementById('tlSlideshow-progress');
  const key = _tlSlideshowIsBuddy ? `c_${_tlSlideshowBuddyKey.slice(0, 16)}_${e.id}_front` : e.imgFront;
  const src = (typeof imgURL === 'function') ? imgURL(key) : null;
  if (imgEl) {
    if (src) imgEl.src = src;
    else imgEl.removeAttribute('src');
  }
  if (dateEl) {
    const parts = [e.date];
    if (e.ci != null) parts.push(`CI-${e.ci}`);
    dateEl.textContent = parts.join(' · ');
  }
  if (progEl) progEl.textContent = `${i + 1} / ${_tlSlideshowEntries.length}`;
}

function _tlSlideshowWireSlider() {
  const slider = document.getElementById('tlSlideshow-slider');
  if (!slider || slider.dataset.wired === '1') return;
  slider.dataset.wired = '1';
  // input fires continuously during drag -- gives a true scrub feel.
  slider.addEventListener('input', () => _tlSlideshowShow(slider.value));
  // Tapping the displayed image advances by one (small bonus -- nice on phones).
  const imgEl = document.getElementById('tlSlideshow-img');
  if (imgEl) {
    imgEl.addEventListener('click', () => {
      const next = Math.min(_tlSlideshowEntries.length - 1, parseInt(slider.value, 10) + 1);
      slider.value = next;
      _tlSlideshowShow(next);
    });
  }
  // ESC closes
  document.addEventListener('keydown', evt => {
    if (evt.key === 'Escape') {
      const overlay = document.getElementById('tlSlideshow');
      if (overlay && overlay.style.display === 'flex') tlCloseSlideshow();
    }
  });
}

// ─────────────────────────────────────────────────────────────────────────────
// Lineups: curated horizontal rows of gallery items for side-by-side
// comparison over time. Multiple lineups stack vertically (one per perspective
// the user wants to track).
// ─────────────────────────────────────────────────────────────────────────────

function renderLineupsStrip() {
  const lineups = (typeof getLineups === 'function') ? getLineups() : [];
  const headerBtn = `<button class="btn btn-primary btn-sm" data-action="lineupCreate" style="font-size:12px">+ New Lineup</button>`;

  if (!lineups.length) {
    return `<div class="panel" style="margin-bottom:1.5rem">
      <div class="ph"><h2>Lineups</h2>${headerBtn}</div>
      <div style="padding:14px 16px;color:var(--muted2);font-size:13px;line-height:1.5">
        Group photos from any of your galleries into a side-by-side row so you can
        watch the same perspective change over time. Create one, name it
        ("Front, flaccid"), and pick the matching shots from each session.
      </div>
    </div>`;
  }

  const rows = lineups.map(l => renderLineupRow(l)).join('');
  return `<div class="panel" style="margin-bottom:1.5rem">
    <div class="ph"><h2>Lineups</h2>${headerBtn}</div>
    <div style="padding:8px 0">${rows}</div>
  </div>`;
}

function renderLineupRow(lineup) {
  const galleries = (typeof getGalleries === 'function') ? getGalleries() : [];
  const tiles = (lineup.items || []).map(it => {
    const g = galleries.find(x => x.id === it.galleryId);
    const item = g?.items?.find(x => x.id === it.itemId);
    if (!g || !item) {
      // Item was deleted from the gallery -- show a tombstone so the user can
      // remove it from the lineup. Keeping silent would leave a phantom slot.
      return `<div title="Photo no longer exists" style="flex-shrink:0;width:120px;height:160px;border-radius:6px;border:1px dashed var(--border2);background:var(--surface2);display:flex;align-items:center;justify-content:center;color:var(--muted2);font-size:11px;text-align:center;padding:6px">Photo removed</div>`;
    }
    const src = (typeof imgURL === 'function') ? imgURL(item.imgKey) : null;
    const dateLabel = item.title || g.title || '';
    if (src) {
      return `<button class="lineup-tile" data-action="openLinkedGalleryImage" data-a0="${esc(g.id)}" data-a1="${esc(item.id)}" title="${esc(dateLabel)}" style="padding:0;border:1px solid var(--border2);border-radius:6px;overflow:hidden;background:var(--surface2);cursor:pointer;width:120px;height:160px;flex-shrink:0;position:relative;display:block">
        <img src="${src}" style="width:100%;height:100%;object-fit:cover;display:block" alt="${esc(dateLabel)}">
        ${dateLabel ? `<div style="position:absolute;bottom:0;left:0;right:0;background:linear-gradient(transparent,rgba(0,0,0,.7));color:#fff;font-family:var(--mono);font-size:10px;padding:14px 6px 4px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(dateLabel)}</div>` : ''}
      </button>`;
    }
    // Lazy-loading; the imgURL hook in storage.js will re-render once decoded
    return `<div style="flex-shrink:0;width:120px;height:160px;border-radius:6px;border:1px solid var(--border2);background:var(--surface2);display:flex;align-items:center;justify-content:center;color:var(--muted2);font-size:22px">📷</div>`;
  }).join('');

  return `<div style="border-bottom:1px solid var(--border2);padding:10px 14px 12px">
    <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
      <div style="font-weight:600;font-size:13px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(lineup.name || 'Untitled lineup')}</div>
      <span style="font-family:var(--mono);font-size:11px;color:var(--muted2)">${(lineup.items||[]).length} photo${(lineup.items||[]).length !== 1 ? 's' : ''}</span>
      <button class="btn btn-ghost btn-sm" style="font-size:11px;padding:3px 8px" data-action="lineupEdit" data-a0="${esc(lineup.id)}">Edit</button>
      <button class="btn btn-ghost btn-sm" style="font-size:11px;padding:3px 8px;color:var(--red,#dc2626)" data-action="lineupDelete" data-a0="${esc(lineup.id)}">×</button>
    </div>
    ${(lineup.items||[]).length === 0
      ? `<div style="color:var(--muted2);font-size:12px;padding:10px 0">No photos in this lineup yet. <button class="btn btn-ghost btn-sm" style="font-size:11px;padding:2px 8px" data-action="lineupEdit" data-a0="${esc(lineup.id)}">Add some</button></div>`
      : `<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none">
           <div style="display:flex;gap:8px;width:max-content">${tiles}</div>
         </div>`}
  </div>`;
}

// ── Actions ──────────────────────────────────────────────────────────────────

function _lineupId() {
  return 'l_' + Array.from(crypto.getRandomValues(new Uint8Array(8)),
    b => b.toString(36).padStart(2,'0')).join('').slice(0, 12);
}

function lineupCreate() {
  const name = prompt('Name this lineup (e.g. "Front, flaccid"):');
  if (!name || !name.trim()) return;
  const lineups = getLineups();
  const id = _lineupId();
  lineups.push({ id, name: name.trim().slice(0, 80), createdAt: Date.now(), updatedAt: Date.now(), items: [] });
  saveLineups(lineups);
  renderTimeline();
  // Open the editor for the freshly-created lineup so the user can immediately
  // start adding photos -- creating a named empty lineup and dropping them on
  // the page would be a weird dead-end.
  lineupEdit(id);
}

function lineupDelete(id) {
  if (!id) { console.warn('[lineupDelete] called without id'); return; }
  const lineups = getLineups();
  const target = lineups.find(l => l.id === id);
  if (!target) {
    console.warn('[lineupDelete] no lineup with id', id, 'available:', lineups.map(l => l.id));
    return;
  }
  // Small, reversible action -- skip the confirm dialog so the X just works.
  // (Galleries themselves are untouched; lineups are cheap to recreate.)
  const fresh = getLineups().filter(l => l.id !== id);
  saveLineups(fresh);
  if (typeof toast === 'function') toast(`Lineup "${target.name || 'untitled'}" deleted`);
  renderTimeline();
}

// ── Editor modal ─────────────────────────────────────────────────────────────

let _lineupEditingId = null;

function lineupEdit(id) {
  const lineup = getLineups().find(l => l.id === id);
  if (!lineup) return;
  _lineupEditingId = id;
  _lineupRenderEditor();
  document.getElementById('lineupEditModal')?.classList.add('open');
}

function lineupCloseEditor() {
  _lineupEditingId = null;
  document.getElementById('lineupEditModal')?.classList.remove('open');
}

function _lineupRenderEditor() {
  const m = document.getElementById('lineupEditModal');
  if (!m) return;
  const lineup = getLineups().find(l => l.id === _lineupEditingId);
  if (!lineup) { lineupCloseEditor(); return; }
  const galleries = (typeof getGalleries === 'function') ? getGalleries() : [];

  const itemRows = (lineup.items || []).map((it, idx) => {
    const g = galleries.find(x => x.id === it.galleryId);
    const item = g?.items?.find(x => x.id === it.itemId);
    const src = (item && typeof imgURL === 'function') ? imgURL(item.imgKey) : null;
    const label = item?.title || g?.title || '(deleted)';
    const isFirst = idx === 0;
    const isLast = idx === (lineup.items.length - 1);
    return `<div style="display:flex;align-items:center;gap:8px;padding:6px;border:1px solid var(--border2);border-radius:6px;background:var(--surface2);margin-bottom:6px">
      ${src
        ? `<img src="${src}" style="width:48px;height:64px;object-fit:cover;border-radius:4px;flex-shrink:0">`
        : `<div style="width:48px;height:64px;border-radius:4px;background:var(--surface3);flex-shrink:0;display:flex;align-items:center;justify-content:center;color:var(--muted2);font-size:14px">${item ? '📷' : '⚠'}</div>`}
      <div style="flex:1;min-width:0">
        <div style="font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(label)}</div>
        <div style="font-family:var(--mono);font-size:10px;color:var(--muted2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(g?.title || 'gallery deleted')}</div>
      </div>
      <button class="btn btn-ghost btn-sm" data-action="lineupMoveItem" data-a0="${idx}" data-a1="-1" style="padding:3px 8px;font-size:13px"${isFirst ? ' disabled' : ''}>↑</button>
      <button class="btn btn-ghost btn-sm" data-action="lineupMoveItem" data-a0="${idx}" data-a1="1" style="padding:3px 8px;font-size:13px"${isLast ? ' disabled' : ''}>↓</button>
      <button class="btn btn-danger btn-sm" data-action="lineupRemoveItem" data-a0="${idx}" style="padding:3px 8px;font-size:12px">×</button>
    </div>`;
  }).join('');

  m.querySelector('#lineupEdit-name').value = lineup.name || '';
  m.querySelector('#lineupEdit-items').innerHTML = itemRows ||
    `<div style="color:var(--muted2);font-size:12px;padding:8px 4px">No photos yet -- tap "Add photos" below to pick from your galleries.</div>`;
}

function lineupSaveName() {
  const lineup = getLineups().find(l => l.id === _lineupEditingId);
  if (!lineup) return;
  const newName = document.getElementById('lineupEdit-name')?.value.trim();
  if (!newName) { toast('Name required', true); return; }
  const lineups = getLineups();
  const idx = lineups.findIndex(l => l.id === _lineupEditingId);
  if (idx < 0) return;
  lineups[idx].name = newName.slice(0, 80);
  lineups[idx].updatedAt = Date.now();
  saveLineups(lineups);
  toast('Saved');
  renderTimeline();
}

function lineupMoveItem(idx, dir) {
  idx = parseInt(idx, 10);
  dir = parseInt(dir, 10);
  const lineups = getLineups();
  const lineup = lineups.find(l => l.id === _lineupEditingId);
  if (!lineup) return;
  const target = idx + dir;
  if (target < 0 || target >= lineup.items.length) return;
  const [moved] = lineup.items.splice(idx, 1);
  lineup.items.splice(target, 0, moved);
  lineup.updatedAt = Date.now();
  saveLineups(lineups);
  _lineupRenderEditor();
  renderTimeline();
}

function lineupRemoveItem(idx) {
  idx = parseInt(idx, 10);
  const lineups = getLineups();
  const lineup = lineups.find(l => l.id === _lineupEditingId);
  if (!lineup) return;
  lineup.items.splice(idx, 1);
  lineup.updatedAt = Date.now();
  saveLineups(lineups);
  _lineupRenderEditor();
  renderTimeline();
}

// ── Picker modal ─────────────────────────────────────────────────────────────

function lineupOpenPicker() {
  if (!_lineupEditingId) return;
  _lineupRenderPicker();
  document.getElementById('lineupPickerModal')?.classList.add('open');
}

function lineupClosePicker() {
  document.getElementById('lineupPickerModal')?.classList.remove('open');
}

function _lineupRenderPicker() {
  const m = document.getElementById('lineupPickerModal');
  if (!m) return;
  const galleries = (typeof getGalleries === 'function') ? getGalleries() : [];
  const lineup = getLineups().find(l => l.id === _lineupEditingId);
  const inLineup = new Set((lineup?.items || []).map(it => `${it.galleryId}|${it.itemId}`));

  const list = m.querySelector('#lineupPicker-list');
  if (!galleries.length) {
    list.innerHTML = `<div style="color:var(--muted2);font-size:13px;padding:20px;text-align:center">You don't have any galleries yet. Create one from the Gallery tab first.</div>`;
    return;
  }

  // Track whether any tile is still waiting on a lazy IDB load. If so, schedule
  // a re-render once they all (or most) have arrived so the picker fills in
  // without the user having to tap or scroll.
  let _pending = 0;

  list.innerHTML = galleries.map(g => {
    if (!g.items || !g.items.length) return '';
    const itemTiles = g.items.map(it => {
      const composite = `${g.id}|${it.id}`;
      const added = inLineup.has(composite);
      const src = (typeof imgURL === 'function') ? imgURL(it.imgKey) : null;
      if (!src) _pending++;
      const label = it.title || '';
      // Tap = toggle: adds if not in lineup, removes if already in it. The
      // border + ✓ overlay reflect the current state; the tile is never
      // disabled so deselect-by-tap always works.
      return `<button class="lineup-pick-tile" data-action="lineupPickItem" data-a0="${esc(g.id)}" data-a1="${esc(it.id)}"
        style="padding:0;border:2px solid ${added ? 'var(--accent)' : 'var(--border2)'};border-radius:6px;overflow:hidden;background:var(--surface2);cursor:pointer;width:96px;height:128px;flex-shrink:0;position:relative;opacity:${added ? '.78' : '1'}"
        title="${esc(label)}${added ? ' (tap to remove)' : ''}">
        ${src
          ? `<img src="${src}" style="width:100%;height:100%;object-fit:cover;display:block">`
          : `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:var(--muted2);font-size:22px">📷</div>`}
        ${added ? `<div style="position:absolute;inset:0;background:rgba(37,99,235,.18);display:flex;align-items:center;justify-content:center;pointer-events:none"><span style="background:var(--accent);color:#fff;border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:14px">✓</span></div>` : ''}
      </button>`;
    }).join('');
    return `<div style="margin-bottom:14px">
      <div style="font-size:12px;font-weight:600;color:var(--muted);margin-bottom:6px">${esc(g.title)} <span style="font-family:var(--mono);font-weight:400;color:var(--muted2)">· ${g.items.length} photo${g.items.length !== 1 ? 's' : ''}</span></div>
      <div style="display:flex;flex-wrap:wrap;gap:6px">${itemTiles}</div>
    </div>`;
  }).join('') || `<div style="color:var(--muted2);font-size:13px;padding:20px;text-align:center">Your galleries are empty. Add photos to a gallery first.</div>`;

  // If photos are still lazy-loading, re-render shortly after so the tiles
  // pick up their decoded blob URLs. Without this, blank 📷 placeholders
  // would persist until the user tapped something that triggered a re-render.
  if (_pending > 0 && !_lineupPickerRetryTimer) {
    _lineupPickerRetryTimer = setTimeout(() => {
      _lineupPickerRetryTimer = null;
      const open = document.getElementById('lineupPickerModal')?.classList.contains('open');
      if (open) _lineupRenderPicker();
    }, 350);
  }
}

let _lineupPickerRetryTimer = null;

function lineupPickItem(galleryId, itemId) {
  const lineups = getLineups();
  const lineup = lineups.find(l => l.id === _lineupEditingId);
  if (!lineup) return;
  // Toggle behavior: tapping a selected tile removes it; tapping an
  // unselected tile adds it. Lets the user quickly correct mistakes
  // without leaving the picker.
  const existingIdx = lineup.items.findIndex(it => it.galleryId === galleryId && it.itemId === itemId);
  if (existingIdx >= 0) {
    lineup.items.splice(existingIdx, 1);
  } else {
    lineup.items.push({ galleryId, itemId, addedAt: Date.now() });
  }
  lineup.updatedAt = Date.now();
  saveLineups(lineups);
  _lineupRenderEditor();
  _lineupRenderPicker(); // refresh check-marks
  renderTimeline();
}

function renderTlSvg(pts, metric) {
  const PAD = { top: 10, right: 42, bottom: 30, left: 42 };
  const W = 600, H = 140;
  const cW = W - PAD.left - PAD.right;
  const cH = H - PAD.top - PAD.bottom;

  const vals = pts.map(e => Number(e[metric.key]));
  const minV = Math.min(...vals);
  const maxV = Math.max(...vals);
  const vRange = maxV - minV || 1;

  const ySteps = 5;
  const yTickVals = [];
  for (let i = 0; i <= ySteps; i++) yTickVals.push(minV + (vRange * i / ySteps));

  const xOf = i => PAD.left + (pts.length > 1 ? (i / (pts.length - 1)) * cW : cW / 2);
  const yOf = v => PAD.top + cH - ((v - minV) / vRange) * cH;

  const linePts = pts.map((e, i) => `${xOf(i).toFixed(1)},${yOf(Number(e[metric.key])).toFixed(1)}`).join(' ');
  const areaPath = `M ${xOf(0).toFixed(1)},${(PAD.top + cH).toFixed(1)} ` +
    pts.map((e, i) => `L ${xOf(i).toFixed(1)},${yOf(Number(e[metric.key])).toFixed(1)}`).join(' ') +
    ` L ${xOf(pts.length-1).toFixed(1)},${(PAD.top + cH).toFixed(1)} Z`;

  const maxLabels = 6;
  const labelStep = Math.ceil(pts.length / maxLabels);
  const xLabels = pts.map((e, i) => {
    if (i % labelStep !== 0 && i !== pts.length - 1) return '';
    return `<text x="${xOf(i).toFixed(1)}" y="${(H - 4).toFixed(1)}" text-anchor="middle" font-size="8" fill="var(--muted2)" font-family="var(--mono)">${esc(e.date.slice(5))}</text>`;
  }).filter(Boolean).join('');

  const yTicksSvg = yTickVals.map(v => {
    const y = yOf(v).toFixed(1);
    const label = Number.isInteger(v) ? v : v.toFixed(1);
    return `<line x1="${PAD.left}" y1="${y}" x2="${(PAD.left + cW).toFixed(1)}" y2="${y}" stroke="var(--border2)" stroke-width="1"/>
      <text x="${(PAD.left - 4).toFixed(1)}" y="${(parseFloat(y) + 3).toFixed(1)}" text-anchor="end" font-size="8" fill="var(--muted2)" font-family="var(--mono)">${label}</text>`;
  }).join('');

  // Average reference line
  const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
  const avgY = yOf(avg).toFixed(1);
  const avgLine = `<line x1="${PAD.left}" y1="${avgY}" x2="${(PAD.left + cW).toFixed(1)}" y2="${avgY}" stroke="${metric.color}" stroke-width="1" stroke-dasharray="5,4" stroke-opacity="0.35"/>`;

  const dots = pts.map((e, i) => {
    const cx = xOf(i).toFixed(1), cy = yOf(Number(e[metric.key])).toFixed(1);
    return `<circle cx="${cx}" cy="${cy}" r="3.5" fill="${metric.color}" stroke="var(--bg)" stroke-width="1.5" style="cursor:pointer">
      <title>${esc(e.date)}: ${e[metric.key]}${metric.unit}</title>
    </circle>`;
  }).join('');

  // Last-value callout at right edge
  const lastVal = vals[vals.length - 1];
  const lastY = yOf(lastVal);
  const lastLabel = Number.isInteger(lastVal) ? String(lastVal) : lastVal.toFixed(1);
  const callout = `<text x="${(PAD.left + cW + 5).toFixed(1)}" y="${(lastY + 4).toFixed(1)}" font-size="9" fill="${metric.color}" font-family="var(--mono)" font-weight="600">${lastLabel}${metric.unit}</text>`;

  const gradId = 'tlG_' + metric.key;
  return `<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none"
    style="width:100%;height:${H}px;display:block;overflow:visible"
    xmlns="http://www.w3.org/2000/svg">
    <defs>
      <linearGradient id="${gradId}" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stop-color="${metric.color}" stop-opacity="0.15"/>
        <stop offset="100%" stop-color="${metric.color}" stop-opacity="0"/>
      </linearGradient>
    </defs>
    ${yTicksSvg}
    ${avgLine}
    <path d="${areaPath}" fill="url(#${gradId})"/>
    <polyline points="${linePts}" fill="none" stroke="${metric.color}" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
    ${dots}
    ${callout}
    <line x1="${PAD.left}" y1="${(PAD.top + cH).toFixed(1)}" x2="${(PAD.left + cW).toFixed(1)}" y2="${(PAD.top + cH).toFixed(1)}" stroke="var(--border)" stroke-width="1"/>
    ${xLabels}
    <line x1="${PAD.left}" y1="${PAD.top}" x2="${PAD.left}" y2="${(PAD.top + cH).toFixed(1)}" stroke="var(--border)" stroke-width="1"/>
  </svg>`;
}

function renderTlEntryRows(entries, buddyPubKey) {
  const sorted = [...entries].sort((a, b) => b.date.localeCompare(a.date));
  return `<div style="border-bottom:1px solid var(--border2)">
    ${sorted.map(e => {
      const thumbSrc = buddyPubKey
        ? imgURL(`c_${buddyPubKey.slice(0,16)}_${e.id}_front`)
        : imgURL(e.imgFront);
      const idx = sorted.indexOf(e);
      const prev = sorted[idx + 1];
      const delta = (() => {
        if (!prev || e.ci == null || prev.ci == null) return '';
        const d = Number(e.ci) - Number(prev.ci);
        if (d === 0) return '';
        return `<span style="color:${d > 0 ? 'var(--green)' : 'var(--red)'};font-family:var(--mono);font-size:10px;margin-left:6px">${d > 0 ? '+' : ''}${d}</span>`;
      })();
      return `<div class="tl-entry-row" data-action="tlRowClick" data-entry-id="${e.id}" data-buddy-pub="${buddyPubKey || ''}">
        ${thumbSrc
          ? `<img class="thumb-hover" style="width:28px;height:38px;object-fit:cover;border-radius:4px;flex-shrink:0" src="${thumbSrc}" alt="">`
          : `<div style="width:28px;height:38px;border-radius:4px;background:var(--surface2);flex-shrink:0;display:flex;align-items:center;justify-content:center;color:var(--muted2);font-size:13px">◳</div>`}
        <div style="flex:1;min-width:0">
          <div style="font-size:13px;font-weight:500">${esc(e.date)}</div>
          <div style="font-size:11px;color:var(--muted2);font-family:var(--mono)">
            <span class="ci-badge ${ciBadgeClass(e.ci)}">CI-${e.ci}</span>${delta}
            ${e.pctTugged != null ? ' · ' + e.pctTugged + '% tugged' : ''}
            ${e.avgHrsTugged != null ? ' · ' + e.avgHrsTugged + 'h/day' : ''}
          </div>
        </div>
        ${e.notes ? `<div style="max-width:180px;font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(e.notes)}</div>` : ''}
      </div>`;
    }).join('')}
  </div>`;
}
