← back to Pitch Guard

public/index.html

935 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pitch Guard — Designer Wallcoverings</title>
<style>
  :root{
    --cream:#f6f3ec; --paper:#fffdf8; --ink:#26221c; --muted:#8a8273;
    --line:#e3ddcf; --gold:#9a7b3f; --accent:#2c4a3e;
    --red:#a3322a; --redbg:#f7e3e0; --amber:#9a6b1e; --amberbg:#f6ecd6;
    --green:#2c6b4f; --greenbg:#e2efe6; --slate:#5a6b78; --slatebg:#e7ecef;
  }
  *{box-sizing:border-box;margin:0;padding:0}
  body{font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:var(--cream);color:var(--ink);height:100vh;overflow:hidden}
  h1,h2,h3,.serif{font-family:Georgia,"Times New Roman",serif}
  header{height:54px;background:var(--ink);color:var(--cream);display:flex;align-items:center;
    padding:0 18px;gap:14px;border-bottom:3px solid var(--gold);flex-shrink:0}
  header .brand{display:flex;align-items:center;gap:9px}
  header .brand svg{display:block}
  header .brand .wm{display:flex;flex-direction:column;line-height:1}
  header .brand .wm b{font-family:Georgia,serif;font-size:13px;letter-spacing:.12em;color:var(--cream)}
  header .brand .wm i{font-style:normal;font-size:8.5px;letter-spacing:.26em;color:var(--gold);margin-top:2px}
  header .divider{width:1px;height:26px;background:#4a4337}
  header h1{font-size:18px;font-weight:600;letter-spacing:.3px}
  header .sub{font-size:12px;color:#cdc6b3;letter-spacing:.4px}
  header .spacer{flex:1}
  header .acct{font-size:12px;background:#3a352b;padding:5px 11px;border-radius:20px;color:#e8e1cd}
  header .reset{font-size:11px;background:none;border:1px solid #4a4438;color:#cdc6b3;
    padding:4px 10px;border-radius:5px;cursor:pointer}
  header .reset:hover{border-color:var(--gold);color:#fff}
  .layout{display:flex;height:calc(100vh - 54px);overflow:hidden}
  /* ── panels ── */
  .panel{display:flex;flex-direction:column;height:100%;overflow:hidden;background:var(--paper);
    flex:0 0 auto;min-width:46px}
  .panel.flexed{flex:1 1 0;min-width:300px}
  .panel.collapsed{flex:0 0 42px !important;min-width:42px;background:var(--cream)}
  .panel.collapsed .pane-body,.panel.collapsed .pane-hd>*:not(.collapse-strip){display:none}
  .panel.dragging{opacity:.4}
  .panel.drop-target{outline:2px dashed var(--gold);outline-offset:-2px}
  .pane-hd{padding:9px 12px;border-bottom:1px solid var(--line);display:flex;
    align-items:center;gap:7px;background:var(--paper);flex-shrink:0;min-height:44px;cursor:grab}
  .pane-hd:active{cursor:grabbing}
  .pane-hd h2{font-size:12px;text-transform:uppercase;letter-spacing:.9px;color:var(--muted);
    font-family:inherit;font-weight:700;white-space:nowrap}
  .pane-hd .grip{color:var(--line);font-size:13px;cursor:grab;flex-shrink:0;letter-spacing:-2px}
  .pane-hd .count{font-size:11px;background:var(--ink);color:var(--cream);border-radius:10px;padding:1px 8px}
  .pane-hd .spacer{flex:1}
  .hd-btn{font-size:12px;line-height:1;background:none;border:1px solid var(--line);color:var(--muted);
    width:24px;height:24px;border-radius:5px;cursor:pointer;flex-shrink:0;padding:0}
  .hd-btn:hover{border-color:var(--gold);color:var(--gold);background:var(--cream)}
  .collapse-strip{display:none;flex-direction:column;align-items:center;gap:10px;padding-top:4px;cursor:pointer}
  .panel.collapsed .pane-hd{flex-direction:column;cursor:pointer;padding:9px 4px;min-height:0}
  .panel.collapsed .collapse-strip{display:flex}
  .collapse-strip .vlabel{writing-mode:vertical-rl;font-size:11px;text-transform:uppercase;
    letter-spacing:1.5px;color:var(--muted);font-weight:700}
  .pane-body{flex:1;overflow-y:auto}
  /* ── resize handle ── */
  .rz-handle{flex:0 0 7px;cursor:col-resize;background:var(--line);position:relative}
  .rz-handle:hover,.rz-handle.active{background:var(--gold)}
  .rz-handle::after{content:"";position:absolute;left:2px;top:50%;width:3px;height:34px;
    transform:translateY(-50%);background:var(--paper);border-radius:2px;opacity:.6}
  /* ── buttons ── */
  button{font:inherit;cursor:pointer;border:1px solid var(--line);background:var(--paper);
    color:var(--ink);border-radius:6px;padding:6px 12px;transition:.12s}
  button:hover{background:var(--cream)}
  button:disabled{opacity:.45;cursor:not-allowed}
  .btn-sm{font-size:11px;padding:4px 9px}
  .btn-go{background:var(--accent);color:#fff;border-color:var(--accent);font-weight:600}
  .btn-go:hover{background:#233b31}
  .btn-danger{background:var(--red);color:#fff;border-color:var(--red)}
  /* ── draft list ── */
  .draft{padding:10px 14px;border-bottom:1px solid var(--line);cursor:pointer;position:relative}
  .draft:hover{background:var(--cream)}
  .draft.active{background:var(--cream);box-shadow:inset 3px 0 0 var(--gold)}
  .draft .who{font-weight:600;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:54px}
  .draft .subj{font-size:12px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px}
  .draft .meta{font-size:11px;color:var(--muted);margin-top:3px}
  .draft .badge{position:absolute;top:9px;right:12px}
  .vgroup-hd{padding:10px 14px;background:var(--cream);border-top:1px solid var(--line);
    border-bottom:1px solid var(--line);cursor:pointer;display:flex;align-items:center;gap:7px;
    font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.7px}
  .vgroup-hd:hover{background:#efeadd}
  .vgroup-hd .arrow{font-size:9px;transition:transform .15s}
  .vgroup-hd.closed .arrow{transform:rotate(-90deg)}
  .vgroup-hd .vct{margin-left:auto;background:var(--slatebg);color:var(--slate);
    border-radius:9px;padding:1px 8px;font-size:11px}
  .vgroup-note{padding:6px 14px 8px;font-size:11px;color:var(--muted);background:var(--cream)}
  .cgroup-hd{padding:10px 14px;background:#eef1ef;border-bottom:1px solid var(--line);
    cursor:pointer;display:flex;align-items:center;gap:7px;font-size:11px;font-weight:700;
    color:var(--accent);text-transform:uppercase;letter-spacing:.7px}
  .cgroup-hd:hover{background:#e6ebe8}
  .cgroup-hd .arrow{font-size:9px;transition:transform .15s}
  .cgroup-hd.closed .arrow{transform:rotate(-90deg)}
  .cgroup-hd .vct{background:var(--accent);color:#fff;border-radius:9px;padding:1px 8px;font-size:11px}
  .cgroup-hd .scan-btn{margin-left:auto;font-size:11px;padding:3px 10px}
  .cand{padding:10px 14px;border-bottom:1px solid var(--line);cursor:pointer;position:relative}
  .cand:hover{background:var(--cream)}
  .cand .who{font-weight:600;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:76px}
  .cand .subj{font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px}
  .cand .meta{font-size:11px;color:var(--muted);margin-top:3px}
  .cand-draft{position:absolute;top:10px;right:12px;font-size:10px;font-weight:700;padding:4px 9px;
    border-radius:5px;border:1px solid var(--accent);background:var(--accent);color:#fff;cursor:pointer}
  .cand-draft:hover{background:#233b31}
  .subbar{display:flex;align-items:center;gap:8px;padding:7px 12px;border-bottom:1px solid var(--line);
    background:var(--paper);flex-shrink:0}
  .subbar label{font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);font-weight:700}
  .subbar select{font:inherit;font-size:12px;border:1px solid var(--line);border-radius:5px;
    padding:4px 7px;background:var(--cream);color:var(--ink);flex:1}
  .panel.collapsed .subbar{display:none}
  .draft-del{position:absolute;bottom:8px;right:10px;width:22px;height:22px;border-radius:5px;
    border:1px solid var(--line);background:var(--paper);color:var(--muted);font-size:11px;
    line-height:1;cursor:pointer;padding:0;display:none}
  .draft:hover .draft-del{display:block}
  .draft-del:hover{border-color:var(--red);color:var(--red);background:var(--redbg)}
  .badge{font-size:10px;font-weight:700;letter-spacing:.5px;border-radius:4px;padding:2px 6px;text-transform:uppercase}
  .r-high{background:var(--redbg);color:var(--red)}
  .r-elevated{background:var(--amberbg);color:var(--amber)}
  .r-caution{background:var(--slatebg);color:var(--slate)}
  .r-clear{background:var(--greenbg);color:var(--green)}
  .r-pending{background:#ece8df;color:var(--muted)}
  .spin{display:inline-block;width:11px;height:11px;border:2px solid var(--line);
    border-top-color:var(--gold);border-radius:50%;animation:sp .7s linear infinite;vertical-align:-1px}
  @keyframes sp{to{transform:rotate(360deg)}}
  /* ── correspondence ── */
  .empty{padding:48px 28px;text-align:center;color:var(--muted);font-size:13px}
  .empty .big{font-size:34px;margin-bottom:10px;opacity:.5}
  .risk-banner{margin:14px;border-radius:8px;padding:13px 15px;border:1px solid}
  .risk-banner.high{background:var(--redbg);border-color:#d8b4ae}
  .risk-banner.elevated{background:var(--amberbg);border-color:#dcc99a}
  .risk-banner.caution{background:var(--slatebg);border-color:#c7d1d8}
  .risk-banner.clear{background:var(--greenbg);border-color:#b6d3c0}
  .risk-banner h3{font-size:14px;display:flex;align-items:center;gap:8px;font-family:inherit}
  .risk-banner p{font-size:12.5px;margin-top:5px;color:var(--ink)}
  .risk-banner .sigs{margin-top:8px;display:flex;gap:6px;flex-wrap:wrap}
  .contact-hd{padding:4px 16px 8px;font-size:12px;color:var(--muted)}
  .contact-hd b{color:var(--ink);font-size:13px}
  .thread{margin:0 14px 12px;border:1px solid var(--line);border-radius:8px;background:var(--paper);overflow:hidden}
  .thread-hd{padding:8px 12px;background:var(--cream);cursor:pointer;display:flex;align-items:center;gap:8px}
  .thread-hd .t-subj{font-weight:600;font-size:12.5px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .thread-hd .t-meta{font-size:11px;color:var(--muted);white-space:nowrap}
  .msg{padding:9px 12px;border-top:1px solid var(--line);cursor:pointer}
  .msg:hover{background:var(--cream)}
  .msg .m-top{display:flex;gap:8px;align-items:baseline}
  .msg .dir{font-size:10px;font-weight:700;border-radius:3px;padding:1px 5px}
  .dir.in{background:var(--slatebg);color:var(--slate)}
  .dir.out{background:#ede7d8;color:var(--gold)}
  .msg .m-from{font-size:12px;font-weight:600;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .msg .m-date{font-size:11px;color:var(--muted)}
  .msg .m-snip{font-size:12px;color:var(--muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .msg .m-body{font-size:12.5px;white-space:pre-wrap;margin-top:7px;padding:9px;background:var(--cream);
    border-radius:6px;border:1px solid var(--line);max-height:300px;overflow-y:auto;display:none}
  .msg.open .m-body{display:block}
  .msg.open .m-snip{display:none}
  .sig-chip{font-size:9px;font-weight:700;letter-spacing:.4px;background:var(--redbg);color:var(--red);
    border-radius:3px;padding:1px 5px}
  /* warm-lead signals (invoice/samples WE sent) — these mean "follow up", not "stop" */
  .warm-chip{font-size:9px;font-weight:700;letter-spacing:.4px;background:var(--greenbg);color:var(--green);
    border-radius:3px;padding:1px 5px}
  .cand.warm{box-shadow:inset 3px 0 0 var(--green)}
  .cand .warms{margin-top:4px;display:flex;gap:5px;flex-wrap:wrap}
  /* ── right / pitch ── */
  .pitch-empty{padding:48px 24px;text-align:center;color:var(--muted);font-size:13px}
  .pitch{display:flex;flex-direction:column;height:100%}
  .pitch-scroll{flex:1;overflow-y:auto;padding:14px}
  .fld{margin-bottom:11px}
  .fld label{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);font-weight:700;margin-bottom:3px}
  .fld input,.fld textarea{width:100%;font:inherit;border:1px solid var(--line);border-radius:6px;
    padding:7px 9px;background:var(--paper);color:var(--ink)}
  .fld textarea{min-height:230px;resize:vertical;font-size:13px;line-height:1.55}
  .fld .ro{background:var(--cream);padding:7px 9px;border:1px solid var(--line);border-radius:6px;font-size:13px}
  .verdict{border-radius:8px;padding:11px 13px;margin-bottom:12px;border:1px solid}
  .verdict.high{background:var(--redbg);border-color:#d8b4ae}
  .verdict.elevated{background:var(--amberbg);border-color:#dcc99a}
  .verdict.caution{background:var(--slatebg);border-color:#c7d1d8}
  .verdict.clear{background:var(--greenbg);border-color:#b6d3c0}
  .verdict h3{font-size:13px;font-family:inherit}
  .verdict p{font-size:12px;margin-top:4px}
  /* ── Already-Sent panel ── */
  .sent-alert{margin:10px 12px;padding:10px 12px;background:var(--redbg);border:1px solid #d8b4ae;
    border-left:4px solid var(--red);border-radius:7px;color:var(--red)}
  .sent-alert h3{font-size:13px;font-weight:700;display:flex;align-items:center;gap:7px}
  .sent-alert .when{margin-top:4px;font-size:12px;font-weight:600}
  .sent-alert p{font-size:11.5px;margin-top:5px;color:#7a2a23}
  .sent-list{padding:4px 12px 12px}
  .sent-item{border:1px solid var(--line);border-radius:7px;padding:8px 10px;margin-bottom:8px;background:var(--paper)}
  .sent-item.fu{border-color:#d8b4ae;background:var(--redbg)}
  .sent-item .si-subj{font-size:12.5px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .sent-item .si-when{font-size:11px;color:var(--muted);margin-top:3px;display:flex;align-items:center;gap:6px}
  .sent-item.fu .si-when{color:var(--red);font-weight:600}
  .sent-item .si-snip{font-size:11px;color:#6b6456;margin-top:5px;line-height:1.5;
    display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
  .sent-item .si-tag{font-size:9px;font-weight:700;letter-spacing:.4px;background:var(--red);color:#fff;
    border-radius:4px;padding:1px 6px;text-transform:uppercase}
  .sent-item a.si-page{font-size:11px;color:var(--gold);text-decoration:none;margin-top:5px;display:inline-block}
  .override{margin-top:9px;font-size:12px;display:flex;gap:7px;align-items:flex-start}
  .override input{margin-top:2px}
  .pitch-foot{padding:11px 14px;border-top:1px solid var(--line);background:var(--paper);
    display:flex;gap:8px;align-items:center;flex-shrink:0}
  .pitch-foot .spacer{flex:1}
  .toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:var(--ink);
    color:var(--cream);padding:10px 18px;border-radius:8px;font-size:13px;opacity:0;
    transition:.25s;pointer-events:none;z-index:50}
  .toast.show{opacity:1}
  .toast.err{background:var(--red)}
  .sent-stamp{font-size:11px;color:var(--green);font-weight:700}
  .body-tabs{display:flex;align-items:center;gap:6px;margin-bottom:5px}
  .bodytab{font-size:11px;padding:3px 10px;border:1px solid var(--line);border-radius:5px;
    background:var(--paper);color:var(--muted);cursor:pointer;text-decoration:none;line-height:1.6}
  .bodytab.active{background:var(--ink);color:var(--cream);border-color:var(--ink)}
  .bodytab:hover{border-color:var(--gold)}
  .body-preview{border:1px solid var(--line);border-radius:6px;padding:16px 18px;background:#fff;
    min-height:230px;font-size:13px;line-height:1.65;color:var(--ink);overflow-wrap:anywhere}
  body.solo .rz-handle,body.solo .grip,body.solo [data-act="popout"]{display:none}
</style>
    <link rel="icon" type="image/svg+xml" href="/favicon.svg">
  </head>
<body>
<header>
  <a class="brand" href="https://designerwallcoverings.com" title="Designer Wallcoverings">
    <svg width="40" height="40" viewBox="0 0 48 48" aria-label="Designer Wallcoverings">
      <circle cx="24" cy="24" r="22" fill="none" stroke="#9a7b3f" stroke-width="1.5"/>
      <text x="24" y="31" font-family="Georgia,'Times New Roman',serif" font-size="19" font-weight="600" fill="#f6f3ec" text-anchor="middle" letter-spacing="0.5">DW</text>
    </svg>
    <span class="wm"><b>DESIGNER</b><i>WALLCOVERINGS</i></span>
  </a>
  <span class="divider"></span>
  <h1>Pitch Guard</h1>
  <span class="sub">review before you follow up</span>
  <span class="spacer"></span>
  <button class="reset" id="resetLayout" title="Reset panels to default layout">⤢ reset layout</button>
  <span class="acct" id="acct">info@designerwallcoverings.com</span>
</header>

<div class="layout" id="layout">
  <!-- DRAFTS -->
  <div class="panel" id="panel-drafts" data-panel="drafts">
    <div class="pane-hd">
      <span class="collapse-strip"><button class="hd-btn" data-act="collapse">▸</button><span class="vlabel">Drafts</span></span>
      <span class="grip">⠿</span>
      <h2>Drafts</h2><span class="count" id="draftCount">·</span>
      <span class="spacer"></span>
      <button class="btn-sm" id="scanAll" title="Scan every contact for prior orders">Scan risk</button>
      <button class="hd-btn" data-act="popout" title="Pop out into its own window">↗</button>
      <button class="hd-btn" data-act="collapse" title="Collapse panel">–</button>
    </div>
    <div class="subbar">
      <label>Sort</label>
      <select id="sortSel">
        <option value="newest">Newest first</option>
        <option value="oldest">Oldest first</option>
        <option value="invoice">Invoice sent first</option>
        <option value="samples">Samples sent first</option>
        <option value="name">Recipient name A→Z</option>
        <option value="subject">Subject A→Z</option>
        <option value="risk">Risk (high → clear)</option>
      </select>
    </div>
    <div class="pane-body" id="draftList">
      <div class="empty"><div class="spin"></div> loading drafts…</div>
    </div>
  </div>

  <!-- CORRESPONDENCE -->
  <div class="panel flexed" id="panel-corr" data-panel="corr">
    <div class="pane-hd">
      <span class="collapse-strip"><button class="hd-btn" data-act="collapse">▸</button><span class="vlabel">Correspondence</span></span>
      <span class="grip">⠿</span>
      <h2>Correspondence</h2>
      <span class="count" id="threadCount" style="display:none"></span>
      <span class="spacer"></span>
      <button class="hd-btn" data-act="popout" title="Pop out into its own window">↗</button>
      <button class="hd-btn" data-act="collapse" title="Collapse panel">–</button>
    </div>
    <div class="pane-body" id="corr">
      <div class="empty"><div class="big">✉</div>Pick a draft to see every thread<br>with that contact before you decide.</div>
    </div>
  </div>

  <!-- PITCH -->
  <div class="panel" id="panel-pitch" data-panel="pitch">
    <div class="pane-hd">
      <span class="collapse-strip"><button class="hd-btn" data-act="collapse">▸</button><span class="vlabel">The Pitch</span></span>
      <span class="grip">⠿</span>
      <h2>The Pitch</h2>
      <span class="spacer"></span>
      <button class="hd-btn" data-act="popout" title="Pop out into its own window">↗</button>
      <button class="hd-btn" data-act="collapse" title="Collapse panel">–</button>
    </div>
    <div class="pane-body" id="pitch">
      <div class="pitch-empty">No draft selected.<br>The followup waiting for this client will appear here.</div>
    </div>
  </div>

  <!-- ALREADY SENT -->
  <div class="panel" id="panel-sent" data-panel="sent">
    <div class="pane-hd">
      <span class="collapse-strip"><button class="hd-btn" data-act="collapse">▸</button><span class="vlabel">Already Sent</span></span>
      <span class="grip">⠿</span>
      <h2>Already Sent</h2><span class="count" id="sentCount" style="display:none"></span>
      <span class="spacer"></span>
      <button class="hd-btn" data-act="popout" title="Pop out into its own window">↗</button>
      <button class="hd-btn" data-act="collapse" title="Collapse panel">–</button>
    </div>
    <div class="pane-body" id="sentBody">
      <div class="empty">Pick a draft to see followups already sent to that contact.</div>
    </div>
  </div>
</div>

<div class="toast" id="toast"></div>

<script>
const $ = s => document.querySelector(s);
let DRAFTS = [], CURRENT = null, MORE = false;
let vendorsOpen = localStorage.getItem('pg.vendorsOpen') === '1';
let sortMode = localStorage.getItem('pg.sort') || 'newest';
let CANDIDATES = [], candScanned = false, candScanning = false, candScannedAt = null;
let candOpen = localStorage.getItem('pg.candOpen') !== '0';
const riskCache = {}; // email -> full /api/contact result
const liteCache = {}; // email -> light /api/risk result
const verdictOf = email => riskCache[email] || liteCache[email] || null;

function toast(msg, err){ const t=$('#toast'); t.textContent=msg; t.className='toast show'+(err?' err':''); setTimeout(()=>t.className='toast',2600); }
function esc(s){ return String(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
function fmtDate(d){ if(!d) return ''; const t=Date.parse(d); if(!t) return d; const dt=new Date(t);
  return dt.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'2-digit'}); }
function fmtDateTime(d){ if(!d) return ''; const t=Date.parse(d); if(!t) return d; const dt=new Date(t);
  return dt.toLocaleString('en-US',{month:'short',day:'numeric',year:'numeric',hour:'numeric',minute:'2-digit'}); }
const riskWord = {high:'High risk',elevated:'Elevated',caution:'Caution',clear:'Clear'};

/* ════════ LAYOUT MANAGER — resize · collapse · reorder · pop-out ════════ */
const PANELS = ['drafts','corr','pitch','sent'];
const LS = 'pg.layout.v2';
const DEFAULT_LAYOUT = { order:['drafts','corr','pitch','sent'], width:{drafts:336,corr:0,pitch:438,sent:308}, collapsed:{} };
let layout = loadLayout();

function loadLayout(){
  try{ const v=JSON.parse(localStorage.getItem(LS)); if(v&&v.order) return Object.assign({},DEFAULT_LAYOUT,v,{width:Object.assign({},DEFAULT_LAYOUT.width,v.width),collapsed:v.collapsed||{}}); }catch(e){}
  return JSON.parse(JSON.stringify(DEFAULT_LAYOUT));
}
function saveLayout(){ try{ localStorage.setItem(LS,JSON.stringify(layout)); }catch(e){} }

// apply order + widths + collapsed to the DOM
function applyLayout(){
  const lay = $('#layout');
  // strip existing handles
  lay.querySelectorAll('.rz-handle').forEach(h=>h.remove());
  // order the panels
  layout.order.forEach(key=>{
    const p = document.getElementById('panel-'+key);
    if(p) lay.appendChild(p);
  });
  // the middle slot flexes; edges are fixed-width
  layout.order.forEach((key,i)=>{
    const p = document.getElementById('panel-'+key);
    if(!p) return;
    p.classList.toggle('collapsed', !!layout.collapsed[key]);
    const mid = (i===1);
    p.classList.toggle('flexed', mid && !layout.collapsed[key]);
    if(!mid && !layout.collapsed[key]){
      p.style.flex = '0 0 ' + (layout.width[key]||340) + 'px';
    } else {
      p.style.flex = '';
    }
  });
  // insert resize handles between panels
  for(let i=0;i<layout.order.length-1;i++){
    const after = document.getElementById('panel-'+layout.order[i]);
    const h = document.createElement('div');
    h.className='rz-handle'; h.dataset.idx=i;
    after.after(h);
    wireHandle(h, i);
  }
  saveLayout();
}

// resize: handle i sits between slot i and slot i+1; drag resizes the non-middle one
function wireHandle(h, i){
  h.addEventListener('pointerdown', e=>{
    e.preventDefault();
    const leftKey = layout.order[i], rightKey = layout.order[i+1];
    // pick the edge (non-middle) panel this handle should resize
    const targetKey = (i===0) ? leftKey : rightKey;
    const grow = (i===0) ? 1 : -1; // dragging right widens slot0; widens slot2 when dragging left
    const startX = e.clientX, startW = layout.width[targetKey] || document.getElementById('panel-'+targetKey).offsetWidth;
    h.classList.add('active');
    h.setPointerCapture(e.pointerId);
    const move = ev=>{
      let w = startW + grow*(ev.clientX-startX);
      w = Math.max(180, Math.min(900, w));
      layout.width[targetKey] = w;
      const p = document.getElementById('panel-'+targetKey);
      if(p) p.style.flex = '0 0 '+w+'px';
    };
    const up = ()=>{ h.classList.remove('active'); h.removeEventListener('pointermove',move);
      h.removeEventListener('pointerup',up); saveLayout(); };
    h.addEventListener('pointermove',move);
    h.addEventListener('pointerup',up);
  });
}

// reorder: drag a panel by its header onto another panel
let dragKey = null;
function wireDragReorder(){
  PANELS.forEach(key=>{
    const p = document.getElementById('panel-'+key);
    const hd = p.querySelector('.pane-hd');
    hd.setAttribute('draggable','true');
    hd.addEventListener('dragstart', e=>{
      dragKey = key; p.classList.add('dragging');
      e.dataTransfer.effectAllowed='move';
      try{ e.dataTransfer.setData('text/plain',key); }catch(_){}
    });
    hd.addEventListener('dragend', ()=>{ p.classList.remove('dragging');
      PANELS.forEach(k=>document.getElementById('panel-'+k).classList.remove('drop-target')); dragKey=null; });
    p.addEventListener('dragover', e=>{ if(dragKey&&dragKey!==key){ e.preventDefault(); p.classList.add('drop-target'); } });
    p.addEventListener('dragleave', ()=>p.classList.remove('drop-target'));
    p.addEventListener('drop', e=>{
      e.preventDefault(); p.classList.remove('drop-target');
      if(!dragKey||dragKey===key) return;
      const ord = layout.order.slice();
      ord.splice(ord.indexOf(dragKey),1);
      ord.splice(ord.indexOf(key),0,dragKey);
      layout.order = ord; applyLayout();
    });
  });
}

function toggleCollapse(key){
  layout.collapsed[key] = !layout.collapsed[key];
  applyLayout();
}
function popout(key){
  const u = new URL(location.href);
  u.searchParams.set('only', key);
  if(CURRENT) u.searchParams.set('draft', CURRENT);
  window.open(u.toString(), 'pg-'+key, 'width=560,height=900');
}
function resetLayout(){ localStorage.removeItem(LS); layout = JSON.parse(JSON.stringify(DEFAULT_LAYOUT)); applyLayout(); toast('Layout reset'); }

// header buttons (collapse / popout) + collapsed-strip click
function wirePanelControls(){
  PANELS.forEach(key=>{
    const p = document.getElementById('panel-'+key);
    if(!p) return; // removed in solo mode
    p.querySelectorAll('[data-act]').forEach(btn=>{
      btn.addEventListener('click', e=>{
        e.stopPropagation();
        if(btn.dataset.act==='collapse') toggleCollapse(key);
        else if(btn.dataset.act==='popout') popout(key);
      });
    });
    // clicking anywhere on a collapsed panel re-expands it
    p.querySelector('.pane-hd').addEventListener('click', e=>{
      if(p.classList.contains('collapsed') && !e.target.closest('[data-act]')) toggleCollapse(key);
    });
  });
}

// solo mode — a popped-out single panel
function applySoloMode(){
  const params = new URLSearchParams(location.search);
  const only = params.get('only');
  if(!only || !PANELS.includes(only)) return null;
  document.body.classList.add('solo');
  PANELS.forEach(k=>{
    const p = document.getElementById('panel-'+k);
    if(k===only){ p.classList.add('flexed'); p.classList.remove('collapsed'); p.style.flex='1 1 0'; }
    else p.remove();
  });
  $('#resetLayout').style.display='none';
  document.querySelector('header .sub').textContent = only==='drafts'?'drafts':only==='corr'?'correspondence':'the pitch';
  return params.get('draft');
}

/* ════════ DATA ════════ */
async function loadDrafts(){
  try{
    const r = await fetch('/api/drafts'); const d = await r.json();
    if(d.error) throw new Error(d.error);
    DRAFTS = d.drafts; MORE = d.more;
    const acct = $('#acct'); if(acct) acct.textContent = d.account;
    const dc = $('#draftCount'); if(dc) dc.textContent = d.count + (d.more ? '+' : '');
    renderDrafts();
  }catch(e){ const el = $('#draftList'); if(el) el.innerHTML = '<div class="empty">⚠ '+esc(e.message)+'</div>'; }
}
const RISK_RANK = {high:0,elevated:1,caution:2,clear:3};
function sortDrafts(arr){
  const a = arr.slice(), ts = d => Date.parse(d.date)||0;
  const nm = d => (d.toName||'').toLowerCase();
  if(sortMode==='oldest') a.sort((x,y)=>ts(x)-ts(y));
  else if(sortMode==='name') a.sort((x,y)=>nm(x).localeCompare(nm(y)));
  else if(sortMode==='subject') a.sort((x,y)=>(x.subject||'').toLowerCase().localeCompare((y.subject||'').toLowerCase()));
  else if(sortMode==='risk') a.sort((x,y)=>{
    const rx=RISK_RANK[(verdictOf(x.toEmail)||{}).risk]??9, ry=RISK_RANK[(verdictOf(y.toEmail)||{}).risk]??9;
    return rx-ry || ts(y)-ts(x);
  });
  else a.sort((x,y)=>ts(y)-ts(x)); // newest
  return a;
}
// Followup candidates carry warm signals (INVOICE SENT / SAMPLES SENT we sent). Default
// order = warm leads first, then longest-quiet. The 'invoice'/'samples' sort options
// pull that signal to the very top — the "find who needs a follow" criteria Steve asked for.
function sortCandidates(arr){
  const a = arr.slice();
  const hasInv = c => (c.warmSignals||[]).includes('INVOICE SENT') ? 1 : 0;
  const hasSam = c => (c.warmSignals||[]).includes('SAMPLES SENT') ? 1 : 0;
  const warmN  = c => (c.warmSignals||[]).length ? 1 : 0;
  if(sortMode==='invoice') a.sort((x,y)=> hasInv(y)-hasInv(x) || (y.sentInvoiceTs||0)-(x.sentInvoiceTs||0) || y.daysSince-x.daysSince);
  else if(sortMode==='samples') a.sort((x,y)=> hasSam(y)-hasSam(x) || (y.sentSamplesTs||0)-(x.sentSamplesTs||0) || y.daysSince-x.daysSince);
  else a.sort((x,y)=> warmN(y)-warmN(x) || y.daysSince-x.daysSince); // warm-first, then longest-quiet
  return a;
}
function draftChip(d){
  const c = verdictOf(d.toEmail);
  let badge = '<span class="badge r-pending">·</span>';
  if(c) badge = '<span class="badge r-'+c.risk+'">'+c.risk+'</span>';
  return '<div class="draft'+(CURRENT===d.id?' active':'')+'" data-id="'+d.id+'">'+
    badge+
    '<div class="who">'+esc(d.toName)+'</div>'+
    '<div class="subj">'+esc(d.subject)+'</div>'+
    '<div class="meta">'+esc(d.toEmail||'no recipient')+' · '+fmtDate(d.date)+'</div>'+
    '<button class="draft-del" data-del="'+d.id+'" title="Move draft to Trash">✕</button>'+
  '</div>';
}
function candidateSectionHTML(){
  let body;
  if(candScanning) body='<div class="empty" style="padding:24px"><div class="spin"></div> scanning sent mail for ghosted quotes…</div>';
  else if(!candScanned) body='<div class="vgroup-note">Not scanned yet — click <b>Scan</b> to find quote/pricing threads we replied to that got no client response in 5+ days and have no followup drafted.</div>';
  else if(!CANDIDATES.length) body='<div class="vgroup-note">No followup candidates — every ghosted quote in range already has a draft.'+(candScannedAt?' <span style="opacity:.7">· auto-scanned '+fmtDate(candScannedAt)+'</span>':'')+'</div>';
  else body=(candScannedAt?'<div class="vgroup-note">Auto-scans daily · last scan '+fmtDate(candScannedAt)+' — review, then ✎ Draft it. <b>Green chips</b> = invoice/samples we sent (top follow-up priority).</div>':'')+sortCandidates(CANDIDATES).map(c=>{
    const warms=(c.warmSignals||[]).map(w=>'<span class="warm-chip">'+esc(w)+'</span>').join(' ');
    return '<div class="cand'+((c.warmSignals||[]).length?' warm':'')+'" data-email="'+esc(c.contact)+'">'+
      '<button class="cand-draft" data-draft-cand="'+esc(c.contact)+'" title="Create the followup draft">✎ Draft it</button>'+
      '<div class="who">'+esc(c.contactName)+(c.uncertain?' <span title="Reply/draft check was throttled — shown so it is not silently dropped; verify the thread before sending" style="color:#b8860b;font-size:11px">⚠ verify</span>':'')+'</div>'+
      '<div class="subj">'+esc(c.subject)+'</div>'+
      '<div class="meta">'+esc(c.contact)+' · '+c.daysSince+'d no reply</div>'+
      (warms?'<div class="warms">'+warms+'</div>':'')+
    '</div>';}).join('');
  return '<div class="cgroup-hd'+(candOpen?'':' closed')+'" id="cgroupHd" title="Quotes with no client reply in 5+ days — the 5-day followup rule">'+
      '<span class="arrow">▼</span> Followup Candidates '+
      '<span class="vct">'+(candScanned?CANDIDATES.length:'—')+'</span>'+
      '<button class="btn-sm scan-btn" id="scanCandBtn">'+(candScanned?'Rescan':'Scan')+'</button>'+
    '</div>'+
    '<div id="cgroupBody" style="display:'+(candOpen?'block':'none')+'">'+body+'</div>';
}
function renderDrafts(){
  const el = $('#draftList');
  if(!el) return; // drafts panel popped out / not in this window
  const clients = sortDrafts(DRAFTS.filter(d=>d.kind!=='vendor'));
  const vendors = sortDrafts(DRAFTS.filter(d=>d.kind==='vendor'));
  let html = candidateSectionHTML();
  if(!DRAFTS.length){
    html += '<div class="empty">No drafts loaded.</div>';
  } else {
    if(MORE) html+='<div class="meta" style="padding:7px 14px;color:var(--muted);border-bottom:1px solid var(--line)">Showing the '+DRAFTS.length+' most recent followup drafts — the bot backlog is larger.</div>';
    html += clients.length ? clients.map(draftChip).join('') : '<div class="empty">No client drafts in this batch.</div>';
    if(vendors.length){
      html += '<div class="vgroup-hd'+(vendorsOpen?'':' closed')+'" id="vgroupHd" title="Vendor / supplier drafts — not clients to pitch">'+
        '<span class="arrow">▼</span> Vendors <span class="vct">'+vendors.length+'</span></div>'+
        '<div id="vgroupBody" style="display:'+(vendorsOpen?'block':'none')+'">'+
          '<div class="vgroup-note">Drafts addressed to suppliers / manufacturers — not clients. Review, don’t pitch.</div>'+
          vendors.map(draftChip).join('')+
        '</div>';
    }
  }
  el.innerHTML = html;
  el.querySelectorAll('.draft').forEach(n=>n.onclick=()=>selectDraft(n.dataset.id));
  el.querySelectorAll('.draft-del').forEach(b=>b.onclick=e=>{ e.stopPropagation(); deleteDraft(b.dataset.del); });
  el.querySelectorAll('.cand').forEach(n=>n.onclick=ev=>{ if(ev.target.closest('[data-draft-cand]'))return; loadContact(n.dataset.email); });
  el.querySelectorAll('[data-draft-cand]').forEach(b=>b.onclick=ev=>{ ev.stopPropagation(); createFollowup(b.dataset.draftCand); });
  const ch=$('#cgroupHd');
  if(ch) ch.onclick=ev=>{
    if(ev.target.closest('#scanCandBtn')) return;
    candOpen=!candOpen; localStorage.setItem('pg.candOpen',candOpen?'1':'0');
    $('#cgroupBody').style.display=candOpen?'block':'none'; ch.classList.toggle('closed',!candOpen);
  };
  const scb=$('#scanCandBtn'); if(scb) scb.onclick=ev=>{ ev.stopPropagation(); scanCandidates(); };
  const vh = $('#vgroupHd');
  if(vh) vh.onclick=()=>{
    vendorsOpen=!vendorsOpen;
    localStorage.setItem('pg.vendorsOpen', vendorsOpen?'1':'0');
    $('#vgroupBody').style.display = vendorsOpen?'block':'none';
    vh.classList.toggle('closed', !vendorsOpen);
  };
  const dc=$('#draftCount'); if(dc) dc.textContent = clients.length + (MORE?'+':'');
}
async function scanCandidates(){
  if(candScanning) return;
  candScanning=true; renderDrafts();
  try{
    const r=await fetch('/api/followup-candidates'+(candScanned?'?fresh=1':''));
    const d=await r.json();
    if(d.error) throw new Error(d.error);
    CANDIDATES=d.candidates||[]; candScanned=true; candScannedAt=d.scannedAt||new Date().toISOString();
    toast('Scan done — '+CANDIDATES.length+' followup candidate'+(CANDIDATES.length===1?'':'s'));
  }catch(e){ toast('Scan failed: '+e.message,1); }
  candScanning=false; renderDrafts();
}
// quiet auto-load on page open — the server keeps the candidate list warm
async function autoLoadCandidates(){
  try{
    const r=await fetch('/api/followup-candidates');
    const d=await r.json();
    if(d.error) return;
    CANDIDATES=d.candidates||[]; candScanned=true; candScannedAt=d.scannedAt||null;
    renderDrafts();
  }catch(e){}
}
async function createFollowup(email){
  const c=CANDIDATES.find(x=>x.contact===email); if(!c) return;
  if(!confirm('Create a followup draft for:\n\n'+c.contactName+' <'+c.contact+'>\n"'+c.subject+'"  ('+c.daysSince+'d no reply)\n\nIt lands as a draft in info@ — nothing is sent until you send it.')) return;
  try{
    const r=await fetch('/api/followup/create',{method:'POST',headers:{'Content-Type':'application/json'},
      body:JSON.stringify({to:c.contact,subject:c.subject,threadId:c.threadId})});
    const out=await r.json();
    if(out.error) throw new Error(out.error);
    CANDIDATES=CANDIDATES.filter(x=>x.contact!==email);
    renderDrafts();
    toast('Followup draft created for '+c.contact);
  }catch(e){ toast('Draft failed: '+e.message,1); }
}

async function deleteDraft(id){
  const d = DRAFTS.find(x=>x.id===id); if(!d) return;
  if(!confirm('Move this draft to Trash?\n\nTo: '+(d.toEmail||d.toName||'(no recipient)')+'\n'+(d.subject||'')+
    '\n\nIt stays recoverable in Gmail Trash for 30 days.')) return;
  try{
    const r = await fetch('/api/draft/'+encodeURIComponent(id)+'/trash',{method:'POST'});
    const out = await r.json();
    if(out.error) throw new Error(out.error);
    DRAFTS = DRAFTS.filter(x=>x.id!==id);
    if(CURRENT===id){
      CURRENT=null;
      const corr=$('#corr'); if(corr) corr.innerHTML='<div class="empty"><div class="big">✉</div>Pick a draft to see every thread<br>with that contact before you decide.</div>';
      const pit=$('#pitch'); if(pit) pit.innerHTML='<div class="pitch-empty">No draft selected.<br>The followup waiting for this client will appear here.</div>';
    }
    renderDrafts();
    toast('Draft moved to Trash');
  }catch(e){ toast('Delete failed: '+e.message,1); }
}
async function selectDraft(id){
  CURRENT = id; renderDrafts();
  const d = DRAFTS.find(x=>x.id===id); if(!d) return;
  const corr = $('#corr');
  if(corr){
    if(!d.toEmail) corr.innerHTML = '<div class="empty">This draft has no recipient address — nothing to cross-check.</div>';
    else { corr.innerHTML = '<div class="empty"><div class="spin"></div> pulling every thread with '+esc(d.toEmail)+'…</div>'; loadContact(d.toEmail); }
  } else if(d.toEmail){ loadContact(d.toEmail); }
  const sb=$('#sentBody');
  if(sb){ sb.innerHTML = d.toEmail ? '<div class="empty"><div class="spin"></div> checking followups sent to '+esc(d.toEmail)+'…</div>' : '<div class="empty">This draft has no recipient address.</div>'; }
  loadPitch(d);
}

async function loadContact(email){
  try{
    let c = riskCache[email];
    if(!c){ const r=await fetch('/api/contact?email='+encodeURIComponent(email)); c=await r.json();
      if(c.error) throw new Error(c.error); riskCache[email]=c; renderDrafts(); }
    renderCorr(c); renderVerdict(c); renderSent(c);
  }catch(e){ if($('#corr')) $('#corr').innerHTML='<div class="empty">⚠ '+esc(e.message)+'</div>';
    if($('#sentBody')) $('#sentBody').innerHTML='<div class="empty">⚠ '+esc(e.message)+'</div>'; }
}
// ── 4th column: followups/letters already sent to this contact ──
function renderSent(c){
  const body = $('#sentBody'); if(!body) return; // panel not in this window
  const cnt = $('#sentCount');
  const letters = (c.sentLetters||[]).slice().sort((a,b)=>(b.ts||0)-(a.ts||0));
  const fu = c.recentFollowup;             // the most-recent matched followup (last 21d)
  if(cnt){ cnt.style.display='inline-block'; cnt.textContent=letters.length; }
  let html='';
  if(fu){
    const days=fu.daysAgo;
    html += '<div class="sent-alert"><h3>⚠ Followup already sent</h3>'+
      '<div class="when">'+esc(fmtDateTime(fu.date))+' · '+(days===0?'today':days+' day'+(days===1?'':'s')+' ago')+'</div>'+
      '<p>A standard followup already went out to this contact. Sending another now reads as spam — hold off unless you have a reason.</p></div>';
  }
  if(!letters.length){
    html += '<div class="empty">'+(fu?'':'✓ ')+'No prior letters found to '+esc(c.email||'this contact')+(fu?'':' — clear to follow up.')+'</div>';
  } else {
    html += '<div class="sent-list">';
    for(const m of letters){
      const isFu = fu && Math.abs((m.ts||0)-(fu.ts||0))<60000; // same message as the matched followup
      const page = (m.id && !String(m.id).startsWith('ledger:')) ? '<a class="si-page" href="/api/draft/'+encodeURIComponent(m.id)+'/html" target="_blank" rel="noopener noreferrer">↗ View letter</a>' : '';
      html += '<div class="sent-item'+(isFu?' fu':'')+'">'+
        '<div class="si-subj">'+(isFu?'<span class="si-tag">followup</span> ':'')+esc(m.subject||'(no subject)')+'</div>'+
        '<div class="si-when">🕓 '+esc(fmtDateTime(m.date))+'</div>'+
        (m.snippet?'<div class="si-snip">'+esc(m.snippet)+'</div>':'')+
        page+'</div>';
    }
    html += '</div>';
  }
  body.innerHTML = html;
}
function renderCorr(c){
  if(!$('#corr')) return; // correspondence panel not in this window
  $('#threadCount').style.display='inline-block';
  $('#threadCount').textContent = c.threadCount+' thread'+(c.threadCount===1?'':'s');
  const sigChips = c.signals.map(s=>'<span class="badge r-high">'+esc(s)+'</span>').join(' ');
  let banner='';
  if(c.risk==='high') banner='<div class="risk-banner high"><h3>⛔ Already a customer — do NOT pitch</h3>'+
    '<p>This contact has thread activity that looks like a completed order. Sending a generic followup here is exactly what upsets Samantha. Verify with her before anything goes out.</p>'+
    (sigChips?'<div class="sigs">'+sigChips+'</div>':'')+'</div>';
  else if(c.risk==='elevated') banner='<div class="risk-banner elevated"><h3>⚠ Likely transacting</h3>'+
    '<p>Invoice / order signals found across this contact’s threads. Read them before deciding to follow up.</p>'+
    (sigChips?'<div class="sigs">'+sigChips+'</div>':'')+'</div>';
  else if(c.risk==='caution') banner='<div class="risk-banner caution"><h3>↔ Multiple threads</h3>'+
    '<p>More than one thread with this contact. Make sure the followup is not duplicating one already handled.</p></div>';
  else banner='<div class="risk-banner clear"><h3>✓ Clear to follow up</h3>'+
    '<p>No invoice / payment / shipping signals found in this contact’s history.</p></div>';

  if(c.recentFollowup){
    const fw=c.recentFollowup.daysAgo;
    if(c.risk==='clear'||c.risk==='caution') banner=banner.replace(/risk-banner (clear|caution)/,'risk-banner elevated');
    banner=banner.replace(/<\/div>\s*$/,'')+
      '<p style="margin-top:8px;font-weight:600;color:var(--amber);">⚠ Followup already sent '+
      (fw===0?'today':fw+' day'+(fw===1?'':'s')+' ago')+
      ' — a second one this soon reads as spam. Hold off.</p></div>';
  }

  const threads = c.threads.map(t=>{
    const tsig = t.signals.length? ' '+t.signals.map(s=>'<span class="sig-chip">'+esc(s)+'</span>').join('') : '';
    const msgs = t.messages.map(m=>{
      const ms = m.signals.length? ' '+m.signals.map(s=>'<span class="sig-chip">'+esc(s)+'</span>').join('') : '';
      return '<div class="msg" data-mid="'+m.id+'">'+
        '<div class="m-top"><span class="dir '+(m.inbound?'in':'out')+'">'+(m.inbound?'IN':'OUT')+'</span>'+
        '<span class="m-from">'+esc(m.inbound?(m.fromName||m.from):'Designer Wallcoverings')+'</span>'+
        '<span class="m-date">'+fmtDate(m.date)+'</span></div>'+
        '<div class="m-snip">'+esc(m.snippet)+ms+'</div>'+
        '<div class="m-body" data-loaded="0">'+ms+'</div>'+
      '</div>';
    }).join('');
    return '<div class="thread"><div class="thread-hd"><span class="t-subj">'+esc(t.subject)+tsig+'</span>'+
      '<span class="t-meta">'+t.messages.length+' msg · '+fmtDate(t.lastDate)+'</span></div>'+msgs+'</div>';
  }).join('');

  // Letters we've sent to this contact — dates + expandable full bodies.
  let sentBlock='';
  const sl = c.sentLetters||[];
  if(sl.length){
    const rows = sl.map(m=>
      '<div class="msg sent-letter" data-mid="'+esc(m.id)+'"'+(m.fromLedger?' data-ledger="1"':'')+'>'+
        '<div class="m-top"><span class="dir out">SENT</span>'+
        '<span class="m-from">'+esc(m.subject)+'</span>'+
        '<span class="m-date">'+fmtDate(m.date)+'</span></div>'+
        '<div class="m-snip">'+esc(m.snippet||'')+'</div>'+
        '<div class="m-body" data-loaded="0"'+(m.fromLedger?(' data-ledgerbody="'+esc(m.ledgerBody||'')+'"'):'')+'></div>'+
      '</div>').join('');
    sentBlock='<div class="thread sent-letters-wrap"><div class="thread-hd">'+
      '<span class="t-subj">✉ Letters we’ve sent to '+esc(c.email)+'</span>'+
      '<span class="t-meta">'+sl.length+' sent</span></div>'+rows+'</div>';
  }

  $('#corr').innerHTML = banner +
    '<div class="contact-hd"><b>'+esc(c.email)+'</b> — '+c.totalMessages+' messages across '+c.threadCount+' thread'+(c.threadCount===1?'':'s')+
    (c.customerReplied?' · customer has replied':'')+(sl.length?' · '+sl.length+' letter'+(sl.length===1?'':'s')+' sent':'')+'</div>' + sentBlock + threads;
  $('#corr').querySelectorAll('.msg').forEach(n=>n.onclick=()=>toggleMsg(n));
}
async function toggleMsg(node){
  node.classList.toggle('open');
  const body = node.querySelector('.m-body');
  if(node.classList.contains('open') && body.dataset.loaded==='0'){
    body.dataset.loaded='1';
    // ledger letters carry their full body inline — no fetch needed
    if(node.dataset.ledger==='1'){
      const lb=body.dataset.ledgerbody||'';
      body.innerHTML = renderEmailHTML(stripBanner(lb)) || '(no text body)';
      return;
    }
    body.textContent='loading…';
    try{ const r=await fetch('/api/message/'+node.dataset.mid); const m=await r.json();
      body.textContent = m.body || m.snippet || '(no text body)';
    }catch(e){ body.textContent='⚠ '+e.message; }
  }
}

// George prepends an internal "From job:" banner to drafts — strip it for clean view/edit
function stripBanner(s){ return String(s||'').replace(/<div style="font-family:ui-monospace[^>]*>[\s\S]*?<\/div>\s*/i,''); }
function renderEmailHTML(text){
  if(/<(br|div|p|a|span|strong|em|ul|ol|li|table|h[1-6])\b/i.test(text)) return text;
  return esc(text).replace(/\n/g,'<br>');
}
function setBodyMode(mode){
  const ta=$('#pBody'), pv=$('#pPreview'), be=$('#btEdit'), bp=$('#btPrev');
  if(!ta||!pv) return;
  if(mode==='preview'){
    pv.innerHTML=renderEmailHTML(ta.value); pv.style.display='block'; ta.style.display='none';
    bp&&bp.classList.add('active'); be&&be.classList.remove('active');
  } else {
    ta.style.display='block'; pv.style.display='none';
    be&&be.classList.add('active'); bp&&bp.classList.remove('active');
  }
}
async function loadPitch(d){
  const el = $('#pitch');
  if(!el) return; // pitch panel not in this window
  el.innerHTML = '<div class="pitch-empty"><div class="spin"></div> loading draft…</div>';
  let body='';
  try{ const r=await fetch('/api/message/'+d.id); const m=await r.json(); body=stripBanner(m.body||''); }
  catch(e){ body='(could not load draft body: '+e.message+')'; }
  el.innerHTML =
    '<div class="pitch">'+
      '<div class="pitch-scroll">'+
        '<div id="verdictSlot"></div>'+
        '<div class="fld"><label>To</label><div class="ro">'+esc(d.to||'(no recipient)')+'</div></div>'+
        '<div class="fld"><label>Subject</label><input id="pSubj" value="'+esc(d.subject)+'"></div>'+
        '<div class="fld">'+
          '<div class="body-tabs">'+
            '<label style="margin:0;flex:1">Pitch / followup body — edit before sending</label>'+
            '<button type="button" id="btEdit" class="bodytab active">✎ Edit</button>'+
            '<button type="button" id="btPrev" class="bodytab">👁 Preview</button>'+
            '<a href="/api/draft/'+d.id+'/html" target="_blank" rel="noopener noreferrer" class="bodytab" title="Open the rendered email as a page">↗ Page</a>'+
          '</div>'+
          '<textarea id="pBody">'+esc(body)+'</textarea>'+
          '<div id="pPreview" class="body-preview" style="display:none"></div>'+
        '</div>'+
      '</div>'+
      '<div class="pitch-foot">'+
        '<span id="sendState"></span><span class="spacer"></span>'+
        '<button id="delBtn" class="btn-sm" style="color:var(--red);border-color:#d8b4ae" title="Move this draft to Trash">🗑 Delete draft</button>'+
        '<button id="skipBtn" class="btn-sm">Skip</button>'+
        '<button id="sendBtn" class="btn-go">Send followup</button>'+
      '</div>'+
    '</div>';
  $('#skipBtn').onclick=()=>{ toast('Skipped — draft left untouched in Gmail'); };
  $('#delBtn').onclick=()=>deleteDraft(d.id);
  $('#sendBtn').onclick=()=>sendPitch(d);
  $('#btEdit').onclick=()=>setBodyMode('edit');
  $('#btPrev').onclick=()=>setBodyMode('preview');
  const c = verdictOf(d.toEmail);
  if(c) renderVerdict(c);
  else if(d.toEmail) $('#verdictSlot').innerHTML='<div class="verdict caution"><h3><span class="spin"></span> Checking every thread with this contact…</h3></div>';
  else $('#verdictSlot').innerHTML='<div class="verdict caution"><h3>No recipient on this draft</h3></div>';
}
function renderVerdict(c){
  const slot = $('#verdictSlot'); if(!slot) return;
  let html='', gated = (c.risk==='high'||c.risk==='elevated');
  if(c.risk==='high') html='<h3>⛔ '+riskWord.high+'</h3><p>Order / payment activity found. This client should likely <b>not</b> get a followup pitch. Confirm with Samantha first.</p>';
  else if(c.risk==='elevated') html='<h3>⚠ '+riskWord.elevated+'</h3><p>Invoice / order signals found in other threads — review the correspondence before sending.</p>';
  else if(c.risk==='caution') html='<h3>↔ '+riskWord.caution+'</h3><p>Multiple threads exist. Make sure this followup is not a duplicate.</p>';
  else html='<h3>✓ '+riskWord.clear+'</h3><p>No prior-order signals. Safe to follow up.</p>';
  let boxClass = c.risk;
  if(c.recentFollowup){
    const fw=c.recentFollowup.daysAgo;
    html+='<p style="margin-top:8px;padding:8px 10px;background:var(--redbg);border:1px solid #d8b4ae;'+
      'border-left:4px solid var(--red);border-radius:6px;'+
      'color:var(--red);font-weight:700;font-size:12px;">⚠ A standard followup was already sent to '+
      'this contact on '+esc(fmtDateTime(c.recentFollowup.date))+' ('+(fw===0?'today':fw+' day'+(fw===1?'':'s')+' ago')+
      '). Sending another now reads as spam — hold off. See <b>Already Sent →</b></p>';
    if(c.risk==='clear'||c.risk==='caution') boxClass='elevated';
  }
  if(gated) html+='<label class="override"><input type="checkbox" id="ovr">'+
    '<span>I reviewed every thread and confirm this followup is still appropriate.</span></label>';
  slot.className=''; slot.innerHTML='<div class="verdict '+boxClass+'">'+html+'</div>';
  const sb=$('#sendBtn');
  if(sb){
    if(gated){ sb.disabled=true; sb.classList.add('btn-danger'); sb.classList.remove('btn-go');
      const ov=$('#ovr'); if(ov) ov.onchange=()=>{ sb.disabled=!ov.checked; }; }
    else { sb.disabled=false; sb.classList.add('btn-go'); sb.classList.remove('btn-danger'); }
  }
}

async function sendPitch(d){
  const subj=$('#pSubj').value.trim(), body=$('#pBody').value.trim();
  if(!d.toEmail){ toast('No recipient on this draft',1); return; }
  if(!body){ toast('Body is empty',1); return; }
  const c = riskCache[d.toEmail];
  let warn='Send this followup to '+d.toEmail+'?';
  if(c && (c.risk==='high'||c.risk==='elevated')) warn='⚠ '+riskWord[c.risk]+' — '+c.signals.join(', ')+
    ' signals found. Send anyway to '+d.toEmail+'?';
  if(!confirm(warn)) return;
  const sb=$('#sendBtn'); sb.disabled=true; $('#sendState').innerHTML='<span class="spin"></span> sending…';
  try{
    const r=await fetch('/api/send',{method:'POST',headers:{'Content-Type':'application/json'},
      body:JSON.stringify({to:d.toEmail,subject:subj,body:body.replace(/\n/g,'<br>'),threadId:d.threadId})});
    const out=await r.json();
    if(out.error) throw new Error(out.error);
    $('#sendState').innerHTML='<span class="sent-stamp">✓ sent in-thread</span>';
    toast('Followup sent to '+d.toEmail);
    // Process finished successfully → auto-remove this draft (Steve 2026-06-30). The followup
    // was sent, so the leftover Gmail draft is done: trash it (reversible → Gmail Trash, 30-day
    // recovery) and drop it from the list immediately so it can't linger or be re-sent.
    try{ const tr=await fetch('/api/draft/'+encodeURIComponent(d.id)+'/trash',{method:'POST'}); await tr.json(); }catch(_){}
    DRAFTS = DRAFTS.filter(x=>x.id!==d.id);
    if(CURRENT===d.id){
      CURRENT=null;
      const corr=$('#corr'); if(corr) corr.innerHTML='<div class="empty"><div class="big">✉</div>Pick a draft to see every thread<br>with that contact before you decide.</div>';
      const pit=$('#pitch'); if(pit) pit.innerHTML='<div class="pitch-empty">No draft selected.<br>The followup waiting for this client will appear here.</div>';
    }
    renderDrafts();
  }catch(e){ $('#sendState').textContent=''; sb.disabled=false; toast('Send failed: '+e.message,1); }
}

async function scanAll(){
  const btn=$('#scanAll'); btn.disabled=true;
  const targets=[...new Set(DRAFTS.filter(d=>d.kind!=='vendor'&&d.toEmail&&!verdictOf(d.toEmail)).map(d=>d.toEmail))];
  if(!targets.length){ btn.disabled=false; toast('Every draft already scanned'); return; }
  let i=0, done=0;
  const tick=()=>{ btn.textContent='scanning '+done+'/'+targets.length; };
  tick();
  async function worker(){
    while(i<targets.length){
      const email=targets[i++];
      try{ const r=await fetch('/api/risk?email='+encodeURIComponent(email)); const v=await r.json();
        if(!v.error) liteCache[email]=v; }catch(e){}
      done++; tick(); renderDrafts();
      await new Promise(r=>setTimeout(r,250));
    }
  }
  await Promise.all([worker(),worker()]);
  btn.disabled=false; btn.textContent='Scan risk';
  const high=DRAFTS.filter(d=>{const v=verdictOf(d.toEmail);return v&&v.risk==='high';}).length;
  const elev=DRAFTS.filter(d=>{const v=verdictOf(d.toEmail);return v&&v.risk==='elevated';}).length;
  toast('Scan done — '+high+' high-risk, '+elev+' elevated');
}

/* ════════ BOOT ════════ */
$('#scanAll').onclick=scanAll;
$('#resetLayout').onclick=resetLayout;
const sortSel=$('#sortSel');
if(sortSel){ sortSel.value=sortMode;
  sortSel.onchange=()=>{ sortMode=sortSel.value; localStorage.setItem('pg.sort',sortMode); renderDrafts(); }; }
const soloDraft = applySoloMode();
if(!document.body.classList.contains('solo')){
  applyLayout();
  wireDragReorder();
}
wirePanelControls();
loadDrafts().then(()=>{ if(soloDraft) selectDraft(soloDraft); });
if(!document.body.classList.contains('solo')) autoLoadCandidates();
</script>
</body>
</html>