<script>
  import { onMount } from 'svelte';
  import { goto } from '$app/navigation';
  import { page } from '$app/stores';
  import { getJSON, postJSON, delJSON } from '$lib/api.js';
  import { auth, refresh } from '$lib/auth.svelte.js';

  let stats = $state(null);
  let feedback = $state([]);
  let error = $state('');

  // Analytics window (days). Windowed metrics — visitors, retention, funnel,
  // sharing, daily trend, top lists — rescale to this; corpus totals don't.
  let range = $state(30);
  async function loadStats() {
    stats = await getJSON('/api/admin/stats?days=' + range);
  }
  async function setRange(n) {
    if (n === range) return;
    range = n;
    try { await loadStats(); } catch { error = "Couldn't load stats."; }
  }

  onMount(async () => {
    if (!auth.ready) await refresh();
    if (!auth.user || !auth.user.is_admin) {
      goto('/', { replaceState: true });
      return;
    }
    try {
      await loadStats();
      feedback = await getJSON('/api/admin/feedback');
      candidates = await getJSON('/api/admin/candidates');
    } catch {
      error = "Couldn't load stats.";
    }
  });

  const TABS = [
    { key: 'overview', label: 'Overview' },
    { key: 'content', label: 'Content' },
    { key: 'sources', label: 'Sources' },
    { key: 'audience', label: 'Audience' },
    { key: 'feedback', label: 'Feedback' },
  ];
  const VALID_SECTIONS = new Set(TABS.map((t) => t.key));
  // Unknown ?section= values fall back to Overview so the page never renders blank.
  let section = $derived(
    VALID_SECTIONS.has($page.url.searchParams.get('section')) ? $page.url.searchParams.get('section') : 'overview'
  );

  function fdate(s) {
    try { return new Date(s + 'Z').toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); }
    catch { return s; }
  }
  // A UTC "YYYY-MM-DD HH:MM:SS" stamp → the viewer's local date + time.
  function fwhen(s) {
    if (!s) return 'now';
    const d = new Date(s.replace(' ', 'T') + 'Z');
    if (isNaN(d)) return s;
    return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
  }

  let sources = $derived(stats?.sources ?? []);
  // status falls back to the legacy active flag during the migration window.
  const sstatus = (s) => s.status || (s.active ? 'active' : 'paused');
  // A 429 rest window that hasn't elapsed — polite wait, not breakage.
  function rateLimited(s) {
    if (!s.retry_after_at) return false;
    const d = new Date(s.retry_after_at.replace(' ', 'T') + 'Z');
    return !isNaN(d) && d > new Date();
  }
  let healthy = $derived(sources.filter((s) => sstatus(s) === 'active' && !s.failures && !s.review_flag).length);
  let resting = $derived(sources.filter((s) => sstatus(s) === 'active' && s.failures > 0).length);
  let flagged = $derived(sources.filter((s) => s.review_flag).length);
  let paused = $derived(sources.filter((s) => sstatus(s) === 'paused').length);
  let retired = $derived(sources.filter((s) => sstatus(s) === 'retired').length);

  // Sources table filter.
  let srcFilter = $state('all');
  let shownSources = $derived(
    srcFilter === 'healthy' ? sources.filter((s) => sstatus(s) === 'active' && !s.failures && !s.review_flag)
      : srcFilter === 'resting' ? sources.filter((s) => sstatus(s) === 'active' && s.failures > 0)
        : srcFilter === 'flagged' ? sources.filter((s) => s.review_flag)
          : srcFilter === 'paused' ? sources.filter((s) => sstatus(s) === 'paused')
            : srcFilter === 'retired' ? sources.filter((s) => sstatus(s) === 'retired')
              : sources
  );

  // Lifecycle (status: active/paused/retired) keeps `active` mirrored server-side;
  // visibility hides existing articles. Both optimistic with revert-on-failure.
  async function setStatus(s, status) {
    const prev = { status: s.status, active: s.active };
    s.status = status; s.active = status === 'active' ? 1 : 0;
    try { await postJSON(`/api/admin/sources/${s.id}/status`, { status }); }
    catch { s.status = prev.status; s.active = prev.active; }
  }
  async function toggleVisible(s) {
    const next = !s.content_visible;
    if (!next && !confirm(`Hide all of “${s.name}”’s articles from the public feed and brief?\n(Reversible — nothing is deleted.)`)) return;
    const prev = s.content_visible;
    s.content_visible = next ? 1 : 0;
    try { await postJSON(`/api/admin/sources/${s.id}/visibility`, { visible: next }); }
    catch { s.content_visible = prev; }
  }
  // Flagging opens a small inline popover for the reason; clearing is immediate.
  let flagging = $state(null); // the source being flagged
  let flagReason = $state('');
  function openFlag(s) { flagging = s; flagReason = ''; }
  function cancelFlag() { flagging = null; flagReason = ''; }
  async function confirmFlag() {
    const s = flagging;
    const reason = flagReason.trim() || null;
    s.review_flag = 1; s.review_reason = reason; // optimistic
    flagging = null; flagReason = '';
    try { await postJSON(`/api/admin/sources/${s.id}/review`, { flag: true, reason }); }
    catch { s.review_flag = 0; s.review_reason = null; }
  }
  async function clearReview(s) {
    const prevR = s.review_reason;
    s.review_flag = 0; s.review_reason = null; // optimistic
    try { await postJSON(`/api/admin/sources/${s.id}/review`, { flag: false }); }
    catch { s.review_flag = 1; s.review_reason = prevR; }
  }

  // --- Source candidates: supervised "add a source" pipeline ---
  let candidates = $state([]);
  let newFeedUrl = $state('');
  let newFeedName = $state('');
  let addBusy = $state(false);
  let addErr = $state('');
  let pendingCandidates = $derived(candidates.filter((c) => c.status !== 'promoted' && c.status !== 'rejected'));

  async function addCandidate() {
    const url = newFeedUrl.trim();
    if (!url || addBusy) return;
    addBusy = true; addErr = '';
    try {
      const c = await postJSON('/api/admin/candidates', { feed_url: url, name: newFeedName.trim() || null });
      candidates = [c, ...candidates.filter((x) => x.id !== c.id)];
      newFeedUrl = ''; newFeedName = '';
    } catch (e) {
      addErr = e?.message || "Couldn't preview that feed.";
    } finally {
      addBusy = false;
    }
  }
  // Merge the server-returned candidate (keeps status/updated_at/preview in sync)
  // while preserving the transient UI-only fields (_cat/_activate/_err).
  async function repreviewCandidate(c) {
    c._err = '';
    try { Object.assign(c, await postJSON(`/api/admin/candidates/${c.id}/preview`)); }
    catch (e) { c._err = e?.message || 'Re-preview failed.'; }
  }
  async function promoteCandidate(c) {
    c._err = '';
    try {
      const res = await postJSON(`/api/admin/candidates/${c.id}/promote`, {
        default_category: (c._cat || '').trim() || null,
        active: !!c._activate,
      });
      Object.assign(c, res.candidate);
      await loadStats(); // surface the new (paused) source in the table below
    } catch (e) {
      c._err = e?.message || 'Promote failed.';
    }
  }
  async function rejectCandidate(c) {
    try { Object.assign(c, await postJSON(`/api/admin/candidates/${c.id}/reject`)); }
    catch { /* leave as-is */ }
  }

  // Feedback inbox: filter + read/unread + delete.
  let fbCat = $state('all'); // 'all' | 'unread' | a category
  let shownFeedback = $derived(
    fbCat === 'all' ? feedback
      : fbCat === 'unread' ? feedback.filter((f) => !f.read_at)
        : feedback.filter((f) => f.category === fbCat)
  );
  let fbCats = $derived([...new Set(feedback.map((f) => f.category))]);
  let unreadCount = $derived(feedback.filter((f) => !f.read_at).length);

  async function markRead(f, read) {
    const prev = f.read_at;
    f.read_at = read ? new Date().toISOString().slice(0, 19).replace('T', ' ') : null; // optimistic
    feedback = [...feedback];
    try {
      await postJSON(`/api/admin/feedback/${f.id}/read`, { read });
    } catch {
      f.read_at = prev; // revert on failure
      feedback = [...feedback];
    }
  }
  async function removeFeedback(f) {
    if (!confirm('Delete this feedback message? This cannot be undone.')) return;
    const snapshot = feedback;
    feedback = feedback.filter((x) => x.id !== f.id); // optimistic
    try {
      await delJSON(`/api/admin/feedback/${f.id}`);
    } catch {
      feedback = snapshot; // revert on failure
    }
  }

  // In-site reply: small admin-only WYSIWYG (contenteditable + execCommand).
  // The editor's HTML is sanitized SERVER-SIDE; the client just collects it.
  let replyingId = $state(null);
  let replyBusy = $state(false);
  let replyErr = $state('');
  let replyEmpty = $state(true);
  let replyEditor = $state(null);
  const SIZE_PX = { small: 13, normal: 15, large: 18, xlarge: 22 };

  function openReply(f) { replyingId = f.id; replyErr = ''; replyEmpty = true; }
  function cancelReply() { replyingId = null; replyErr = ''; }
  function focusEditor(node) { queueMicrotask(() => node.focus()); }
  function onEditorInput() { replyEmpty = !(replyEditor?.textContent || '').trim(); }
  // Paste as plain text — never let rich/pasted HTML into the editor.
  function onPaste(e) {
    e.preventDefault();
    const text = (e.clipboardData || window.clipboardData).getData('text/plain');
    document.execCommand('insertText', false, text);
  }
  // Bold/italic/lists via execCommand; styleWithCSS=false → semantic <b>/<i>/<ul>.
  function cmd(c) {
    replyEditor?.focus();
    try { document.execCommand('styleWithCSS', false, false); } catch { /* not all browsers */ }
    document.execCommand(c, false, null);
    onEditorInput();
  }
  // Font size: wrap the selection in a span with a fixed, whitelisted px.
  function setSize(key) {
    const px = SIZE_PX[key];
    if (!px) return;
    replyEditor?.focus();
    const sel = window.getSelection();
    if (!sel || !sel.rangeCount) return;
    const range = sel.getRangeAt(0);
    if (range.collapsed) return; // need a selection to size
    const span = document.createElement('span');
    span.style.fontSize = px + 'px';
    try { span.appendChild(range.extractContents()); range.insertNode(span); } catch { /* ignore */ }
    sel.removeAllRanges();
    onEditorInput();
  }
  async function sendReply(f) {
    if (replyEmpty || replyBusy) return;
    const html = replyEditor?.innerHTML || '';
    replyBusy = true; replyErr = '';
    try {
      const res = await postJSON(`/api/admin/feedback/${f.id}/reply`, { html });
      f.replies = [...(f.replies || []), res.reply];
      f.read_at = f.read_at || res.reply.sent_at; // server marked it read
      replyingId = null;
    } catch (e) {
      replyErr = e?.message || 'Could not send — your draft is kept.';
    } finally {
      replyBusy = false;
    }
  }

  const SHARE_LABEL = {
    share_ub: 'Copied UB link',
    native_share: 'Native share',
    copy_source: 'Copied source',
    source_click: 'Source clicks (from /a)',
  };
  function pct(v, m) { return m > 0 ? Math.max(4, Math.round((v / m) * 100)) : 0; }
  const max = (rows, key) => rows.reduce((m, r) => Math.max(m, r[key]), 0);
  function coverage(part, whole) { return whole > 0 ? Math.round((100 * part) / whole) : 0; }
</script>

<header class="bar">
  <div class="container inner">
    <a class="brand" href="/"><img class="logo" src="/logo.svg" alt="Upbeat Bytes" /></a>
    <a class="back" href="/account"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 12H5M11 6l-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/></svg>Account</a>
  </div>
</header>

<main class="container page">
  <h1>Dashboard</h1>
  {#if error}
    <p class="muted">{error}</p>
  {:else if !stats}
    <p class="muted">Loading…</p>
  {:else}
    <nav class="tabs" aria-label="Dashboard sections">
      {#each TABS as t (t.key)}
        <a href={'?section=' + t.key} class:active={section === t.key}>
          {t.label}{#if t.key === 'feedback' && unreadCount}<span class="badge">{unreadCount}</span>{/if}
        </a>
      {/each}
    </nav>

    {#if section === 'overview' || section === 'content' || section === 'audience'}
      <div class="rangepick">
        <span class="rl">Window</span>
        {#each [7, 30, 90] as d (d)}
          <button class="chip" class:on={range === d} onclick={() => setRange(d)}>{d}d</button>
        {/each}
      </div>
    {/if}

    {#if section === 'overview'}
      <!-- Attention Needed -->
      {#if (stats.attention ?? []).length}
        <div class="attn-strip">
          {#each stats.attention as a}
            <div class="attn {a.level}">{a.text}</div>
          {/each}
        </div>
      {:else}
        <div class="attn ok">✓ All clear — nothing needs attention right now.</div>
      {/if}

      <h2>Pulse</h2>
      <div class="cards">
        <div class="stat"><span class="n">{stats.visitors.today}</span><span class="l">Visitors today</span></div>
        <div class="stat"><span class="n">{stats.visitors.d7}</span><span class="l">Visitors (7d)</span></div>
        <div class="stat"><span class="n">{stats.visitors.d30}</span><span class="l">Visitors ({range}d)</span></div>
        <div class="stat"><span class="n">{stats.content.served}</span><span class="l">Articles live</span></div>
        <div class="stat"><span class="n">{stats.content.accepted_7d}</span><span class="l">Fresh (7d)</span></div>
        <div class="stat"><span class="n">{stats.content.latest_brief_size}</span><span class="l">In today's brief</span></div>
        <div class="stat"><span class="n">{healthy}/{sources.length}</span><span class="l">Sources healthy</span></div>
        <div class="stat"><span class="n">{stats.accounts.total}</span><span class="l">Accounts</span></div>
      </div>

    {:else if section === 'content'}
      <h2>Corpus</h2>
      <div class="cards">
        <div class="stat"><span class="n">{stats.content.served}</span><span class="l">Articles live</span></div>
        <div class="stat"><span class="n">{stats.content.rejected}</span><span class="l">Filtered out</span></div>
        <div class="stat"><span class="n">{stats.content.added_24h}</span><span class="l">Ingested (24h)</span></div>
        <div class="stat"><span class="n">{stats.content.added_7d}</span><span class="l">Ingested (7d)</span></div>
        <div class="stat"><span class="n">{stats.content.accepted_7d}</span><span class="l">Fresh & live (7d)</span></div>
        <div class="stat"><span class="n">{stats.content.latest_brief_size}</span><span class="l">In today's brief</span></div>
      </div>

      <h2>Coverage</h2>
      <div class="cards">
        <div class="stat"><span class="n">{coverage(stats.content.with_image, stats.content.served)}%</span><span class="l">Articles with image ({stats.content.with_image}/{stats.content.served})</span></div>
        <div class="stat"><span class="n">{coverage(stats.content.summaries, stats.content.served)}%</span><span class="l">Summaries ({stats.content.summaries}/{stats.content.served})</span></div>
        <div class="stat"><span class="n">{coverage(stats.content.summaries_with_image, stats.content.summaries)}%</span><span class="l">Summary pages w/ image</span></div>
        <div class="stat"><span class="n">{stats.content.brief_with_image}/{stats.content.latest_brief_size}</span><span class="l">Brief w/ image</span></div>
        <div class="stat"><span class="n">{stats.content.recent_enrich_fail}</span><span class="l">Image misses (24h)</span></div>
      </div>

      <section>
        <h2>Most-opened articles</h2>
        {#if stats.top_articles.length}
          <ul class="bars">
            {#each stats.top_articles as a (a.id)}
              <li>
                <a href={'/a/' + a.id} class="lbl" title={a.title}>{a.title}</a>
                <span class="bar" style="width:{pct(a.opens, max(stats.top_articles, 'opens'))}%"></span>
                <span class="v">{a.opens}</span>
              </li>
            {/each}
          </ul>
        {:else}<p class="muted">No opens yet.</p>{/if}
      </section>

      <div class="two">
        <section>
          <h2>Popular groupings</h2>
          {#if stats.top_groupings.length}
            <ul class="bars">
              {#each stats.top_groupings as g (g.tag)}
                <li>
                  <span class="lbl">{g.tag.replace('-', ' ')}</span>
                  <span class="bar" style="width:{pct(g.opens, max(stats.top_groupings, 'opens'))}%"></span>
                  <span class="v">{g.opens}</span>
                </li>
              {/each}
            </ul>
          {:else}<p class="muted">No data yet.</p>{/if}
        </section>
        <section>
          <h2>Popular topics</h2>
          {#if stats.top_topics.length}
            <ul class="bars">
              {#each stats.top_topics as t (t.topic)}
                <li>
                  <span class="lbl">{t.topic}</span>
                  <span class="bar" style="width:{pct(t.opens, max(stats.top_topics, 'opens'))}%"></span>
                  <span class="v">{t.opens}</span>
                </li>
              {/each}
            </ul>
          {:else}<p class="muted">No data yet.</p>{/if}
        </section>
      </div>

    {:else if section === 'sources'}
      <section class="addsrc">
        <h2>Add a source</h2>
        <div class="addrow">
          <input type="url" placeholder="Feed URL (RSS / Atom)" bind:value={newFeedUrl} onkeydown={(e) => e.key === 'Enter' && addCandidate()} />
          <input type="text" placeholder="Name (optional)" bind:value={newFeedName} />
          <button class="csend" onclick={addCandidate} disabled={addBusy || !newFeedUrl.trim()}>{addBusy ? 'Previewing…' : 'Preview & add'}</button>
        </div>
        {#if addErr}<p class="cerr">{addErr}</p>{/if}
        <p class="legend2">Fetches a sample (safely) and stages it as a candidate — nothing is added live until you Promote.</p>
      </section>

      {#if pendingCandidates.length}
        <section>
          <h2>Candidate queue <span class="count">({pendingCandidates.length})</span></h2>
          <ul class="candlist">
            {#each pendingCandidates as c (c.id)}
              <li>
                <div class="chead">
                  <span class="cname">{c.name || c.feed_url}</span>
                  <span class="cstatus">{c.status}</span>
                </div>
                <div class="curl">{c.feed_url}</div>
                {#if c.preview}
                  <div class="cprev">
                    {c.preview.accepted ?? 0}/{c.preview.sampled ?? 0} sampled would pass{#if c.preview.acceptance_rate != null} · {Math.round(c.preview.acceptance_rate * 100)}% accept{/if}{#if c.preview.recent_7d != null} · {c.preview.recent_7d} in last 7d{/if}
                    {#if c.preview.examples_accepted?.length}<div class="cex">e.g. {c.preview.examples_accepted.slice(0, 3).join(' · ')}</div>{/if}
                  </div>
                {/if}
                {#if c._err}<p class="cerr">{c._err}</p>{/if}
                <div class="cactions">
                  <input class="ccat" type="text" placeholder="category (optional)" bind:value={c._cat} />
                  <label class="cchk"><input type="checkbox" bind:checked={c._activate} /> Activate immediately</label>
                  <button class="csend" onclick={() => promoteCandidate(c)}>Promote{c._activate ? '' : ' as paused'}</button>
                  <button class="act" onclick={() => repreviewCandidate(c)}>Re-preview</button>
                  <button class="act del" onclick={() => rejectCandidate(c)}>Reject</button>
                </div>
              </li>
            {/each}
          </ul>
        </section>
      {/if}

      <h2>Sources</h2>
      <p class="sub2">{healthy} healthy · {resting} resting · {flagged} flagged · {paused} paused · {retired} retired · {sources.length} total</p>
      <div class="filterchips">
        {#each [['all', 'All'], ['healthy', 'Healthy'], ['resting', 'Resting'], ['flagged', 'Flagged'], ['paused', 'Paused'], ['retired', 'Retired']] as [key, label] (key)}
          <button class="chip" class:on={srcFilter === key} onclick={() => (srcFilter = key)}>{label}</button>
        {/each}
      </div>
      <div class="tablewrap">
        <table class="srctable">
          <thead>
            <tr>
              <th>Source</th><th class="num">Served</th><th class="num">Accept</th><th class="num">Dup</th>
              <th>Last success</th><th>Next poll</th><th class="num">Fails</th><th>Status</th><th>Actions</th>
            </tr>
          </thead>
          <tbody>
            {#each shownSources as s (s.id)}
              {@const st = sstatus(s)}
              <tr class:warn={st === 'active' && s.failures > 0} class:flag={s.review_flag} class:paused={st !== 'active'}>
                <td class="sname">
                  {s.name}{#if s.category}<span class="cat">{s.category}</span>{/if}
                  {#if s.review_flag && s.review_reason}<span class="rr">“{s.review_reason}”</span>{/if}
                </td>
                <td class="num">{s.served}</td>
                <td class="num">{s.acceptance_rate != null ? s.acceptance_rate + '%' : '—'}</td>
                <td class="num" title={s.accepted_dup_rate != null ? `${s.accepted_dup_rate}% of accepted were duplicates` : ''}>{s.duplicate_rate != null ? s.duplicate_rate + '%' : '—'}</td>
                <td class="dim">{s.last_success_at ? fwhen(s.last_success_at) : '—'}</td>
                <td class="dim">{st === 'active' ? fwhen(s.next_due_at) : '—'}</td>
                <td class="num">{s.failures || ''}</td>
                <td class="status">
                  {#if st === 'retired'}<span class="paustxt">retired</span>
                  {:else if st === 'paused'}<span class="paustxt">paused</span>
                  {:else if rateLimited(s)}<span class="bad" title={s.last_error || ''}>rate-limited · rests until {fwhen(s.retry_after_at)}{#if s.review_flag} · review{/if}</span>
                  {:else if s.failures > 0}<span class="bad" title={s.last_error || ''}>⚠ resting{#if s.review_flag} · review{/if}</span>
                  {:else if s.review_flag}<span class="flagtxt">⚑ review</span>
                  {:else}<span class="good">✓ ok</span>{/if}
                  {#if !s.content_visible}<span class="hidtxt" title="Articles hidden from the public feed">· hidden</span>{/if}
                </td>
                <td class="rowactions">
                  {#if st === 'retired'}
                    <button class="act" onclick={() => setStatus(s, 'active')}>Restore</button>
                  {:else}
                    <button class="act" onclick={() => setStatus(s, st === 'active' ? 'paused' : 'active')}>{st === 'active' ? 'Pause' : 'Resume'}</button>
                    <button class="act" onclick={() => setStatus(s, 'retired')}>Retire</button>
                  {/if}
                  <button class="act" onclick={() => (s.review_flag ? clearReview(s) : openFlag(s))}>{s.review_flag ? 'Clear' : 'Flag'}</button>
                  <button class="act" onclick={() => toggleVisible(s)}>{s.content_visible ? 'Hide' : 'Show'}</button>
                </td>
              </tr>
            {/each}
          </tbody>
        </table>
      </div>
      <p class="legend2">“served” = accepted, non-duplicate articles live · accept/dup % is of all ingested · pausing stops polling but keeps existing articles live · times in your local zone</p>

    {:else if section === 'audience'}
      <section>
        <h2>Visitors</h2>
        <div class="cards">
          <div class="stat"><span class="n">{stats.visitors.today}</span><span class="l">Today</span></div>
          <div class="stat"><span class="n">{stats.visitors.d7}</span><span class="l">Last 7 days</span></div>
          <div class="stat"><span class="n">{stats.visitors.d30}</span><span class="l">Last {range} days</span></div>
        </div>
      </section>
      <section>
        <h2>Returning visitors ({range}d)</h2>
        <div class="cards">
          <div class="stat"><span class="n">{stats.retention.new}</span><span class="l">New (1 day)</span></div>
          <div class="stat"><span class="n">{stats.retention['2-3']}</span><span class="l">2–3 days</span></div>
          <div class="stat"><span class="n">{stats.retention['4-7']}</span><span class="l">4–7 days</span></div>
          <div class="stat"><span class="n">{stats.retention['8+']}</span><span class="l">8+ days</span></div>
        </div>
      </section>
      <section>
        <h2>Accounts</h2>
        <div class="cards">
          <div class="stat"><span class="n">{stats.accounts.total}</span><span class="l">Total</span></div>
          <div class="stat"><span class="n">{stats.accounts.new_today}</span><span class="l">New today</span></div>
          <div class="stat"><span class="n">{stats.accounts.new_7d}</span><span class="l">New this week</span></div>
          <div class="stat"><span class="n">{stats.accounts.active_7d}</span><span class="l">Active this week</span></div>
        </div>
      </section>
      <section>
        <h2>Reading funnel</h2>
        <div class="cards">
          <div class="stat"><span class="n">{stats.funnel.summary_viewed}</span><span class="l">Summaries read</span></div>
          <div class="stat"><span class="n">{stats.funnel.source_click}</span><span class="l">→ Source (from summary)</span></div>
          <div class="stat"><span class="n">{stats.funnel.source_rate}%</span><span class="l">Summary → source rate</span></div>
          <div class="stat"><span class="n">{stats.funnel.full_story}</span><span class="l">Straight to source</span></div>
        </div>
      </section>
      <section>
        <h2>Emotional mix &amp; friction</h2>
        <div class="cards">
          <div class="stat"><span class="n">{stats.emotional_mix.not_today}</span><span class="l">Not today</span></div>
          <div class="stat"><span class="n">{stats.emotional_mix.less_like_this}</span><span class="l">Less like this</span></div>
          <div class="stat"><span class="n">{stats.emotional_mix.hide_topic}</span><span class="l">Hide topic</span></div>
          <div class="stat"><span class="n">{stats.replace.used}</span><span class="l">Replaces</span></div>
          <div class="stat"><span class="n">{stats.paywall.paywalled_source_open}</span><span class="l">Paywalled opens</span></div>
        </div>
      </section>
      <section>
        <h2>Sharing</h2>
        <div class="cards">
          {#each Object.entries(stats.shares) as [kind, n]}
            <div class="stat"><span class="n">{n}</span><span class="l">{SHARE_LABEL[kind] ?? kind}</span></div>
          {/each}
        </div>
      </section>
      <section>
        <h2>Daily trend</h2>
        {#if stats.daily.length}
          {@const dmax = Math.max(1, ...stats.daily.map((d) => Math.max(d.opens, d.visits)))}
          <div class="trend">
            {#each stats.daily as d (d.day)}
              <div class="col" title={`${d.day}: ${d.visits} visits, ${d.opens} opens`}>
                <span class="visits" style="height:{pct(d.visits, dmax)}%"></span>
                <span class="opens" style="height:{pct(d.opens, dmax)}%"></span>
              </div>
            {/each}
          </div>
          <p class="legend"><span class="sw visits"></span> visits &nbsp; <span class="sw opens"></span> opens</p>
        {:else}<p class="muted">No data yet.</p>{/if}
      </section>

    {:else if section === 'feedback'}
      <h2>Feedback {#if feedback.length}<span class="count">({unreadCount} unread / {feedback.length})</span>{/if}</h2>
      {#if feedback.length}
        <div class="filterchips">
          <button class="chip" class:on={fbCat === 'all'} onclick={() => (fbCat = 'all')}>All</button>
          <button class="chip" class:on={fbCat === 'unread'} onclick={() => (fbCat = 'unread')}>Unread{#if unreadCount} ({unreadCount}){/if}</button>
          {#each fbCats as cat (cat)}
            <button class="chip" class:on={fbCat === cat} onclick={() => (fbCat = cat)}>{cat}</button>
          {/each}
        </div>
        {#if shownFeedback.length}
          <ul class="fb">
            {#each shownFeedback as f (f.id)}
              <li class:unread={!f.read_at}>
                <div class="fhead">
                  {#if !f.read_at}<span class="dot" title="Unread"></span>{/if}
                  <span class="cat">{f.category}</span>
                  <span class="when">{fdate(f.created_at)}</span>
                  {#if f.contact_email}<span class="addr">{f.contact_email}</span>{:else}<span class="anon">anonymous</span>{/if}
                </div>
                <p class="msg">{f.message}</p>

                {#if f.replies?.length}
                  <div class="thread">
                    {#each f.replies as r (r.id)}
                      <div class="rep">
                        <span class="rl">↪ Replied · {fwhen(r.sent_at)}</span>
                        {#if r.message_html}
                          <!-- server-sanitized HTML (tiny Markdown subset) -->
                          <div class="repmsg">{@html r.message_html}</div>
                        {:else}
                          <p class="repmsg">{r.message}</p>
                        {/if}
                      </div>
                    {/each}
                  </div>
                {/if}

                {#if replyingId === f.id}
                  <div class="composer">
                    <div class="rtbar">
                      <button type="button" class="rtbtn" title="Bold" onmousedown={(e) => e.preventDefault()} onclick={() => cmd('bold')}><b>B</b></button>
                      <button type="button" class="rtbtn" title="Italic" onmousedown={(e) => e.preventDefault()} onclick={() => cmd('italic')}><i>I</i></button>
                      <select class="szsel" title="Text size" onmousedown={(e) => e.stopPropagation()} onchange={(e) => { setSize(e.currentTarget.value); e.currentTarget.value = ''; }}>
                        <option value="">Size</option>
                        <option value="small">Small</option>
                        <option value="normal">Normal</option>
                        <option value="large">Large</option>
                        <option value="xlarge">X-Large</option>
                      </select>
                      <button type="button" class="rtbtn" title="Bullet list" onmousedown={(e) => e.preventDefault()} onclick={() => cmd('insertUnorderedList')}>• List</button>
                      <button type="button" class="rtbtn" title="Numbered list" onmousedown={(e) => e.preventDefault()} onclick={() => cmd('insertOrderedList')}>1. List</button>
                    </div>
                    <div
                      class="rtedit"
                      contenteditable="true"
                      bind:this={replyEditor}
                      use:focusEditor
                      oninput={onEditorInput}
                      onpaste={onPaste}
                      role="textbox"
                      aria-multiline="true"
                      aria-label="Write a reply"
                    ></div>
                    {#if replyErr}<p class="cerr">{replyErr}</p>{/if}
                    <div class="cbtns">
                      <button class="csend" onclick={() => sendReply(f)} disabled={replyBusy || replyEmpty}>
                        {replyBusy ? 'Sending…' : 'Send reply'}
                      </button>
                      <button class="act" onclick={cancelReply}>Cancel</button>
                    </div>
                  </div>
                {/if}

                <div class="factions">
                  {#if f.contact_email && replyingId !== f.id}
                    <button class="act" onclick={() => openReply(f)}>Reply</button>
                  {:else if !f.contact_email}
                    <span class="noreply">No reply address</span>
                  {/if}
                  <button class="act" onclick={() => markRead(f, !f.read_at)}>{f.read_at ? 'Mark unread' : 'Mark read'}</button>
                  <button class="act del" onclick={() => removeFeedback(f)}>Delete</button>
                </div>
              </li>
            {/each}
          </ul>
        {:else}<p class="muted">Nothing in this filter.</p>{/if}
      {:else}<p class="muted">No feedback yet.</p>{/if}
    {/if}
  {/if}
</main>

{#if flagging}
  <!-- Flag-reason popover: backdrop closes; Escape/Cancel too (keyboard path). -->
  <!-- svelte-ignore a11y_click_events_have_key_events -->
  <div class="modal-overlay" role="presentation" onclick={(e) => e.target === e.currentTarget && cancelFlag()}>
    <div class="modal" role="dialog" aria-modal="true" aria-label="Flag source for review" tabindex="-1">
      <h3>Flag “{flagging.name}” for review</h3>
      <p class="msub">An optional note on why — it shows under the source in the table.</p>
      <input
        type="text"
        bind:value={flagReason}
        placeholder="e.g. acceptance dropping, off-topic lately"
        onkeydown={(e) => (e.key === 'Enter' ? confirmFlag() : e.key === 'Escape' ? cancelFlag() : null)}
      />
      <div class="mbtns">
        <button class="msend" onclick={confirmFlag}>Flag for review</button>
        <button class="act" onclick={cancelFlag}>Cancel</button>
      </div>
    </div>
  </div>
{/if}

<style>
  header.bar { background: var(--surface); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 20; }
  .inner { display: flex; align-items: center; justify-content: space-between; height: 64px; }
  .logo { height: 40px; display: block; }
  .back { color: var(--accent-deep); font-size: 0.9rem; display: inline-flex; align-items: center; gap: 5px; }
  .back svg { width: 17px; height: 17px; display: block; }
  .page { padding: 22px 20px 70px; }
  h1 { font-size: clamp(2rem, 5vw, 2.6rem); margin: 6px 0 14px; }
  h2 { font-size: 1.1rem; margin: 28px 0 12px; }
  .muted { color: var(--muted); }

  /* Sticky section tabs */
  .tabs {
    display: flex; gap: 6px; flex-wrap: wrap; margin: 0 0 22px;
    position: sticky; top: 64px; z-index: 15; background: var(--bg);
    padding: 10px 0; border-bottom: 1px solid var(--line);
  }
  .tabs a {
    border: 1px solid var(--line); background: var(--surface); color: var(--ink);
    border-radius: 999px; padding: 7px 15px; font-size: 0.9rem; display: inline-flex; align-items: center; gap: 6px;
  }
  .tabs a:hover { border-color: var(--accent); color: var(--accent-deep); }
  .tabs a.active { background: var(--accent); border-color: var(--accent); color: #fff; }
  .tabs .badge { background: var(--accent-soft); color: var(--accent-deep); border-radius: 999px; padding: 0 7px; font-size: 0.74rem; }
  .tabs a.active .badge { background: rgba(255,255,255,0.25); color: #fff; }

  /* Attention Needed — soft amber/blue, never alarming red unless truly broken */
  .attn-strip { display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px; }
  .attn { border-radius: 12px; padding: 11px 15px; font-size: 0.9rem; border-left: 3px solid; }
  .attn.warn { background: #fdf3e3; color: #875a16; border-color: #e0a648; }
  .attn.info { background: #eef4f8; color: var(--accent-deep); border-color: var(--accent); }
  .attn.ok { background: #eef5ee; color: #3f7048; border-left: 3px solid #6aa86a; border-radius: 12px; padding: 11px 15px; font-size: 0.9rem; }

  .cards { display: flex; flex-wrap: wrap; gap: 12px; }
  .stat {
    flex: 1 1 130px; background: var(--surface); border: 1px solid var(--line);
    border-radius: 14px; padding: 14px 16px; display: flex; flex-direction: column; gap: 3px;
  }
  .stat .n { font-size: 1.7rem; font-weight: 700; font-family: var(--label); color: var(--ink); }
  .stat .l { color: var(--muted); font-size: 0.8rem; }

  .two { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }
  @media (max-width: 620px) { .two { grid-template-columns: 1fr; } }

  ul.bars { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
  ul.bars li { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 8px; position: relative; }
  ul.bars .lbl {
    grid-column: 1; z-index: 1; font-size: 0.86rem; color: var(--ink);
    text-transform: capitalize; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 4px 0;
  }
  a.lbl { text-decoration: none; }
  a.lbl:hover { color: var(--accent-deep); }
  ul.bars .bar {
    grid-column: 1 / 2; grid-row: 1; height: 100%; min-height: 26px; align-self: stretch;
    background: var(--accent-soft); border-radius: 8px; z-index: 0;
  }
  ul.bars .v { grid-column: 2; color: var(--muted); font-size: 0.82rem; font-variant-numeric: tabular-nums; }

  .trend { display: flex; align-items: flex-end; gap: 3px; height: 120px; }
  .trend .col { flex: 1; display: flex; align-items: flex-end; gap: 1px; height: 100%; }
  .trend .visits { flex: 1; background: var(--accent-soft); border-radius: 2px 2px 0 0; }
  .trend .opens { flex: 1; background: var(--accent); border-radius: 2px 2px 0 0; }
  .legend { color: var(--muted); font-size: 0.78rem; margin-top: 8px; }
  .legend .sw { display: inline-block; width: 10px; height: 10px; border-radius: 2px; vertical-align: middle; }
  .legend .sw.visits { background: var(--accent-soft); }
  .legend .sw.opens { background: var(--accent); }

  .sub2 { color: var(--muted); font-size: 0.84rem; margin: 0 0 12px; }
  .legend2 { color: var(--muted); font-size: 0.76rem; margin: 10px 0 0; font-style: italic; }

  /* Analytics window picker */
  .rangepick { display: flex; align-items: center; gap: 7px; margin: 0 0 18px; }
  .rangepick .rl { font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin-right: 2px; }
  .rangepick .chip {
    border: 1px solid var(--line); background: var(--surface); color: var(--ink);
    border-radius: 999px; padding: 4px 12px; font-size: 0.8rem; cursor: pointer;
  }
  .rangepick .chip:hover { border-color: var(--accent); }
  .rangepick .chip.on { background: var(--accent); border-color: var(--accent); color: #fff; }

  /* Filter chips (sources + feedback) */
  .filterchips { display: flex; gap: 7px; flex-wrap: wrap; margin: 0 0 14px; }
  .filterchips .chip {
    border: 1px solid var(--line); background: var(--surface); color: var(--ink);
    border-radius: 999px; padding: 5px 13px; font-size: 0.82rem; cursor: pointer; text-transform: capitalize;
  }
  .filterchips .chip:hover { border-color: var(--accent); }
  .filterchips .chip.on { background: var(--accent); border-color: var(--accent); color: #fff; }

  /* Add a source + candidate queue */
  .addsrc { background: var(--surface); border: 1px solid var(--line); border-radius: 14px; padding: 14px 16px; margin-bottom: 6px; }
  .addrow { display: flex; gap: 8px; flex-wrap: wrap; }
  .addrow input { flex: 1 1 200px; box-sizing: border-box; font: inherit; font-size: 0.9rem; padding: 8px 11px; border: 1px solid var(--line); border-radius: 9px; background: var(--bg); color: var(--ink); }
  .addrow input:focus { outline: none; border-color: var(--accent); }
  ul.candlist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
  ul.candlist li { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; }
  .chead { display: flex; align-items: baseline; gap: 10px; }
  .chead .cname { font-weight: 600; color: var(--ink); }
  .chead .cstatus { font-size: 0.72rem; color: var(--muted); text-transform: capitalize; }
  .curl { font-size: 0.76rem; color: var(--muted); word-break: break-all; margin-top: 2px; }
  .cprev { font-size: 0.84rem; color: var(--ink); margin-top: 7px; }
  .cprev .cex { color: var(--muted); font-size: 0.8rem; margin-top: 2px; font-style: italic; }
  .cactions { display: flex; gap: 9px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
  .cactions .ccat { font: inherit; font-size: 0.8rem; padding: 5px 9px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--ink); width: 150px; }
  .cactions .cchk { font-size: 0.8rem; color: var(--muted); display: inline-flex; align-items: center; gap: 5px; }

  /* Source health table */
  .tablewrap { overflow-x: auto; }
  .srctable { width: 100%; border-collapse: collapse; font-size: 0.86rem; min-width: 560px; }
  .srctable th {
    text-align: left; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em;
    color: var(--muted); font-weight: 600; padding: 6px 10px; border-bottom: 1px solid var(--line);
  }
  .srctable td { padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: baseline; }
  .srctable .num { text-align: right; font-variant-numeric: tabular-nums; }
  .srctable .dim { color: var(--muted); white-space: nowrap; font-size: 0.82rem; }
  .srctable .sname { font-weight: 600; color: var(--ink); }
  .srctable .sname .cat { display: block; font-weight: 400; font-size: 0.72rem; color: var(--muted); text-transform: capitalize; }
  .srctable .sname .rr { display: block; font-weight: 400; font-size: 0.72rem; color: var(--accent-deep); font-style: italic; }
  .srctable tr.warn { background: #fdf3e3; }
  .srctable tr.flag { background: #eef4f8; }
  .srctable tr.paused { opacity: 0.6; }
  .srctable .status .bad { color: #875a16; }
  .srctable .status .flagtxt { color: var(--accent-deep); }
  .srctable .status .good { color: #3f7048; }
  .srctable .status .paustxt { color: var(--muted); font-style: italic; }
  .srctable .status .hidtxt { color: #9a3b3b; font-size: 0.78rem; }
  .srctable .rowactions { white-space: nowrap; }
  .srctable .rowactions .act {
    background: none; border: 1px solid var(--line); color: var(--accent-deep);
    border-radius: 999px; padding: 3px 11px; font-size: 0.76rem; cursor: pointer; margin-right: 5px;
  }
  .srctable .rowactions .act:hover { border-color: var(--accent); }

  .count { color: var(--muted); font-weight: 400; font-size: 0.9rem; }
  ul.fb { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
  ul.fb li { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; }
  ul.fb li.unread { border-color: var(--accent); box-shadow: inset 3px 0 0 var(--accent); }
  .fhead { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; font-size: 0.78rem; }
  .fhead .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
  .fhead .cat { background: var(--accent-soft); color: var(--accent-deep); border-radius: 999px; padding: 2px 9px; text-transform: capitalize; }
  .fhead .when { color: var(--muted); }
  .fhead .addr { color: var(--accent-deep); margin-left: auto; }
  .fhead .anon { color: var(--muted); margin-left: auto; font-style: italic; }
  .msg { margin: 0; color: var(--ink); font-size: 0.92rem; white-space: pre-wrap; }

  /* Reply thread + composer */
  .thread { margin: 10px 0 0; display: flex; flex-direction: column; gap: 8px; }
  .rep { border-left: 2px solid var(--accent-soft); padding: 2px 0 2px 12px; }
  .rep .rl { font-size: 0.72rem; color: var(--muted); }
  .rep .repmsg { margin: 2px 0 0; font-size: 0.9rem; color: var(--ink); }
  .rep .repmsg :global(p) { margin: 0 0 6px; white-space: pre-wrap; }
  .rep .repmsg :global(ul) { margin: 4px 0; padding-left: 20px; }
  .rep .repmsg :global(h3),
  .rep .repmsg :global(h4),
  .rep .repmsg :global(h5) { margin: 6px 0 4px; font-size: 0.95rem; }
  .composer { margin: 10px 0 0; }
  .rtbar { display: flex; gap: 6px; margin-bottom: 6px; flex-wrap: wrap; align-items: center; }
  .rtbtn {
    font: inherit; font-size: 0.82rem; background: var(--surface); border: 1px solid var(--line);
    color: var(--ink); border-radius: 7px; padding: 3px 10px; cursor: pointer; line-height: 1.4; min-width: 30px;
  }
  .rtbtn:hover { border-color: var(--accent); color: var(--accent-deep); }
  .szsel {
    font: inherit; font-size: 0.8rem; background: var(--surface); border: 1px solid var(--line);
    color: var(--ink); border-radius: 7px; padding: 3px 6px; cursor: pointer;
  }
  .rtedit {
    min-height: 90px; box-sizing: border-box; font-size: 0.92rem; line-height: 1.5;
    padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px;
    background: var(--bg); color: var(--ink);
  }
  .rtedit:focus { outline: none; border-color: var(--accent); }
  .rtedit :global(ul), .rtedit :global(ol) { margin: 4px 0; padding-left: 22px; }
  .rtedit :global(p) { margin: 0 0 6px; }
  .cerr { margin: 6px 0 0; color: #9a3b3b; font-size: 0.82rem; }
  .cbtns { display: flex; align-items: center; gap: 12px; margin-top: 8px; }
  .csend {
    font: inherit; font-size: 0.82rem; font-weight: 600; background: var(--accent); color: #fff;
    border: none; border-radius: 999px; padding: 7px 18px; cursor: pointer;
  }
  .csend:hover { background: var(--accent-deep); }
  .csend:disabled { opacity: 0.55; cursor: default; }
  .noreply { color: var(--muted); font-size: 0.76rem; font-style: italic; }

  /* Flag-reason popover */
  .modal-overlay {
    position: fixed; inset: 0; background: rgba(10, 22, 38, 0.32);
    display: flex; align-items: center; justify-content: center; padding: 20px; z-index: 60;
  }
  .modal {
    background: var(--surface); border: 1px solid var(--line); border-radius: 16px;
    box-shadow: 0 12px 34px rgba(40, 38, 28, 0.16); width: 100%; max-width: 420px; padding: 22px 24px;
  }
  .modal h3 { margin: 0 0 6px; font-size: 1.2rem; }
  .modal .msub { margin: 0 0 14px; color: var(--muted); font-size: 0.85rem; }
  .modal input {
    width: 100%; box-sizing: border-box; font: inherit; font-size: 0.92rem;
    padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--bg); color: var(--ink);
  }
  .modal input:focus { outline: none; border-color: var(--accent); }
  .mbtns { display: flex; align-items: center; gap: 12px; margin-top: 14px; }
  .msend {
    font: inherit; font-size: 0.85rem; font-weight: 600; background: var(--accent); color: #fff;
    border: none; border-radius: 999px; padding: 8px 18px; cursor: pointer;
  }
  .msend:hover { background: var(--accent-deep); }
  .factions { display: flex; gap: 14px; margin-top: 9px; }
  .factions .act {
    background: none; border: none; padding: 0; cursor: pointer;
    color: var(--muted); font-size: 0.76rem; border-bottom: 1px dotted var(--line);
  }
  .factions .act:hover { color: var(--accent-deep); border-bottom-color: var(--accent); }
  .factions .act.del:hover { color: #9a3b3b; border-bottom-color: #9a3b3b; }
</style>
