← back to Ventura Corridor

public/magazine.html

550 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Ventura Corridor — the magazine</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
  @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400;1,500&family=Inter:wght@200;300;400;500;600&family=JetBrains+Mono:wght@200;300;400&display=swap');
  :root {
    --paper: #faf6ee;
    --ink: #1a1815;
    --ink-mute: #6e6356;
    --metal: #8a6d3b;
    --metal-glow: #b89968;
    --accent: #6a3a1a;
    --rule: #d8cdb8;
    --serif: 'Cormorant Garamond', 'Times New Roman', serif;
    --sans: 'Inter', -apple-system, system-ui, sans-serif;
    --mono: 'JetBrains Mono', ui-monospace, monospace;
  }
  html, body { margin: 0; background: var(--paper); color: var(--ink); font-family: var(--sans); font-weight: 300; min-height: 100vh; }
  * { box-sizing: border-box; }

  /* Masthead */
  header.masthead {
    text-align: center;
    padding: 56px 24px 32px;
    border-bottom: 4px double var(--ink);
    background: var(--paper);
  }
  header.masthead .kicker {
    font-family: var(--sans); font-weight: 500;
    font-size: 10px; letter-spacing: 0.4em; text-transform: uppercase;
    color: var(--metal); margin-bottom: 14px;
  }
  header.masthead h1 {
    font-family: var(--serif); font-style: italic; font-weight: 500;
    font-size: 72px; line-height: 0.95; margin: 0; letter-spacing: -0.02em;
    color: var(--ink);
  }
  header.masthead h1 em { font-style: normal; color: var(--metal); }
  header.masthead .deck {
    font-family: var(--serif); font-weight: 300; font-style: italic;
    font-size: 17px; color: var(--ink-mute); margin-top: 14px;
    max-width: 540px; margin-left: auto; margin-right: auto; line-height: 1.5;
  }
  header.masthead .issue {
    margin-top: 20px;
    font-family: var(--mono); font-size: 9px; letter-spacing: 0.3em;
    text-transform: uppercase; color: var(--ink-mute);
  }

  /* Navigation rail (admin links — collapsed by default) */
  nav.admin-rail {
    background: rgba(0,0,0,0.04);
    padding: 10px 24px;
    text-align: center;
    border-bottom: 1px solid var(--rule);
  }
  nav.admin-rail a, nav.admin-rail span {
    font-size: 9px; letter-spacing: 0.22em; text-transform: uppercase;
    color: var(--ink-mute); margin: 0 8px; text-decoration: none;
  }
  nav.admin-rail a:hover { color: var(--metal); }

  /* Filter chips */
  .chips {
    display: flex; gap: 8px; flex-wrap: wrap; justify-content: center;
    padding: 18px 24px; border-bottom: 1px solid var(--rule);
  }
  .chips button {
    background: transparent; border: 1px solid var(--rule);
    color: var(--ink-mute); font-size: 9px; letter-spacing: 0.22em;
    text-transform: uppercase; padding: 5px 12px; cursor: pointer;
    font-family: var(--sans);
  }
  .chips button.active { color: var(--ink); border-color: var(--metal); background: rgba(184,153,104,0.08); }
  .chips button:hover { color: var(--ink); border-color: var(--metal); }

  /* Magazine grid */
  main.spread {
    max-width: 1240px; margin: 0 auto; padding: 32px 24px 80px;
    display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 36px;
  }

  article.feature {
    background: var(--paper);
    padding: 8px 0;
    position: relative;
    break-inside: avoid;
  }
  article.feature .photo-frame {
    aspect-ratio: 4/3;
    background:
      repeating-linear-gradient(45deg, rgba(0,0,0,0.04) 0 8px, transparent 8px 16px),
      linear-gradient(135deg, #c8b894 0%, #8a7754 100%);
    border: 1px solid var(--rule);
    display: flex; align-items: center; justify-content: center;
    color: var(--paper);
    font-family: var(--serif); font-style: italic; font-size: 18px;
    margin-bottom: 14px;
    position: relative;
    overflow: hidden;
  }
  /* Per-category mood gradients — distinct identity per vertical, no external imagery */
  article.feature .photo-frame.cat-restaurant   { background: linear-gradient(135deg, #8a3a1a 0%, #c47b5a 60%, #f4d6b8 100%); }
  article.feature .photo-frame.cat-restaurant::before { content:'🍴'; position:absolute; right:14px; bottom:14px; font-size:48px; opacity:.18; z-index:1 }
  article.feature .photo-frame.cat-professional { background: linear-gradient(135deg, #2d3a4a 0%, #5a6c7d 50%, #8a9ba8 100%); }
  article.feature .photo-frame.cat-professional::before { content:'⚖'; position:absolute; right:14px; bottom:14px; font-size:54px; opacity:.18; z-index:1 }
  article.feature .photo-frame.cat-medical      { background: linear-gradient(135deg, #2a5a4a 0%, #5a8a7a 50%, #b0d4c4 100%); }
  article.feature .photo-frame.cat-medical::before { content:'+'; position:absolute; right:18px; bottom:6px; font-size:140px; opacity:.10; z-index:1; font-weight:300; line-height:1 }
  article.feature .photo-frame.cat-beauty       { background: linear-gradient(135deg, #6a3a5a 0%, #a76a8a 50%, #e8c4d4 100%); }
  article.feature .photo-frame.cat-beauty::before { content:'✦'; position:absolute; right:14px; bottom:14px; font-size:54px; opacity:.20; z-index:1 }
  article.feature .photo-frame.cat-salon        { background: linear-gradient(135deg, #5a2a4a 0%, #8a5a7a 50%, #d4a4c4 100%); }
  article.feature .photo-frame.cat-salon::before { content:'§'; position:absolute; right:14px; bottom:14px; font-size:60px; opacity:.18; z-index:1; font-style:italic }
  article.feature .photo-frame.cat-fitness      { background: linear-gradient(135deg, #2a3a1a 0%, #5a7a3a 50%, #a4c47a 100%); }
  article.feature .photo-frame.cat-fitness::before { content:'△'; position:absolute; right:14px; bottom:14px; font-size:60px; opacity:.20; z-index:1 }
  article.feature .photo-frame.cat-shop         { background: linear-gradient(135deg, #4a2a1a 0%, #8a5a3a 50%, #d4a48a 100%); }
  article.feature .photo-frame.cat-shop::before { content:'◊'; position:absolute; right:14px; bottom:14px; font-size:54px; opacity:.20; z-index:1 }
  article.feature .photo-frame.cat-real-estate  { background: linear-gradient(135deg, #1a3a4a 0%, #3a6a8a 50%, #8aa4c4 100%); }
  article.feature .photo-frame.cat-real-estate::before { content:'◮'; position:absolute; right:14px; bottom:14px; font-size:60px; opacity:.18; z-index:1 }
  article.feature .photo-frame.cat-hospitality  { background: linear-gradient(135deg, #4a3a1a 0%, #8a6a3a 50%, #d4b48a 100%); }
  article.feature .photo-frame.cat-hospitality::before { content:'※'; position:absolute; right:14px; bottom:14px; font-size:60px; opacity:.20; z-index:1 }
  article.feature .photo-frame.cat-automotive   { background: linear-gradient(135deg, #2a2a2a 0%, #5a5a5a 50%, #a4a4a4 100%); }
  article.feature .photo-frame.cat-automotive::before { content:'⊙'; position:absolute; right:14px; bottom:14px; font-size:60px; opacity:.20; z-index:1 }
  article.feature .photo-frame .placeholder-text {
    text-align: center; padding: 12px; opacity: 0.85;
    text-shadow: 0 1px 2px rgba(0,0,0,0.3);
    font-size: 13px; letter-spacing: 0.05em;
  }
  article.feature .photo-frame::after {
    content: '';
    position: absolute; inset: 0;
    background: linear-gradient(180deg, transparent 60%, rgba(0,0,0,0.4));
  }
  article.feature .photo-frame .cat-tag {
    position: absolute; top: 12px; left: 12px;
    background: var(--ink); color: var(--paper);
    padding: 4px 10px; font-size: 8px; letter-spacing: 0.3em;
    text-transform: uppercase; font-family: var(--sans); font-weight: 500;
    z-index: 2;
  }

  article.feature h2 {
    font-family: var(--serif); font-weight: 500; font-style: italic;
    font-size: 30px; line-height: 1.05; margin: 0 0 8px;
    letter-spacing: -0.01em;
    color: var(--ink);
  }
  article.feature .subhead {
    font-family: var(--serif); font-weight: 300; font-style: italic;
    font-size: 16px; color: var(--ink-mute); margin: 0 0 16px;
    line-height: 1.4;
  }
  article.feature .lede {
    font-family: var(--serif); font-weight: 400;
    font-size: 16px; line-height: 1.65;
    color: var(--ink); margin: 0 0 16px;
  }
  article.feature .lede::first-letter {
    font-family: var(--serif); font-weight: 600;
    font-size: 52px; line-height: 0.9; float: left;
    margin: 4px 8px 0 0; color: var(--accent);
  }
  article.feature .pull {
    font-family: var(--serif); font-style: italic; font-weight: 400;
    font-size: 19px; line-height: 1.35;
    color: var(--accent);
    border-left: 2px solid var(--metal);
    padding: 8px 0 8px 16px;
    margin: 14px 0;
  }
  article.feature footer {
    margin-top: 16px; padding-top: 12px;
    border-top: 1px solid var(--rule);
    display: flex; justify-content: space-between; align-items: baseline;
    gap: 12px; flex-wrap: wrap;
  }
  article.feature footer .biz {
    font-family: var(--sans); font-weight: 500; font-size: 11px;
    color: var(--ink); letter-spacing: 0.02em;
  }
  article.feature footer .addr {
    font-family: var(--mono); font-size: 9px; letter-spacing: 0.08em;
    color: var(--ink-mute);
  }
  article.feature footer .actions {
    display: flex; gap: 6px;
  }
  article.feature footer .actions button {
    background: transparent; border: 1px solid var(--rule); color: var(--ink-mute);
    font-size: 8px; letter-spacing: 0.22em; text-transform: uppercase;
    padding: 3px 9px; cursor: pointer; font-family: var(--sans);
  }
  article.feature footer .actions button:hover { color: var(--metal); border-color: var(--metal); }
  article.feature footer .actions button.publish { color: var(--accent); border-color: var(--accent); }
  article.feature footer .actions button.regen { color: var(--metal); }

  article.feature.s-published { border-top: 3px solid var(--accent); padding-top: 14px; }
  article.feature.s-spiked { opacity: 0.4; }

  .empty { text-align: center; padding: 80px 24px; color: var(--ink-mute); font-style: italic; font-family: var(--serif); font-size: 22px; }
  .loading { text-align: center; padding: 60px; color: var(--ink-mute); font-size: 11px; letter-spacing: .26em; text-transform: uppercase; }

  /* Print */
  @media print {
    nav.admin-rail, .chips, article.feature footer .actions { display: none; }
    article.feature { break-inside: avoid; page-break-inside: avoid; }
  }
</style>
</head>
<body>

<header class="masthead">
  <div class="kicker">Volume I · Spring Issue · 2026</div>
  <h1>The <em>Corridor</em></h1>
  <div class="deck">A magazine of who's here. The corridor's restaurants, professionals, ateliers, and ateliers-disguised-as-storefronts — written warm.</div>
  <div class="issue" id="issue-stat">— · — · —</div>
</header>

<nav class="admin-rail">
  <span style="color:var(--ink-mute)">admin:</span>
  <a href="/today.html">today</a>
  <a href="/pitches.html">pitches</a>
  <a href="/buildings.html">buildings</a>
  <a href="/walk-route.html">walk route</a>
  <a href="/responses.html">responses</a>
  <a href="/postcards.html">postcards</a>
  <a href="/magazine.html" style="color:var(--metal-glow);font-weight:500">magazine</a>
  <a href="/issue" style="color:var(--accent)">★ public issue</a>
  <a href="/scoreboard.html">🏆 scoreboard</a>
  <a href="/search.html">🔍 search</a>
  <a href="/rate-card.html">💵 rate card</a>
  <a href="/crawl-derby.html">🏇 derby</a>
</nav>

<div class="chips">
  <button data-status="all" class="active">All features</button>
  <button data-status="draft">⚙ Drafts</button>
  <button data-status="reviewed">✓ Reviewed</button>
  <button data-status="published">★ Published</button>
</div>
<div class="chips" id="cat-chips" style="border-bottom:1px solid var(--rule);padding-top:0">
  <span style="color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;align-self:center;margin-right:8px">vertical →</span>
  <!-- chips injected by JS -->
</div>
<div class="chips" style="padding-top:0;border-bottom:1px solid var(--rule);align-items:center">
  <span style="color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;align-self:center;margin-right:8px">candidates →</span>
  <button id="ads-only-btn" onclick="toggleAdsOnly()">$ paid-ad runners only</button>
  <input id="search-box" type="search" placeholder="search businesses, headlines…" oninput="onSearch()" style="background:transparent;border:1px solid var(--rule);color:var(--ink);padding:5px 12px;font-size:11px;font-family:var(--sans);min-width:240px;margin-left:auto">
  <button onclick="toggleView()" id="view-btn">📑 Table of contents</button>
  <button onclick="reviewDrafts()" style="border:1px solid var(--metal);color:var(--metal)">⚙→✓ Review drafts</button>
  <button onclick="publishReviewed()" style="border:1px solid var(--metal-glow);color:var(--metal-glow)">★ Publish reviewed</button>
  <button id="since-last-btn" onclick="toggleSinceLast()" title="Show only features generated since you last opened this page">🆕 since last</button>
  <a href="/issue" target="_blank" style="display:inline-block;background:transparent;border:1px solid var(--accent);color:var(--accent);font-size:9px;letter-spacing:.22em;text-transform:uppercase;padding:5px 12px;text-decoration:none;font-family:var(--sans)">↗ Public issue</a>
</div>

<main class="spread" id="spread">
  <div class="loading">Loading the issue…</div>
</main>

<script>
function escHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])); }
let currentStatus = 'all';
let currentCat    = 'all';
let adsOnly       = false;
let searchTerm    = '';
let viewMode      = 'cards'; // 'cards' | 'toc'
let sinceLast     = false;
const SINCE_LAST_KEY = 'corridor-magazine-last-seen';
const SINCE_LAST_MARK_AT = Date.now();
const _lastSeenAt = (() => { try { return parseInt(localStorage.getItem(SINCE_LAST_KEY) || '0', 10); } catch { return 0; } })();
// Stamp current visit at end-of-load so "since last" filters anything older than the PREVIOUS visit.
function stampSeen() { try { localStorage.setItem(SINCE_LAST_KEY, String(SINCE_LAST_MARK_AT)); } catch {} }
let _searchT;
function onSearch() {
  clearTimeout(_searchT);
  _searchT = setTimeout(() => { searchTerm = document.getElementById('search-box').value.trim().toLowerCase(); load(); }, 200);
}
function toggleView() {
  viewMode = viewMode === 'cards' ? 'toc' : 'cards';
  document.getElementById('view-btn').textContent = viewMode === 'cards' ? '📑 Table of contents' : '📰 Card view';
  load();
}
function toggleAdsOnly() {
  adsOnly = !adsOnly;
  const btn = document.getElementById('ads-only-btn');
  btn.classList.toggle('active', adsOnly);
  load();
}
function toggleSinceLast() {
  sinceLast = !sinceLast;
  const btn = document.getElementById('since-last-btn');
  btn.classList.toggle('active', sinceLast);
  load();
}

const CATS = [
  { id: 'all',          label: 'All' },
  { id: 'restaurant',   label: '🍴 Restaurants' },
  { id: 'professional', label: '⚖️ Professional' },
  { id: 'beauty',       label: '✨ Beauty' },
  { id: 'salon',        label: '💇 Salon' },
  { id: 'medical',      label: '🩺 Medical' },
  { id: 'fitness',      label: '💪 Fitness' },
  { id: 'shop',         label: '🛍 Shops' },
  { id: 'real-estate',  label: '🏠 Real estate' },
  { id: 'hospitality',  label: '🏨 Hospitality' },
  { id: 'automotive',   label: '🚗 Automotive' },
];
function renderCatChips() {
  const root = document.getElementById('cat-chips');
  // keep the leading label span if it's already there
  const lead = root.querySelector('span') ? root.querySelector('span').outerHTML : '';
  root.innerHTML = lead + CATS.map(c =>
    `<button data-cat="${c.id}" class="${currentCat === c.id ? 'active' : ''}">${c.label}</button>`
  ).join('');
  root.querySelectorAll('button').forEach(b => {
    b.addEventListener('click', () => { currentCat = b.dataset.cat; renderCatChips(); load(); });
  });
}
renderCatChips();

async function load() {
  try {
    const params = new URLSearchParams();
    if (currentStatus !== 'all') params.set('status', currentStatus);
    if (currentCat !== 'all')    params.set('cat',    currentCat);
    const url = params.toString() ? '/api/magazine?' + params : '/api/magazine';
    const data = await fetch(url).then(r => r.json());
    let rows = data.rows || [];
    if (adsOnly) rows = rows.filter(r => Number(r.paid_ads_count || 0) > 0);
    if (sinceLast && _lastSeenAt) {
      rows = rows.filter(r => new Date(r.generated_at).getTime() > _lastSeenAt);
    }
    if (searchTerm) {
      rows = rows.filter(r => {
        const hay = ((r.name || '') + ' ' + (r.headline || '') + ' ' + (r.editorial || '') + ' ' + (r.address || '') + ' ' + (r.city || '') + ' ' + (r.naics || '')).toLowerCase();
        return hay.includes(searchTerm);
      });
    }
    const counts = data.by_status || [];
    const total = counts.reduce((a, x) => a + Number(x.n), 0);
    const draftN = (counts.find(x => x.status === 'draft') || {}).n || 0;
    const pubN   = (counts.find(x => x.status === 'published') || {}).n || 0;
    document.getElementById('issue-stat').textContent =
      `${total} features · ${draftN} drafts awaiting review · ${pubN} published`;
    const main = document.getElementById('spread');
    if (!rows.length) {
      main.innerHTML = `<div class="empty">No features yet.<br><br><span style="font-size:14px;font-style:normal;color:var(--metal)">Click "+ Generate 5 more" to seed the issue.</span></div>`;
      return;
    }
    if (viewMode === 'toc') {
      // Group by category, sorted alphabetically; show TOC list
      const byCat = {};
      for (const r of rows) {
        const c = r.category_tag || 'uncategorized';
        (byCat[c] = byCat[c] || []).push(r);
      }
      const cats = Object.keys(byCat).sort();
      main.style.display = 'block';
      main.style.maxWidth = '720px';
      main.style.margin = '0 auto';
      main.innerHTML = cats.map(cat => `
        <div style="margin-bottom:36px">
          <h3 style="font-family:var(--serif);font-style:italic;font-weight:500;font-size:24px;color:var(--metal);border-bottom:1px solid var(--rule);padding-bottom:6px;margin:0 0 14px;text-transform:capitalize">${escHtml(cat.replace(/-/g, ' '))} <span style="color:var(--ink-mute);font-style:normal;font-size:12px;letter-spacing:.2em;margin-left:10px">${byCat[cat].length}</span></h3>
          ${byCat[cat].sort((a,b) => (a.name || '').localeCompare(b.name || '')).map(r => `
            <a href="/magazine/${r.id}" style="display:grid;grid-template-columns:1fr auto;gap:14px;padding:7px 0;border-bottom:1px dotted var(--rule);text-decoration:none;color:var(--ink);font-size:14px">
              <div>
                <span style="font-family:var(--serif);font-style:italic;font-size:18px;color:var(--ink)">${escHtml(r.headline || r.name)}</span>
                <span style="color:var(--ink-mute);font-size:11px;margin-left:8px">${escHtml(r.name)}${r.paid_ads_count > 0 ? ` <span style="color:var(--accent);font-weight:500">· $</span>` : ''}</span>
              </div>
              <span style="color:var(--ink-mute);font-family:var(--mono);font-size:10px;align-self:center">${escHtml((r.address || '').split(' ').slice(0, 2).join(' '))}</span>
            </a>
          `).join('')}
        </div>
      `).join('');
      return;
    }
    main.style.display = '';
    main.style.maxWidth = '';
    main.style.margin = '';
    main.innerHTML = rows.map(r => {
      const cat = (r.category_tag || 'shop').toUpperCase();
      const adsCount = Number(r.paid_ads_count || 0);
      const adsBadge = adsCount > 0
        ? `<span style="position:absolute;top:12px;right:12px;background:var(--accent);color:var(--paper);padding:4px 10px;font-size:8px;letter-spacing:.3em;text-transform:uppercase;font-family:'Inter',sans-serif;font-weight:500;z-index:2" title="${adsCount} paid-ad pixel${adsCount===1?'':'s'} detected — proven ad buyer, candidate sponsored placement">$ ${adsCount}× ads</span>`
        : '';
      return `
      <article class="feature s-${r.status}" data-id="${r.id}">
        <div class="photo-frame cat-${escHtml(r.category_tag || 'shop')}">
          <span class="cat-tag">${escHtml(cat)}</span>
          ${adsBadge}
          <div class="placeholder-text" style="z-index:2;position:relative">${escHtml(r.name)}</div>
        </div>
        <h2><a href="/magazine/${r.id}" style="color:inherit;text-decoration:none">${escHtml(r.headline || '(untitled)')}</a></h2>
        <div class="subhead">${escHtml(r.subhead || '')}</div>
        <p class="lede">${escHtml(r.editorial || '')}</p>
        ${r.pull_quote ? `<div class="pull">"${escHtml(r.pull_quote)}"</div>` : ''}
        <div style="display:flex;gap:14px;font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:.1em;color:var(--ink-mute);margin-top:10px;border-top:1px dotted var(--rule);padding-top:8px">
          <span title="Generated">⏱ ${new Date(r.generated_at).toLocaleDateString('en-US',{month:'short',day:'numeric'})}</span>
          ${r.views > 0 ? `<span title="Reader-page views" style="color:var(--metal)">👁 ${r.views}</span>` : ''}
          ${r.taps > 0 ? `<span title="Outbound taps (share/sponsor/etc)" style="color:var(--metal-glow)">↗ ${r.taps}</span>` : ''}
          ${r.editorial ? `<span title="Editorial length">${r.editorial.length}c</span>` : ''}
          <span style="color:var(--ink-mute)">${escHtml(r.model || '')}</span>
        </div>
        <details class="margin-note" ${r.notes ? 'open' : ''} style="margin-top:10px;font-family:'Cormorant Garamond',serif;font-style:italic;font-size:13px;color:var(--ink-mute);border-top:1px dotted var(--rule);padding-top:8px">
          <summary style="cursor:pointer;font-family:'Inter',sans-serif;font-style:normal;font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:${r.notes ? 'var(--metal)' : 'var(--ink-mute)'};font-weight:500;outline:none">${r.notes ? '✎ note' : '✎ add note'}</summary>
          <textarea data-feature-id="${r.id}" oninput="this.dataset.dirty='1'" onblur="saveNote(this)" placeholder="margin note (private editor's note)" rows="2" style="width:100%;margin-top:6px;background:transparent;color:var(--ink);border:1px solid var(--rule);padding:6px 8px;font-family:'Cormorant Garamond',serif;font-style:italic;font-size:13px;resize:vertical">${escHtml(r.notes || '')}</textarea>
        </details>
        <footer>
          <div>
            <div class="biz">${escHtml(r.name)}</div>
            <div class="addr">${escHtml(r.address || '')} · ${escHtml(r.city || '')}</div>
          </div>
          <div class="actions">
            ${r.status === 'draft' ? `<button onclick="advance(${r.id}, 'reviewed')">✓ Mark reviewed</button>` : ''}
            ${r.status === 'reviewed' ? `<button class="publish" onclick="advance(${r.id}, 'published')">★ Publish</button>` : ''}
            ${r.status === 'published' ? `<button onclick="setCover(${r.id})" style="border:1px solid var(--metal-glow);color:var(--metal-glow)" title="Set as this month's cover">⛧ Cover</button>` : ''}
            ${r.status !== 'spiked' ? `<button onclick="advance(${r.id}, 'spiked')">✕ Spike</button>` : `<button onclick="advance(${r.id}, 'draft')">↶ Restore</button>`}
            <button class="regen" onclick="regen(${r.id})">↻ Regen</button>
          </div>
        </footer>
      </article>`;
    }).join('');
    // Mark seen AFTER first successful render so the badge reflects truly-new gen on next visit.
    if (!window._stamped) { stampSeen(); window._stamped = true; }
    // Update the "since last" button label with the unseen count
    const sinceBtn = document.getElementById('since-last-btn');
    if (sinceBtn && _lastSeenAt) {
      const newCount = (data.rows || []).filter(r => new Date(r.generated_at).getTime() > _lastSeenAt).length;
      sinceBtn.textContent = newCount > 0 ? `🆕 since last (${newCount})` : '🆕 since last';
      sinceBtn.style.color = newCount > 0 ? 'var(--metal-glow)' : '';
      sinceBtn.style.borderColor = newCount > 0 ? 'var(--metal-glow)' : '';
    }
  } catch (e) {
    document.getElementById('spread').innerHTML = `<div class="empty">load failed: ${e.message}</div>`;
  }
}

async function advance(id, status) {
  try {
    const r = await fetch('/api/magazine/' + id, {
      method: 'PATCH', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ status })
    });
    if (!r.ok) throw new Error(await r.text());
    load();
  } catch (e) { alert('failed: ' + e.message); }
}

async function saveNote(el) {
  if (el.dataset.dirty !== '1') return;
  const id = Number(el.dataset.featureId);
  try {
    const r = await fetch('/api/magazine/' + id, {
      method: 'PATCH', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ notes: el.value || '' })
    });
    if (!r.ok) throw new Error(await r.text());
    el.dataset.dirty = '0';
    const summary = el.parentElement.querySelector('summary');
    if (summary) {
      summary.style.color = el.value ? 'var(--metal)' : 'var(--ink-mute)';
      summary.textContent = el.value ? '✎ note' : '✎ add note';
    }
  } catch (e) { console.error('saveNote', e); }
}

async function setCover(id) {
  try {
    const month = new Date().toISOString().slice(0, 7);
    const caption = prompt(`Cover for ${month} → feature ${id}\n\nOptional cover-page caption (leave blank to use the feature's subhead):`, '') || null;
    const r = await fetch(`/api/issues/${month}`, {
      method: 'PATCH', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ cover_feature_id: id, cover_caption: caption, cover_kicker: 'On the cover this month' })
    });
    if (!r.ok) throw new Error(await r.text());
    alert(`Set as cover for ${month}. View at /issue.`);
  } catch (e) { alert('failed: ' + e.message); }
}

async function regen(featureId) {
  if (!confirm('Regenerate via Mac1 Ollama? Takes ~30s.')) return;
  // Find the article + show a spinner state on its regen button
  const art = document.querySelector(`article.feature[data-id="${featureId}"]`);
  if (art) {
    const btn = art.querySelector('button.regen');
    if (btn) { btn.textContent = '⟳ regenerating…'; btn.disabled = true; }
  }
  try {
    const r = await fetch('/api/magazine/' + featureId + '/regen', { method: 'POST' });
    if (!r.ok) throw new Error(await r.text());
    const d = await r.json();
    console.log('regen ok in', d.ms, 'ms →', d.headline);
    load();
  } catch (e) {
    alert('Regen failed: ' + e.message);
    if (art) {
      const btn = art.querySelector('button.regen');
      if (btn) { btn.textContent = '↻ Regen'; btn.disabled = false; }
    }
  }
}

async function generateMore() {
  alert('Run in terminal:\n\nnpm run magazine:gen 5\n\nTakes ~2 minutes for 5 features. Then click any chip to refresh.');
}

async function publishReviewed() {
  if (!confirm('Publish ALL features currently in "reviewed" status? They will appear on the public /issue page.')) return;
  try {
    const r = await fetch('/api/magazine/publish-reviewed', { method: 'POST' });
    if (!r.ok) throw new Error(await r.text());
    const d = await r.json();
    alert(`Published ${d.published} feature${d.published === 1 ? '' : 's'} → /issue`);
    load();
  } catch (e) { alert('Publish failed: ' + e.message); }
}

async function reviewDrafts() {
  const cat = currentCat === 'all' ? null : currentCat;
  const scopeMsg = cat ? `category "${cat}"` : 'ALL categories';
  if (!confirm(`Move drafts → reviewed in ${scopeMsg}? Only features with editorial ≥120 chars qualify.`)) return;
  try {
    const r = await fetch('/api/magazine/review-drafts', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ cat, min_len: 120 })
    });
    if (!r.ok) throw new Error(await r.text());
    const d = await r.json();
    alert(`Reviewed ${d.reviewed} feature${d.reviewed === 1 ? '' : 's'} (${d.scope})`);
    load();
  } catch (e) { alert('Review failed: ' + e.message); }
}

document.querySelectorAll('.chips button[data-status]').forEach(b => {
  b.addEventListener('click', () => {
    document.querySelectorAll('.chips button[data-status]').forEach(x => x.classList.remove('active'));
    b.classList.add('active');
    currentStatus = b.dataset.status;
    load();
  });
});

load();
</script>
</body>
</html>