'use strict';
  function renderStatusNav(){
    const counts = {0:0,1:0,2:0,3:0,4:0,5:0,99:0};
    for(const c of getAllCollections()){
      if(c.status !== 'confirmed_deleted' && c.status !== 'missing_candidate') counts[0]++;
      if(c.status === 'missing_candidate' || c.status === 'protected_local' || c.status === 'confirmed_deleted') counts[99]++;
      if(c.status !== 'missing_candidate' && c.status !== 'confirmed_deleted') counts[c.collection_type] = (counts[c.collection_type]||0)+1;
    }
    // Hide the safety box (status 99) from guests (read-only public mode, not the admin).
    $('statusNav').innerHTML = Object.entries(STATUS).filter(([k])=>authReady || Number(k)!==99).map(([k,v])=>`<button data-status="${k}" class="${filters.status==k?'active':''}"><span>${statusLabelFor(Number(k),filters.type)||v}</span><span class="badge ${k==99&&counts[99]?'danger':''}">${counts[k]||0}</span></button>`).join('');
    $('statusNav').querySelectorAll('button').forEach(btn=>btn.onclick=()=>{ resetRenderLimit(); filters.status=Number(btn.dataset.status); filters.tag='全部'; render(); });
  }
  function renderTags(){
    const base = getAllCollections().filter(c => {
      if(filters.status === 99) return c.status === 'missing_candidate' || c.status === 'protected_local' || c.status === 'confirmed_deleted';
      if(c.status === 'missing_candidate' || c.status === 'confirmed_deleted') return false;
      if(filters.status && c.collection_type !== filters.status) return false;
      if(filters.type && c.subject_type !== filters.type) return false;
      return true;
    });
    const myMap = new Map(); const publicMap = new Map(); let no=0;
    for(const c of base){
      const my = myTagsOf(c); const pub = publicTagNames(c);
      if(!my.length && !pub.length) no++;
      for(const t of my) myMap.set(t,(myMap.get(t)||0)+1);
      for(const t of pub) publicMap.set(t,(publicMap.get(t)||0)+1);
    }
    // Sidebar tag quick-filter (substring on tag name); applied BEFORE slicing so tags beyond top-N are findable.
    const q = ($('tagFilterInput')?.value || '').trim().toLowerCase();
    const matchTag = ([name]) => !q || String(name).toLowerCase().includes(q);
    const myTags = [...myMap.entries()].filter(matchTag).sort((a,b)=>b[1]-a[1] || a[0].localeCompare(b[0],'zh-Hans-CN')).slice(0,70).map(([name,n])=>({key:tagKey('my',name), label:name, n, kind:'my', prefix:'我的'}));
    const publicTags = [...publicMap.entries()].filter(matchTag).sort((a,b)=>b[1]-a[1] || a[0].localeCompare(b[0],'zh-Hans-CN')).slice(0,90).map(([name,n])=>({key:tagKey('public',name), label:name, n, kind:'public', prefix:'公共'}));
    const items = [{key:'全部',label:'全部',n:base.length,kind:'all'}, ...(!q && no?[{key:'无标签',label:'无标签',n:no,kind:'none'}]:[]), ...myTags, ...publicTags];
    $('tagList').innerHTML = items.map(it=>`<button class="tag-chip tag-filter-btn ${it.kind==='my'?'my-tag':it.kind==='public'?'public-tag':''} ${filters.tag===it.key?'active':''}" data-tag-key="${esc(it.key)}" title="${esc(it.prefix ? it.prefix + '标签：' + it.label : it.label)}">${it.prefix?`<span class="tag-kind">${esc(it.prefix)}</span>`:''}${esc(it.label)} · ${it.n}</button>`).join('');
    $('tagList').querySelectorAll('button').forEach(b=>b.onclick=()=>{ resetRenderLimit(); filters.tag=b.dataset.tagKey; render(); });
    updateTagListMore();
  }
  // Mobile sidebar: reveal the "展开更多" toggle only when the tag list overflows its collapsed height.
  // COLLAPSED must stay in sync with the `.tag-list` max-height in the mobile media query.
  function updateTagListMore(){
    const tl = $('tagList'), btn = $('tagListMore'); if(!tl || !btn) return;
    const COLLAPSED = 138;
    const overflow = tl.scrollHeight > COLLAPSED + 8;
    btn.classList.toggle('show', overflow);
    if(!overflow){ tl.classList.remove('tags-expanded'); btn.textContent = '展开更多'; }
  }
  // Drop the hover-expand fade mask on comments whose full text fits within the 7-line expanded height
  // (so a fully-shown comment isn't needlessly faded). 7-line threshold is fixed → measure once per card.
  function markCommentHoverFade(){
    const list = $('content').querySelectorAll('.comment:not(.cmt-checked)');
    if(!list.length) return;
    const lh = parseFloat(getComputedStyle(list[0]).lineHeight) || 20;
    const maxHover = lh * 7 + 2;
    list.forEach(c => { c.classList.add('cmt-checked'); if(c.scrollHeight <= maxHover) c.classList.add('cmt-fits-hover'); });
  }
  async function renderCards(){
    const arr = visibleCollections();
    if(!renderLimit) resetRenderLimit();
    const step = pageSize();
    $('countPill').textContent = `${arr.length} 条`;
    $('filterSummary').textContent = `${STATUS[filters.status] || '全部'} / ${tagLabelFromKey(filters.tag)} / ${filters.type ? SUBJECT_LABEL[filters.type] : '全部分类'}${filters.hasComment?' / 有吐槽':''}${filters.nsfw==='only'?' / 仅 NSFW':filters.nsfw==='hide'?' / 屏蔽 NSFW':''}`;
    const anyFilter = filters.status || filters.type || filters.tag!=='全部' || (filters.q && filters.q.trim()) || filters.nsfw!=='all' || filters.hasComment;
    $('filterSummary').classList.toggle('has-active', !!anyFilter);
    if(!arr.length){
      if(filters.status === 99){
        $('content').innerHTML = `<div class="safety-empty"><div class="safety-panel"><h2>安全箱现在是空的，这是好事。</h2><p>安全箱不是普通分类，它只在“本地以前有，但这次远端完整同步后突然没有出现”的时候才会有内容。</p><div class="safety-steps"><div class="safety-step"><b>1. 正常同步新增条目：</b>你在 Bangumi 网页新增或修改收藏后，点“同步最新变动”，它会写入本地。</div><div class="safety-step"><b>2. 远端疑似吞条目：</b>条目不会被本地删除，而是进入安全箱。</div><div class="safety-step"><b>3. 你手动判断：</b>确认线上删除 / 本地保留 / 等下次同步恢复。</div></div></div></div>`;
      } else {
        $('content').innerHTML = `<div class="empty"><div><h2>这里还没有条目</h2><p>先在设置里粘贴 Access Token，然后点“同步最新变动”。</p></div></div>`;
      }
      return;
    }
    const shown = arr.slice(0, renderLimit);
    $('content').innerHTML = `<div class="poster-grid">${shown.map((c,i)=>cardHTML(c,i)).join('')}</div>${arr.length>shown.length?`<div class="load-more-wrap"><button class="btn" id="loadMoreBtn">再显示 ${Math.min(step, arr.length-shown.length)} 条（共 ${arr.length} 条）</button></div>`:''}`;
    $('loadMoreBtn')?.addEventListener('click',()=>{
      const btn=$('loadMoreBtn');
      if(btn){ btn.disabled=true; btn.textContent='加载中…'; }
      requestAnimationFrame(()=>{
        const arr2=visibleCollections(); const step2=pageSize(); const from=renderLimit;
        renderLimit=Math.min(renderLimit+step2, arr2.length);
        const newItems=arr2.slice(from,renderLimit);
        const grid=$('content')?.querySelector('.poster-grid');
        if(grid && newItems.length){
          const t=document.createElement('template');
          t.innerHTML=newItems.map((c,i)=>cardHTML(c,from+i)).join('');
          grid.append(t.content);
          Array.from(grid.children).slice(from).forEach(el=>{
            if(!el.classList.contains('card')) return;
            el.onclick=()=>openDrawer(el.dataset.id);
            el.onmousemove=(e)=>{ const r=el.getBoundingClientRect(); el.style.setProperty('--mx',((e.clientX-r.left)/r.width*100).toFixed(1)+'%'); el.style.setProperty('--my',((e.clientY-r.top)/r.height*100).toFixed(1)+'%'); };
          });
          grid.querySelectorAll('img[data-id]:not([data-lz])').forEach(img=>{ img.dataset.lz='1'; getCachedImage(img.dataset.id).then(c=>{ if(c?.dataUrl) img.src=c.dataUrl; else if(c?.url) img.src=c.url; }).catch(()=>{}); });
          markCommentHoverFade();
        }
        const wrap=$('content')?.querySelector('.load-more-wrap');
        if(renderLimit>=arr2.length){ if(wrap) wrap.remove(); }
        else if(wrap){ const nb=wrap.querySelector('.btn'); if(nb){ nb.disabled=false; nb.textContent=`再显示 ${Math.min(step2,arr2.length-renderLimit)} 条（共 ${arr2.length} 条）`; } }
      });
    });
    $('content').querySelectorAll('.card').forEach(el => {
      el.onclick = () => openDrawer(el.dataset.id);
      el.onmousemove = (e) => { const r=el.getBoundingClientRect(); el.style.setProperty('--mx', ((e.clientX-r.left)/r.width*100).toFixed(1)+'%'); el.style.setProperty('--my', ((e.clientY-r.top)/r.height*100).toFixed(1)+'%'); };
    });
    const imgs = [...$('content').querySelectorAll('img[data-id]')];
    for(const img of imgs.slice(0, Math.min(pageSize()+80, 420))){
      getCachedImage(img.dataset.id).then(cache=>{ if(cache?.dataUrl) img.src = cache.dataUrl; else if(cache?.url) img.src = cache.url; }).catch(()=>{});
    }
    markCommentHoverFade();
  }
  function cardHTML(c, i=0){
    const img = coverUrl(c); const statusLabel = statusLabelFor(c.collection_type, c.subject_type); const tags=uniqStrings([...myTagsOf(c), ...publicTagNames(c)]).slice(0,5);
    const flag = c.status === 'missing_candidate' ? '<span class="pill danger">疑似消失</span>' : c.status === 'protected_local' ? '<span class="pill">本地保留</span>' : c.status === 'confirmed_deleted' ? '<span class="pill danger">已确认删除</span>' : '';
    const comment = c.comment ? `<div class="comment">${esc(c.comment)}</div>` : '<div class="comment no-comment">没有填写吐槽。</div>';
    return `<article class="card" data-id="${c.subject_id}" style="--i:${i}" title="${esc(c.title)}">
      <div class="card-glow"></div>
      <div class="cover-wrap"><div class="status-ribbon" data-ct="${c.collection_type}">${esc(statusLabel)}</div>${c.rate?`<div class="rate">★ ${esc(c.rate)}</div>`:''}${img?`<img class="cover" data-id="${c.subject_id}" src="${esc(img)}" loading="lazy" referrerpolicy="no-referrer" />`:`<div class="no-cover">${esc(c.title)}</div>`}</div>
      ${c.nsfw?`<div class="r18-badge">NSFW</div>`:''}
      <div class="card-body"><div class="name">${esc(c.title)}</div><div class="meta"><span>${esc(SUBJECT_LABEL[c.subject_type]||'条目')}</span>${c.date?`<span>${esc(c.date)}</span>`:''}${flag}</div>${comment}<div class="mini-tags">${tags.map(t=>`<span>${esc(t)}</span>`).join('')}</div></div>
    </article>`;
  }


  function numericValue(v){
    if(v === null || v === undefined || v === '') return 0;
    const n = Number(v);
    return Number.isFinite(n) ? n : 0;
  }
  function formatScoreValue(v){
    const n = numericValue(v);
    if(!n) return '';
    return Number.isInteger(n) ? String(n) : n.toFixed(1).replace(/\.0$/,'');
  }
  function episodeProgressHTML(c){
    const watched = Math.max(0, Math.floor(numericValue(c.ep_status)));
    const total = Math.max(0, Math.floor(numericValue(c.eps)));
    if(!watched && !total) return '';
    const count = total || watched;
    if(!count) return '';
    const visibleTotal = Math.min(count, 96);
    const cols = Math.max(8, Math.min(24, visibleTotal));
    const mobileCols = Math.max(8, Math.min(18, visibleTotal));
    const cells = Array.from({length: visibleTotal}, (_,i)=>`<span class="ep-cell-v9 ${i < watched ? 'watched' : ''}" title="第 ${i+1} 集 · ${i < watched ? '已看' : '未看'}"></span>`).join('');
    const more = count > visibleTotal ? ` · 显示前 ${visibleTotal} 格` : '';
    const percent = total ? Math.round(Math.min(watched,total)/total*100) : 100;
    return `<div class="episode-strip-v9 ${count>48?'compact':''}">
      <div class="episode-strip-head"><span>观看进度</span><span>${watched}${total?` / ${total}`:''} 集 · ${percent}%${more}</span></div>
      <div class="episode-grid-v9 ${count>48?'too-many':''}" style="--ep-cols:${cols};--ep-cols-mobile:${mobileCols}">${cells}</div>
    </div>`;
  }
  function scoreCardHTML(c){
    const personal = Math.max(0, Math.min(10, numericValue(c.rate)));
    const personalPct = Math.round(personal * 10);
    const avg = Math.max(0, Math.min(10, numericValue(c.bgm_score || c.raw?.subject?.score || c.raw?.subject?.rating?.score)));
    const avgPct = Math.round(avg * 10);
    const personalText = formatScoreValue(personal);
    const avgText = formatScoreValue(avg);
    const stars = Array.from({length:10}, (_,i)=>`<span class="${i < Math.round(personal) ? 'on' : ''}">★</span>`).join('');
    return `<div class="score-card-v8" style="--score-pct:${personalPct}%;--sub-pct:${avgPct}%">
      <div class="score-row-v8"><div class="score-big-v8 ${personal ? '' : 'score-empty-v9'}">${personal ? esc(personalText) + '<small>/10</small>' : '未评分'}</div><div class="score-word-v8">我的评分<br>${personal ? personalPct+'%' : 'NO RATE'}</div></div>
      <div class="stars-v8" aria-label="我的星星评分">${stars}</div>
      <div class="score-track-v8"><div class="score-fill-v8"></div></div>
      <div class="score-sub-v9"><div class="score-sub-row"><b>Bangumi</b><div class="sub-track"><div class="sub-fill"></div></div><span>${avg ? esc(avgText) + '/10' : '暂无'}</span></div></div>
      <div class="score-caption-v8"><span>${c.rank?`Bangumi 排名 #${esc(c.rank)}`:''}</span><span>Bangumi 平均分</span></div>
    </div>`;
  }

  // Render Bangumi infobox into an array of kv-row HTML strings. value is a string or an array of {v:...}.
  // Skips rows that exactly duplicate fields already shown in the 资料库信息 card (中文名 / 日期 / 平台).
  function infoboxRowList(list, c){
    if(!Array.isArray(list) || !list.length) return [];
    const dup = new Set([
      c?.name_cn ? `中文名 ${c.name_cn}` : '',
      c?.date ? `发行日期 ${c.date}` : '',
      c?.date ? `放送开始 ${c.date}` : '',
      c?.platform ? `平台 ${c.platform}` : '',
    ].filter(Boolean));
    return list.map(item=>{
      const k = String(item?.key ?? '').trim();
      let v = item?.value;
      if(Array.isArray(v)) v = v.map(x=>(x && typeof x==='object') ? (x.v ?? '') : x).filter(s=>String(s).trim()!=='').join(' / ');
      v = String(v ?? '').trim();
      if(!k && !v) return '';
      if(dup.has(`${k} ${v}`)) return '';
      return `<div>${esc(k)}</div><div>${esc(v)}</div>`;
    }).filter(Boolean);
  }

  // Build the 条目资料 card from rendered infobox rows (≤5 flat, >5 collapsible). '' when empty.
  function infoboxCardHTML(rows, id){
    const IB_HEAD = 5;
    if(!rows.length) return '';
    if(rows.length <= IB_HEAD) return `<div class="info-card"><h3>条目资料</h3><div class="kv">${rows.join('')}</div></div>`;
    return `<div class="info-card infobox-card"><h3>条目资料</h3><input type="checkbox" id="ibx-${esc(String(id))}" class="infobox-check" hidden><div class="kv">${rows.slice(0,IB_HEAD).join('')}</div><div class="kv infobox-extra">${rows.slice(IB_HEAD).join('')}</div><label class="infobox-toggle" for="ibx-${esc(String(id))}"></label></div>`;
  }

  async function openDrawer(id){
    activeDrawerId = id; const c = state.collections[id]; if(!c) return;
    const imgCache = await getCachedImage(id); const img = imgCache?.dataUrl || imgCache?.url || coverUrl(c);
    const hist = (await fetchSubjectHistory(id)).slice(-10).reverse();
    const cachedIb = infoboxCache[id]; const ibCard = Array.isArray(cachedIb) ? infoboxCardHTML(infoboxRowList(cachedIb, c), id) : '<div id="infoboxSlot"></div>';
    const myTags = myTagsOf(c);
    const publicTags = publicTagsOf(c);
    const publicTagsShown = publicTagsForDisplay(c);
    const myTagsHtml = myTags.length ? myTags.map(t=>`<button class="tag-chip tag-filter-btn my-tag" data-tag-key="${esc(tagKey('my', t))}"><span class="tag-kind">我的</span>${esc(t)}</button>`).join(' ') : '<span class="muted-inline">无</span>';
    const publicTagsHtml = publicTagsShown.length ? publicTagsShown.map(t=>`<span class="tag-pair"><button class="tag-chip tag-filter-btn public-tag" data-tag-key="${esc(tagKey('public', t.name))}"><span class="tag-kind">公共</span>${esc(t.name)}${t.count?` · ${esc(t.count)}`:''}</button><a class="tag-open" href="${esc(bgmTagUrl(c.subject_type, t.name))}" target="_blank" rel="noopener" title="打开 Bangumi 标签页">↗</a></span>`).join('') : '<span class="muted-inline">未缓存。可点“刷新公共标签”。</span>';
    const hiddenPublicTags = Math.max(0, publicTags.length - publicTagsShown.length);
    const publicTagsNote = publicTags.length
      ? `已缓存 ${publicTags.length} 个公共标签${c.public_tags_source === 'website' ? '。来源：Bangumi 条目页“大家将…标注为”区域' : '。来源：API ，可能与官网标签区不完全一致'}${c.public_tags_fetched_at ? '。刷新于 ' + esc(formatDate(c.public_tags_fetched_at)) : '。可点“刷新公共标签”重取。'}`
      : '公共标签来自 Bangumi 条目公开信息；在线版/网络异常时可能无法补全。';

    const statusPills = `<span class="pill">${esc(SUBJECT_LABEL[c.subject_type]||'条目')}</span><span class="pill">${esc(statusLabelFor(c.collection_type,c.subject_type))}</span>${c.nsfw?`<span class="pill bad">NSFW</span>`:''}${(c.platform && c.platform !== (SUBJECT_LABEL[c.subject_type]||''))?`<span class="pill">${esc(c.platform)}</span>`:''}${c.private?`<span class="pill">私密</span>`:''}${c.status==='missing_candidate'?`<span class="pill danger">安全箱 · 疑似消失</span>`:''}${c.mark_changed_at?`<span class="pill" style="opacity:.62;font-weight:600">标记于 ${esc(formatDate(c.mark_changed_at))}</span>`:''}`;
    const actions = `${c.status==='missing_candidate'?`<button class="btn good small wide" id="keepLocalBtn">本地保留</button><button class="btn bad small wide" id="confirmDeleteBtn">确认线上删除</button>`:''}${c.status==='confirmed_deleted'?`<button class="btn good small wide" id="restoreBtn">恢复显示</button>`:''}`;
    // Mobile (≤980px) detail layout: drop the standalone poster, move score + actions below the hero banner.
    const mobileDetail = window.matchMedia('(max-width:980px)').matches;
    const posterH = `<div class="detail-poster-frame">${img?`<img class="drawer-cover" src="${esc(img)}" referrerpolicy="no-referrer" />`:`<div class="drawer-cover"></div>`}</div>`;
    const scoreH = scoreCardHTML(c);
    const actionsH = `<div class="detail-actions-v8"><button class="btn small" id="openBgmBtn">打开 Bangumi</button><button class="btn small" id="copyCommentBtn">复制吐槽</button>${actions}</div>`;
    $('drawer').innerHTML = `<div class="detail-bg">${img?`<img src="${esc(img)}" referrerpolicy="no-referrer" />`:''}</div>
      <button class="btn small detail-close-v8" id="closeDrawerBtn">关闭</button>
      <div class="detail-v8">
        <aside class="detail-side-v8">${mobileDetail ? '' : posterH + scoreH + actionsH}</aside>
        <main class="detail-main-v8">
          <section class="detail-hero-v8" style="${img?`background-image:linear-gradient(90deg,rgba(8,9,15,.72),rgba(8,9,15,.30),rgba(8,9,15,.07)),url(&quot;${esc(img)}&quot;);background-size:cover;background-position:center;`:``}">
            <div class="detail-hero-content">
              <div class="detail-kicker-v8">Bangumi 本地备份详情</div>
              <h2>${esc(c.title)}</h2>
              <div class="detail-pills-v8">${statusPills}</div>
              ${episodeProgressHTML(c)}
            </div>
          </section>
          ${mobileDetail ? scoreH + actionsH : ''}
          <section class="detail-grid-v8">
            <div class="detail-stack">
              <div class="info-card"><h3>我的吐槽 / 观后感</h3><div class="longtext big-comment">${esc(c.comment || '没有填写吐槽。')}</div></div>
              ${c.summary?`<div class="info-card"><h3>条目简介</h3><div class="longtext">${esc(c.summary)}</div></div>`:''}
            </div>
            <div class="detail-stack">
              ${ibCard}<div class="info-card"><h3>资料库信息</h3><div class="kv"><div>条目 ID</div><div>${esc(c.subject_id||'-')}</div><div>原名</div><div>${esc(c.name||'-')}</div><div>中文名</div><div>${esc(c.name_cn||'-')}</div><div>上映日期</div><div>${esc(c.date||'-')}</div>${(c.platform && c.platform !== (SUBJECT_LABEL[c.subject_type]||''))?`<div>平台</div><div>${esc(c.platform)}</div>`:''}${c.subject_fetched_at?`<div>NSFW</div><div>${c.nsfw?'是':'否'}</div>`:''}<div>进度</div><div>${esc([c.ep_status?`章节 ${c.ep_status}`:'',c.vol_status?`卷 ${c.vol_status}`:''].filter(Boolean).join(' / ') || '-')}</div>${c.subject_fetched_at?`<div>条目详情获取</div><div>${esc(formatDate(c.subject_fetched_at))}</div>`:''}<div>条目最后更新时间</div><div>${esc(formatDate(c.bangumi_updated_at)||'-')}</div><div>首次备份</div><div>${esc(formatDate(c.first_seen_at)||'-')}</div><div>本地同步</div><div>${esc(formatDate(c.last_synced_at || c.last_seen_at)||'-')}</div><div>状态</div><div>${esc(c.status||'active')}${c.missing_count?`，缺失次数 ${c.missing_count}`:''}</div></div></div>
              <div class="info-card"><h3>我的标签</h3><div class="tag-cloud">${myTagsHtml}</div></div>
              <div class="info-card"><h3>公共标签</h3><div class="tag-cloud">${publicTagsHtml}</div><p class="help" style="margin:10px 0 0">${publicTagsNote}<br>点击标签在本地筛选；点 ↗ 打开 Bangumi 对应标签页。</p></div>
              <div class="info-card"><h3>备份历史</h3><div class="history-list">${hist.length?hist.map(h=>`<div class="hist"><b>${esc(h.change_type)}</b> · ${esc(formatDate(h.created_at))}<br>${historyChangeHtml(h)}</div>`).join(''):'<div class="hist">还没有历史变更。</div>'}</div></div>
            </div>
          </section>
        </main>
      </div>`;
    $('drawerMask').classList.remove('closing'); $('drawer').classList.remove('closing'); $('drawerMask').classList.add('show'); $('drawer').classList.add('show');
    $('closeDrawerBtn').onclick = closeDrawer; $('drawerMask').onclick = closeDrawer;
    $('drawer').querySelectorAll('[data-tag-key]').forEach(btn=>btn.onclick=(ev)=>{ ev.stopPropagation(); resetRenderLimit(); filters.tag = btn.dataset.tagKey; closeDrawer(); render(); });
    $('openBgmBtn').onclick = () => window.open(`${bgmSiteBase()}/subject/${c.subject_id}`,'_blank','noopener');
    $('copyCommentBtn').onclick = async () => { await navigator.clipboard?.writeText(c.comment||''); toast('已复制吐槽'); };
    const keep=$('keepLocalBtn'); if(keep) keep.onclick=async()=>{ await setCollectionStatus(id,'protected_local',0); render(); openDrawer(id); toast('已标记为本地保留'); };
    const del=$('confirmDeleteBtn'); if(del) del.onclick=async()=>{ if(confirm('确认把这个条目标记为线上已删除？历史和导出仍会保留。')){ await setCollectionStatus(id,'confirmed_deleted',(c.missing_count||0)+1); render(); openDrawer(id); } };
    const res=$('restoreBtn'); if(res) res.onclick=async()=>{ await setCollectionStatus(id,'active',0); render(); openDrawer(id); };
    // Infobox loads off the render path (its /api/subject-meta query is slow); fill + cache async so the drawer opens instantly.
    if(!Array.isArray(cachedIb)){
      fetchSubjectInfobox(id).then(list=>{
        infoboxCache[id] = list;
        if(activeDrawerId !== id) return;
        const slot = $('infoboxSlot');
        if(slot) slot.outerHTML = infoboxCardHTML(infoboxRowList(list, c), id);
      });
    }
    // Scroll the banner + columns together; once the banner is scrolled out, pin the grid so each
    // column scrolls independently with their tops aligned (.cols-pinned, see styles.css).
    const mainEl = $('drawer').querySelector('.detail-main-v8'), heroEl = $('drawer').querySelector('.detail-hero-v8'), gridEl = $('drawer').querySelector('.detail-grid-v8');
    if(mainEl && heroEl && gridEl){
      const threshold = Math.max(0, gridEl.offsetTop - heroEl.offsetTop);
      const syncCols = () => gridEl.classList.toggle('cols-pinned', mainEl.scrollTop >= threshold - 1);
      mainEl.addEventListener('scroll', syncCols, { passive: true });
      syncCols();
    }
  }

  function closeDrawer(){ const mask=$('drawerMask'), d=$('drawer'); if(!d.classList.contains('show')) return; d.classList.add('closing'); mask.classList.add('closing'); const ms=(state.settings.motion==='off')?0:190; setTimeout(()=>{ mask.classList.remove('show','closing'); d.classList.remove('show','closing'); activeDrawerId=null; }, ms); }
  function formatDate(s){ if(!s) return ''; const d=new Date(s); return isNaN(d)?String(s):d.toLocaleString(); }
  function renderStats(){
    const arr = getAllCollections(); const active = arr.filter(c=>c.status!=='confirmed_deleted'&&c.status!=='missing_candidate');
    $('statTotal').textContent = active.length; $('statMissing').textContent = arr.filter(c=>c.status==='missing_candidate').length;
    $('statComments').textContent = active.filter(c=>c.comment).length; const tagSet = new Set(); active.forEach(c=>allTagNames(c).forEach(t=>tagSet.add(t))); $('statTags').textContent = tagSet.size;
    $('statCommentsCard')?.classList.toggle('active', !!filters.hasComment);
    const acctUser = state.settings.username;
    $('accountPill').innerHTML = acctUser ? `<a href="${esc(bgmSiteBase())}/user/${encodeURIComponent(acctUser)}" target="_blank" rel="noopener">@${esc(acctUser)}</a>` : '未登录';
    const last = state.runs.filter(r=>r.success).at(-1); $('lastSyncText').textContent = last ? `上次同步：${formatDate(last.finished_at)}，新增 ${last.added||0}，修改 ${last.modified||0}，疑似消失 ${last.missing||0}` : '尚未同步';
    const pill=$('syncModePill');
    if(pill && last){ const t=last.trigger==='auto'?'定时':'手动'; const m=last.mode==='full'?'全局':'增量'; pill.textContent=`最近同步 · ${t}${m}`; }
  }
  function refreshNsfwBtn(){
    const btn=$('nsfwBtn'); if(!btn) return;
    const cfg={all:['NSFW',''],hide:['屏蔽 NSFW',' good'],only:['仅 NSFW',' bad']};
    const [label,mod]=cfg[filters.nsfw]||cfg.all;
    btn.textContent=label; btn.className=`btn small${mod}`;
  }
  function updateAutoSyncHelp(){
    const el=$('autoSyncHelp'); if(!el) return;
    const mode = state.settings.autoSyncMode || 'recent';
    const desc = (typeof cronDescribe === 'function') ? cronDescribe(state.settings.autoSyncCron || '0 2 * * *') : (state.settings.autoSyncCron || '');
    el.textContent = `${desc} · 自动${mode==='recent'?'增量':'完整'}同步`;
  }
  function applyAuthVisibility(){
    // admin-only actions — incl. the mobile drawer duplicates (mFullSync etc.) so guests can't see/trigger them
    ['syncBtn','fullSyncBtn','exportBtn','cacheCoversBtn','importBtn','enrichTagsBtn','mSyncRecent','mFullSync','mExport','mCacheCovers','mImport','mEnrichTags'].forEach(id=>{
      const el=$(id); if(el) el.classList.toggle('hidden',!authReady);
    });
    const sf=document.querySelector('.settings-footer');
    if(sf) sf.classList.toggle('hidden',!authReady);
    document.querySelectorAll('.settings-tab').forEach(t=>{
      const tab=t.dataset.tab;
      if(tab==='stab-appearance'){ t.classList.remove('hidden'); }
      else if(tab==='stab-login'){ t.classList.toggle('hidden',authReady); }
      else{ t.classList.toggle('hidden',!authReady); }
    });
    const nsfwHidden = isPublicMode && !authReady && !publicShowNsfw;
    $('nsfwBtn')?.classList.toggle('hidden', nsfwHidden);
  }
  function render(){ renderStats(); renderStatusNav(); renderTags(); renderCards(); refreshNsfwBtn(); applyAuthVisibility(); }

